Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4051d347 |
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: hindsight
|
||||
description: Hindsight helm chart
|
||||
type: application
|
||||
version: 0.1.13
|
||||
appVersion: "0.1.13"
|
||||
version: 0.1.12
|
||||
appVersion: "0.1.12"
|
||||
keywords:
|
||||
- ai
|
||||
- memory
|
||||
|
||||
@@ -33,10 +33,6 @@ ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
|
||||
ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
|
||||
ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS"
|
||||
|
||||
# Optimization flags
|
||||
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
|
||||
ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
|
||||
|
||||
# Default values
|
||||
DEFAULT_DATABASE_URL = "pg0"
|
||||
DEFAULT_LLM_PROVIDER = "openai"
|
||||
@@ -111,10 +107,6 @@ class HindsightConfig:
|
||||
# Recall
|
||||
graph_retriever: str
|
||||
|
||||
# Optimization flags
|
||||
skip_llm_verification: bool
|
||||
lazy_reranker: bool
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "HindsightConfig":
|
||||
"""Create configuration from environment variables."""
|
||||
@@ -141,9 +133,6 @@ class HindsightConfig:
|
||||
mcp_enabled=os.getenv(ENV_MCP_ENABLED, str(DEFAULT_MCP_ENABLED)).lower() == "true",
|
||||
# Recall
|
||||
graph_retriever=os.getenv(ENV_GRAPH_RETRIEVER, DEFAULT_GRAPH_RETRIEVER),
|
||||
# Optimization flags
|
||||
skip_llm_verification=os.getenv(ENV_SKIP_LLM_VERIFICATION, "false").lower() == "true",
|
||||
lazy_reranker=os.getenv(ENV_LAZY_RERANKER, "false").lower() == "true",
|
||||
)
|
||||
|
||||
def get_llm_base_url(self) -> str:
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
"""
|
||||
Daemon mode support for Hindsight API.
|
||||
|
||||
Provides idle timeout and lockfile management for running as a background daemon.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default daemon configuration
|
||||
DEFAULT_DAEMON_PORT = 8889
|
||||
DEFAULT_IDLE_TIMEOUT = 0 # 0 = no auto-exit (hindsight-embed passes its own timeout)
|
||||
LOCKFILE_PATH = Path.home() / ".hindsight" / "daemon.lock"
|
||||
DAEMON_LOG_PATH = Path.home() / ".hindsight" / "daemon.log"
|
||||
|
||||
|
||||
class IdleTimeoutMiddleware:
|
||||
"""ASGI middleware that tracks activity and exits after idle timeout."""
|
||||
|
||||
def __init__(self, app, idle_timeout: int = DEFAULT_IDLE_TIMEOUT):
|
||||
self.app = app
|
||||
self.idle_timeout = idle_timeout
|
||||
self.last_activity = time.time()
|
||||
self._checker_task = None
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
# Update activity timestamp on each request
|
||||
self.last_activity = time.time()
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
def start_idle_checker(self):
|
||||
"""Start the background task that checks for idle timeout."""
|
||||
self._checker_task = asyncio.create_task(self._check_idle())
|
||||
|
||||
async def _check_idle(self):
|
||||
"""Background task that exits the process after idle timeout."""
|
||||
# If idle_timeout is 0, don't auto-exit
|
||||
if self.idle_timeout <= 0:
|
||||
return
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
idle_time = time.time() - self.last_activity
|
||||
if idle_time > self.idle_timeout:
|
||||
logger.info(f"Idle timeout reached ({self.idle_timeout}s), shutting down daemon")
|
||||
# Give a moment for any in-flight requests
|
||||
await asyncio.sleep(1)
|
||||
os._exit(0)
|
||||
|
||||
|
||||
class DaemonLock:
|
||||
"""
|
||||
File-based lock to prevent multiple daemon instances.
|
||||
|
||||
Uses fcntl.flock for atomic locking on Unix systems.
|
||||
"""
|
||||
|
||||
def __init__(self, lockfile: Path = LOCKFILE_PATH):
|
||||
self.lockfile = lockfile
|
||||
self._fd = None
|
||||
|
||||
def acquire(self) -> bool:
|
||||
"""
|
||||
Try to acquire the daemon lock.
|
||||
|
||||
Returns True if lock acquired, False if another daemon is running.
|
||||
"""
|
||||
self.lockfile.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
self._fd = open(self.lockfile, "w")
|
||||
fcntl.flock(self._fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
# Write PID for debugging
|
||||
self._fd.write(str(os.getpid()))
|
||||
self._fd.flush()
|
||||
return True
|
||||
except (IOError, OSError):
|
||||
# Lock is held by another process
|
||||
if self._fd:
|
||||
self._fd.close()
|
||||
self._fd = None
|
||||
return False
|
||||
|
||||
def release(self):
|
||||
"""Release the daemon lock."""
|
||||
if self._fd:
|
||||
try:
|
||||
fcntl.flock(self._fd.fileno(), fcntl.LOCK_UN)
|
||||
self._fd.close()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._fd = None
|
||||
# Remove lockfile
|
||||
try:
|
||||
self.lockfile.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""Check if the lock is held by another process."""
|
||||
if not self.lockfile.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
fd = open(self.lockfile, "r")
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
# We got the lock, so no one else has it
|
||||
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
|
||||
fd.close()
|
||||
return False
|
||||
except (IOError, OSError):
|
||||
return True
|
||||
|
||||
def get_pid(self) -> int | None:
|
||||
"""Get the PID of the daemon holding the lock."""
|
||||
if not self.lockfile.exists():
|
||||
return None
|
||||
try:
|
||||
with open(self.lockfile, "r") as f:
|
||||
return int(f.read().strip())
|
||||
except (ValueError, IOError):
|
||||
return None
|
||||
|
||||
|
||||
def daemonize():
|
||||
"""
|
||||
Fork the current process into a background daemon.
|
||||
|
||||
Uses double-fork technique to properly detach from terminal.
|
||||
"""
|
||||
# First fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# Parent exits
|
||||
sys.exit(0)
|
||||
|
||||
# Create new session
|
||||
os.setsid()
|
||||
|
||||
# Second fork to prevent zombie processes
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# Redirect standard file descriptors to log file
|
||||
DAEMON_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
# Redirect stdin to /dev/null
|
||||
with open("/dev/null", "r") as devnull:
|
||||
os.dup2(devnull.fileno(), sys.stdin.fileno())
|
||||
|
||||
# Redirect stdout/stderr to log file
|
||||
log_fd = open(DAEMON_LOG_PATH, "a")
|
||||
os.dup2(log_fd.fileno(), sys.stdout.fileno())
|
||||
os.dup2(log_fd.fileno(), sys.stderr.fileno())
|
||||
|
||||
|
||||
def check_daemon_running(port: int = DEFAULT_DAEMON_PORT) -> bool:
|
||||
"""Check if a daemon is running and responsive on the given port."""
|
||||
import socket
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex(("127.0.0.1", port))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def stop_daemon(port: int = DEFAULT_DAEMON_PORT) -> bool:
|
||||
"""Stop a running daemon by sending SIGTERM to the process."""
|
||||
lock = DaemonLock()
|
||||
pid = lock.get_pid()
|
||||
|
||||
if pid is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
import signal
|
||||
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
# Wait for process to exit
|
||||
for _ in range(50): # Wait up to 5 seconds
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
os.kill(pid, 0) # Check if process exists
|
||||
except OSError:
|
||||
return True # Process exited
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
@@ -202,8 +202,6 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
run_migrations: bool = True,
|
||||
operation_validator: "OperationValidatorExtension | None" = None,
|
||||
tenant_extension: "TenantExtension | None" = None,
|
||||
skip_llm_verification: bool | None = None,
|
||||
lazy_reranker: bool | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize the temporal + semantic memory system.
|
||||
@@ -229,23 +227,12 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
If provided, retain/recall/reflect operations will be validated.
|
||||
tenant_extension: Optional extension for multi-tenancy and API key authentication.
|
||||
If provided, operations require a RequestContext for authentication.
|
||||
skip_llm_verification: Skip LLM connection verification during initialization.
|
||||
Defaults to HINDSIGHT_API_SKIP_LLM_VERIFICATION env var or False.
|
||||
lazy_reranker: Delay reranker initialization until first use. Useful for retain-only
|
||||
operations that don't need the cross-encoder. Defaults to
|
||||
HINDSIGHT_API_LAZY_RERANKER env var or False.
|
||||
"""
|
||||
# Load config from environment for any missing parameters
|
||||
from ..config import get_config
|
||||
|
||||
config = get_config()
|
||||
|
||||
# Apply optimization flags from config if not explicitly provided
|
||||
self._skip_llm_verification = (
|
||||
skip_llm_verification if skip_llm_verification is not None else config.skip_llm_verification
|
||||
)
|
||||
self._lazy_reranker = lazy_reranker if lazy_reranker is not None else config.lazy_reranker
|
||||
|
||||
# Apply defaults from config
|
||||
db_url = db_url or config.database_url
|
||||
memory_llm_provider = memory_llm_provider or config.llm_provider
|
||||
@@ -605,8 +592,6 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
await loop.run_in_executor(None, lambda: asyncio.run(cross_encoder.initialize()))
|
||||
else:
|
||||
await cross_encoder.initialize()
|
||||
# Mark reranker as initialized
|
||||
self._cross_encoder_reranker._initialized = True
|
||||
|
||||
async def init_query_analyzer():
|
||||
"""Initialize query analyzer model."""
|
||||
@@ -615,26 +600,16 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
|
||||
async def verify_llm():
|
||||
"""Verify LLM connection is working."""
|
||||
if not self._skip_llm_verification:
|
||||
await self._llm_config.verify_connection()
|
||||
await self._llm_config.verify_connection()
|
||||
|
||||
# Build list of initialization tasks
|
||||
init_tasks = [
|
||||
# Run pg0 and all model initializations in parallel
|
||||
await asyncio.gather(
|
||||
start_pg0(),
|
||||
init_embeddings(),
|
||||
init_cross_encoder(),
|
||||
init_query_analyzer(),
|
||||
]
|
||||
|
||||
# Only init cross-encoder eagerly if not using lazy initialization
|
||||
if not self._lazy_reranker:
|
||||
init_tasks.append(init_cross_encoder())
|
||||
|
||||
# Only verify LLM if not skipping
|
||||
if not self._skip_llm_verification:
|
||||
init_tasks.append(verify_llm())
|
||||
|
||||
# Run pg0 and selected model initializations in parallel
|
||||
await asyncio.gather(*init_tasks)
|
||||
verify_llm(),
|
||||
)
|
||||
|
||||
# Run database migrations if enabled
|
||||
if self._run_migrations:
|
||||
@@ -1664,9 +1639,6 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
step_start = time.time()
|
||||
reranker_instance = self._cross_encoder_reranker
|
||||
|
||||
# Ensure reranker is initialized (for lazy initialization mode)
|
||||
await reranker_instance.ensure_initialized()
|
||||
|
||||
# Rerank using cross-encoder
|
||||
scored_results = reranker_instance.rerank(query, merged_candidates)
|
||||
|
||||
|
||||
@@ -26,23 +26,6 @@ class CrossEncoderReranker:
|
||||
|
||||
cross_encoder = create_cross_encoder_from_env()
|
||||
self.cross_encoder = cross_encoder
|
||||
self._initialized = False
|
||||
|
||||
async def ensure_initialized(self):
|
||||
"""Ensure the cross-encoder model is initialized (for lazy initialization)."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
import asyncio
|
||||
|
||||
cross_encoder = self.cross_encoder
|
||||
# For local providers, run in thread pool to avoid blocking event loop
|
||||
if cross_encoder.provider_name == "local":
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, lambda: asyncio.run(cross_encoder.initialize()))
|
||||
else:
|
||||
await cross_encoder.initialize()
|
||||
self._initialized = True
|
||||
|
||||
def rerank(self, query: str, candidates: list[MergedCandidate]) -> list[ScoredResult]:
|
||||
"""
|
||||
|
||||
@@ -4,9 +4,6 @@ Command-line interface for Hindsight API.
|
||||
Run the server with:
|
||||
hindsight-api
|
||||
|
||||
Run as background daemon:
|
||||
hindsight-api --daemon
|
||||
|
||||
Stop with Ctrl+C.
|
||||
"""
|
||||
|
||||
@@ -24,13 +21,9 @@ from . import MemoryEngine
|
||||
from .api import create_app
|
||||
from .banner import print_banner
|
||||
from .config import HindsightConfig, get_config
|
||||
from .daemon import (
|
||||
DEFAULT_DAEMON_PORT,
|
||||
DEFAULT_IDLE_TIMEOUT,
|
||||
DaemonLock,
|
||||
IdleTimeoutMiddleware,
|
||||
daemonize,
|
||||
)
|
||||
|
||||
print()
|
||||
print_banner()
|
||||
|
||||
# Filter deprecation warnings from third-party libraries
|
||||
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
||||
@@ -113,52 +106,8 @@ def main():
|
||||
parser.add_argument("--ssl-keyfile", default=None, help="SSL key file")
|
||||
parser.add_argument("--ssl-certfile", default=None, help="SSL certificate file")
|
||||
|
||||
# Daemon mode options
|
||||
parser.add_argument(
|
||||
"--daemon",
|
||||
action="store_true",
|
||||
help=f"Run as background daemon (uses port {DEFAULT_DAEMON_PORT}, auto-exits after idle)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--idle-timeout",
|
||||
type=int,
|
||||
default=DEFAULT_IDLE_TIMEOUT,
|
||||
help=f"Idle timeout in seconds before auto-exit in daemon mode (default: {DEFAULT_IDLE_TIMEOUT})",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Daemon mode handling
|
||||
if args.daemon:
|
||||
# Use fixed daemon port
|
||||
args.port = DEFAULT_DAEMON_PORT
|
||||
args.host = "127.0.0.1" # Only bind to localhost for security
|
||||
|
||||
# Check if another daemon is already running
|
||||
daemon_lock = DaemonLock()
|
||||
if not daemon_lock.acquire():
|
||||
print(f"Daemon already running (PID: {daemon_lock.get_pid()})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Fork into background
|
||||
daemonize()
|
||||
|
||||
# Re-acquire lock in child process
|
||||
daemon_lock = DaemonLock()
|
||||
if not daemon_lock.acquire():
|
||||
sys.exit(1)
|
||||
|
||||
# Register cleanup to release lock
|
||||
def release_lock():
|
||||
daemon_lock.release()
|
||||
|
||||
atexit.register(release_lock)
|
||||
|
||||
# Print banner (not in daemon mode)
|
||||
if not args.daemon:
|
||||
print()
|
||||
print_banner()
|
||||
|
||||
# Configure Python logging based on log level
|
||||
# Update config with CLI override if provided
|
||||
if args.log_level != config.log_level:
|
||||
@@ -179,12 +128,9 @@ def main():
|
||||
log_level=args.log_level,
|
||||
mcp_enabled=config.mcp_enabled,
|
||||
graph_retriever=config.graph_retriever,
|
||||
skip_llm_verification=config.skip_llm_verification,
|
||||
lazy_reranker=config.lazy_reranker,
|
||||
)
|
||||
config.configure_logging()
|
||||
if not args.daemon:
|
||||
config.log_config()
|
||||
config.log_config()
|
||||
|
||||
# Register cleanup handlers
|
||||
atexit.register(_cleanup)
|
||||
@@ -203,12 +149,6 @@ def main():
|
||||
initialize_memory=True,
|
||||
)
|
||||
|
||||
# Wrap with idle timeout middleware in daemon mode
|
||||
idle_middleware = None
|
||||
if args.daemon:
|
||||
idle_middleware = IdleTimeoutMiddleware(app, idle_timeout=args.idle_timeout)
|
||||
app = idle_middleware
|
||||
|
||||
# Prepare uvicorn config
|
||||
uvicorn_config = {
|
||||
"app": app,
|
||||
@@ -232,38 +172,18 @@ def main():
|
||||
if args.ssl_certfile:
|
||||
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
||||
|
||||
# Print startup info (not in daemon mode)
|
||||
if not args.daemon:
|
||||
from .banner import print_startup_info
|
||||
from .banner import print_startup_info
|
||||
|
||||
print_startup_info(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
database_url=config.database_url,
|
||||
llm_provider=config.llm_provider,
|
||||
llm_model=config.llm_model,
|
||||
embeddings_provider=config.embeddings_provider,
|
||||
reranker_provider=config.reranker_provider,
|
||||
mcp_enabled=config.mcp_enabled,
|
||||
)
|
||||
|
||||
# Start idle checker in daemon mode
|
||||
if idle_middleware is not None:
|
||||
# Start the idle checker in a background thread with its own event loop
|
||||
import threading
|
||||
|
||||
def run_idle_checker():
|
||||
import time
|
||||
|
||||
time.sleep(2) # Wait for uvicorn to start
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(idle_middleware._check_idle())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=run_idle_checker, daemon=True).start()
|
||||
print_startup_info(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
database_url=config.database_url,
|
||||
llm_provider=config.llm_provider,
|
||||
llm_model=config.llm_model,
|
||||
embeddings_provider=config.embeddings_provider,
|
||||
reranker_provider=config.reranker_provider,
|
||||
mcp_enabled=config.mcp_enabled,
|
||||
)
|
||||
|
||||
uvicorn.run(**uvicorn_config) # type: ignore[invalid-argument-type] - dict kwargs
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hindsight-cli"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
edition = "2021"
|
||||
authors = ["Hindsight Team"]
|
||||
description = "A beautiful CLI for Hindsight - semantic memory system"
|
||||
|
||||
@@ -85,21 +85,9 @@ class Hindsight:
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""Close the API client (sync version - use aclose() in async code)."""
|
||||
"""Close the API client."""
|
||||
if self._api_client:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# We're in an async context - schedule but don't wait
|
||||
# The caller should use aclose() instead
|
||||
loop.create_task(self._api_client.close())
|
||||
except RuntimeError:
|
||||
# No running loop - safe to run synchronously
|
||||
_run_async(self._api_client.close())
|
||||
|
||||
async def aclose(self):
|
||||
"""Close the API client (async version)."""
|
||||
if self._api_client:
|
||||
await self._api_client.close()
|
||||
_run_async(self._api_client.close())
|
||||
|
||||
# Simplified methods for main operations
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "hindsight-client"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
description = "Python client for Hindsight - Semantic memory system with personality-driven thinking"
|
||||
authors = [
|
||||
{name = "Hindsight Team"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.12",
|
||||
"description": "TypeScript client for Hindsight - Semantic memory system with personality-driven thinking",
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-control-plane",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.12",
|
||||
"description": "Control plane for Hindsight - Semantic memory system",
|
||||
"bin": {
|
||||
"hindsight-control-plane": "./bin/cli.js"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-dev"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
description = "Development utilities for Hindsight"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -76,45 +76,24 @@ You have access to persistent memory via the `hindsight-embed` CLI. Use it to re
|
||||
|
||||
Run: `uvx hindsight-embed configure`
|
||||
|
||||
This will configure your LLM provider and start a local daemon that manages your memory bank.
|
||||
|
||||
## Commands
|
||||
|
||||
The CLI uses a bank ID to organize memories. Use `default` for general memories or create project-specific banks.
|
||||
|
||||
### Store a memory
|
||||
|
||||
Use `memory retain` to store important facts, preferences, decisions, or context:
|
||||
Use `retain` to store important facts, preferences, decisions, or context:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed memory retain default "User prefers dark mode for all UIs"
|
||||
uvx hindsight-embed memory retain default "Project uses Python 3.11 with FastAPI" --context work
|
||||
uvx hindsight-embed memory retain myproject "API uses JWT authentication"
|
||||
uvx hindsight-embed retain "User prefers dark mode for all UIs"
|
||||
uvx hindsight-embed retain "Project uses Python 3.11 with FastAPI" --context work
|
||||
```
|
||||
|
||||
### Recall memories
|
||||
|
||||
Use `memory recall` to search for relevant memories before starting tasks:
|
||||
Use `recall` to search for relevant memories before starting tasks:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed memory recall default "What are the user'"'"'s UI preferences?"
|
||||
uvx hindsight-embed memory recall default "What tech stack does this project use?"
|
||||
```
|
||||
|
||||
### Reflect on memories
|
||||
|
||||
Use `memory reflect` for contextual answers that synthesize multiple memories:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed memory reflect default "How should I set up the dev environment?"
|
||||
```
|
||||
|
||||
### Other commands
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed bank list # List all memory banks
|
||||
uvx hindsight-embed daemon status # Check daemon status
|
||||
uvx hindsight-embed --help # Full CLI help
|
||||
uvx hindsight-embed recall "What are the user'"'"'s UI preferences?"
|
||||
uvx hindsight-embed recall "What tech stack does this project use?"
|
||||
```
|
||||
|
||||
## When to Use
|
||||
@@ -135,7 +114,6 @@ uvx hindsight-embed --help # Full CLI help
|
||||
1. **Be specific**: Store "User prefers 2-space indentation" not "User has preferences"
|
||||
2. **Recall first**: Before starting tasks, recall relevant context
|
||||
3. **Use context tags**: Organize with `--context` (work, personal, preferences)
|
||||
4. **Use project banks**: Create separate banks for different projects
|
||||
'
|
||||
|
||||
# Get skills directory for app (bash 3.x compatible)
|
||||
@@ -193,34 +171,22 @@ print_banner
|
||||
|
||||
# Validate app parameter
|
||||
if [ -z "$APP" ]; then
|
||||
# Check if we have a terminal for interactive prompts
|
||||
if [ -t 0 ] || [ -e /dev/tty ]; then
|
||||
echo -e "${DIM}Select your AI coding assistant:${NC}"
|
||||
echo ""
|
||||
echo " ${BOLD}1)${NC} Claude Code"
|
||||
echo " ${BOLD}2)${NC} OpenCode"
|
||||
echo " ${BOLD}3)${NC} Codex CLI"
|
||||
echo ""
|
||||
# Use /dev/tty for input if stdin is piped
|
||||
if [ -t 0 ]; then
|
||||
read -p "Enter choice [1]: " app_choice
|
||||
else
|
||||
read -p "Enter choice [1]: " app_choice </dev/tty
|
||||
fi
|
||||
app_choice=${app_choice:-1}
|
||||
echo -e "${DIM}Select your AI coding assistant:${NC}"
|
||||
echo ""
|
||||
echo " ${BOLD}1)${NC} Claude Code"
|
||||
echo " ${BOLD}2)${NC} OpenCode"
|
||||
echo " ${BOLD}3)${NC} Codex CLI"
|
||||
echo ""
|
||||
read -p "Enter choice [1]: " app_choice
|
||||
app_choice=${app_choice:-1}
|
||||
|
||||
case $app_choice in
|
||||
1) APP="claude" ;;
|
||||
2) APP="opencode" ;;
|
||||
3) APP="codex" ;;
|
||||
*) APP="claude" ;;
|
||||
esac
|
||||
echo ""
|
||||
else
|
||||
# Non-interactive (CI) - default to claude
|
||||
APP="claude"
|
||||
print_info "Non-interactive mode detected, defaulting to Claude Code"
|
||||
fi
|
||||
case $app_choice in
|
||||
1) APP="claude" ;;
|
||||
2) APP="opencode" ;;
|
||||
3) APP="codex" ;;
|
||||
*) APP="claude" ;;
|
||||
esac
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Get skills directory for selected app
|
||||
@@ -269,8 +235,8 @@ echo ""
|
||||
echo -e " The Hindsight skill is now available in ${BOLD}$APP_NAME${NC}."
|
||||
echo ""
|
||||
echo -e " ${DIM}Test the CLI:${NC}"
|
||||
echo -e " ${CYAN}uvx hindsight-embed memory retain default \"Test memory\"${NC}"
|
||||
echo -e " ${CYAN}uvx hindsight-embed memory recall default \"test\"${NC}"
|
||||
echo -e " ${CYAN}uvx hindsight-embed retain \"Test memory\"${NC}"
|
||||
echo -e " ${CYAN}uvx hindsight-embed recall \"test\"${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}$APP_NAME will automatically use the skill when relevant.${NC}"
|
||||
echo ""
|
||||
|
||||
+16
-113
@@ -1,18 +1,8 @@
|
||||
# hindsight-embed
|
||||
|
||||
Hindsight embedded CLI - local memory operations with automatic daemon management.
|
||||
Hindsight embedded CLI - local memory operations without a server.
|
||||
|
||||
This package provides a simple CLI for storing and recalling memories using Hindsight's memory engine. It automatically manages a background daemon for fast operations - no manual server setup required.
|
||||
|
||||
## How It Works
|
||||
|
||||
`hindsight-embed` uses a background daemon architecture for optimal performance:
|
||||
|
||||
1. **First command**: Automatically starts a local daemon (first run downloads dependencies and loads ML models - can take 1-3 minutes)
|
||||
2. **Subsequent commands**: Near-instant responses (~1-2s) since daemon is already running
|
||||
3. **Auto-shutdown**: Daemon automatically exits after 5 minutes of inactivity
|
||||
|
||||
The daemon runs on `localhost:8889` and uses an embedded PostgreSQL database (pg0) - everything stays local on your machine.
|
||||
This package provides a simple CLI for storing and recalling memories using Hindsight's memory engine with an embedded PostgreSQL database (pg0). No external server or database setup required.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -25,115 +15,49 @@ uvx hindsight-embed --help
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Interactive setup (recommended)
|
||||
hindsight-embed configure
|
||||
|
||||
# Or set your LLM API key manually
|
||||
# Set your LLM API key
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
# Store a memory (bank_id = "default")
|
||||
hindsight-embed memory retain default "User prefers dark mode"
|
||||
# Store a memory
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
|
||||
# Recall memories
|
||||
hindsight-embed memory recall default "What are user preferences?"
|
||||
hindsight-embed recall "What are user preferences?"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### configure
|
||||
|
||||
Interactive setup wizard:
|
||||
|
||||
```bash
|
||||
hindsight-embed configure
|
||||
```
|
||||
|
||||
This will:
|
||||
- Let you choose an LLM provider (OpenAI, Groq, Google, Ollama)
|
||||
- Configure your API key
|
||||
- Set the model and memory bank ID
|
||||
- Start the daemon with your configuration
|
||||
|
||||
### memory retain
|
||||
### retain
|
||||
|
||||
Store a memory:
|
||||
|
||||
```bash
|
||||
hindsight-embed memory retain default "User prefers dark mode"
|
||||
hindsight-embed memory retain default "Meeting on Monday" --context work
|
||||
hindsight-embed memory retain myproject "API uses JWT authentication"
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
hindsight-embed retain "Meeting on Monday" --context work
|
||||
```
|
||||
|
||||
### memory recall
|
||||
### recall
|
||||
|
||||
Search memories:
|
||||
|
||||
```bash
|
||||
hindsight-embed memory recall default "user preferences"
|
||||
hindsight-embed memory recall default "upcoming events"
|
||||
hindsight-embed recall "user preferences"
|
||||
hindsight-embed recall "upcoming events" --budget high
|
||||
hindsight-embed recall "project details" -v # verbose output
|
||||
```
|
||||
|
||||
Use `-o json` for JSON output:
|
||||
```bash
|
||||
hindsight-embed memory recall default "user preferences" -o json
|
||||
```
|
||||
|
||||
### memory reflect
|
||||
|
||||
Get contextual answers that synthesize multiple memories:
|
||||
|
||||
```bash
|
||||
hindsight-embed memory reflect default "How should I set up the dev environment?"
|
||||
```
|
||||
|
||||
### bank list
|
||||
|
||||
List all memory banks:
|
||||
|
||||
```bash
|
||||
hindsight-embed bank list
|
||||
```
|
||||
|
||||
### daemon
|
||||
|
||||
Manage the background daemon:
|
||||
|
||||
```bash
|
||||
hindsight-embed daemon status # Check if daemon is running
|
||||
hindsight-embed daemon start # Start the daemon
|
||||
hindsight-embed daemon stop # Stop the daemon
|
||||
hindsight-embed daemon logs # View last 50 lines of logs
|
||||
hindsight-embed daemon logs -f # Follow logs in real-time
|
||||
hindsight-embed daemon logs -n 100 # View last 100 lines
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Interactive Setup
|
||||
|
||||
Run `hindsight-embed configure` for a guided setup that saves to `~/.hindsight/embed`.
|
||||
|
||||
### Environment Variables
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_EMBED_LLM_API_KEY` | LLM API key (or use `OPENAI_API_KEY`) | Required |
|
||||
| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider (`openai`, `groq`, `google`, `ollama`) | `openai` |
|
||||
| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider (`openai`, `anthropic`, `google`, `ollama`) | `openai` |
|
||||
| `HINDSIGHT_EMBED_LLM_MODEL` | LLM model | `gpt-4o-mini` |
|
||||
| `HINDSIGHT_EMBED_BANK_ID` | Memory bank ID | `default` |
|
||||
|
||||
### Files
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `~/.hindsight/embed` | Configuration file |
|
||||
| `~/.hindsight/config.env` | Alternative config file location |
|
||||
| `~/.hindsight/daemon.log` | Daemon logs |
|
||||
| `~/.hindsight/daemon.lock` | Daemon lock file (PID) |
|
||||
|
||||
## Use with AI Coding Assistants
|
||||
|
||||
This CLI is designed to work with AI coding assistants like Claude Code, Cursor, and Windsurf. Install the Hindsight skill:
|
||||
This CLI is designed to work with AI coding assistants like Claude Code, OpenCode, and Codex CLI. Install the Hindsight skill:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash
|
||||
@@ -141,27 +65,6 @@ curl -fsSL https://hindsight.vectorize.io/get-skill | bash
|
||||
|
||||
This will configure the LLM provider and install the skill to your assistant's skills directory.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Daemon won't start:**
|
||||
```bash
|
||||
# Check logs for errors
|
||||
hindsight-embed daemon logs
|
||||
|
||||
# Stop any stuck daemon and restart
|
||||
hindsight-embed daemon stop
|
||||
hindsight-embed daemon start
|
||||
```
|
||||
|
||||
**Slow first command:**
|
||||
This is expected - the first command needs to download dependencies, start the daemon, and load ML models. First run can take 1-3 minutes depending on network speed. Subsequent commands will be fast (~1-2s).
|
||||
|
||||
**Change configuration:**
|
||||
```bash
|
||||
# Re-run configure (automatically restarts daemon)
|
||||
hindsight-embed configure
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
"""
|
||||
Hindsight Embedded CLI.
|
||||
|
||||
A wrapper CLI that manages a local daemon and forwards commands to hindsight-cli.
|
||||
No external server required - runs everything locally with automatic daemon management.
|
||||
A simple CLI for local memory operations using embedded PostgreSQL (pg0).
|
||||
No external server required - runs everything locally.
|
||||
|
||||
Usage:
|
||||
hindsight-embed configure # Interactive setup
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
hindsight-embed recall "What are user preferences?"
|
||||
hindsight-embed daemon status # Check daemon status
|
||||
|
||||
Environment variables:
|
||||
HINDSIGHT_EMBED_LLM_API_KEY: Required. API key for LLM provider.
|
||||
HINDSIGHT_EMBED_LLM_PROVIDER: Optional. LLM provider (default: "openai").
|
||||
HINDSIGHT_EMBED_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
|
||||
HINDSIGHT_EMBED_BANK_ID: Optional. Memory bank ID (default: "default").
|
||||
HINDSIGHT_EMBED_LOG_LEVEL: Optional. Log level (default: "warning").
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@@ -25,12 +26,11 @@ from pathlib import Path
|
||||
|
||||
CONFIG_DIR = Path.home() / ".hindsight"
|
||||
CONFIG_FILE = CONFIG_DIR / "embed"
|
||||
CONFIG_FILE_ALT = CONFIG_DIR / "config.env" # Alternative config file location
|
||||
|
||||
|
||||
def setup_logging(verbose: bool = False):
|
||||
"""Configure logging."""
|
||||
level_str = os.environ.get("HINDSIGHT_EMBED_LOG_LEVEL", "info").lower()
|
||||
level_str = os.environ.get("HINDSIGHT_EMBED_LOG_LEVEL", "warning").lower()
|
||||
if verbose:
|
||||
level_str = "debug"
|
||||
|
||||
@@ -40,7 +40,7 @@ def setup_logging(verbose: bool = False):
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
}
|
||||
level = level_map.get(level_str, logging.INFO)
|
||||
level = level_map.get(level_str, logging.WARNING)
|
||||
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
@@ -52,20 +52,17 @@ def setup_logging(verbose: bool = False):
|
||||
|
||||
def load_config_file():
|
||||
"""Load configuration from file if it exists."""
|
||||
# Check both config file locations
|
||||
config_files = [CONFIG_FILE, CONFIG_FILE_ALT]
|
||||
for config_path in config_files:
|
||||
if config_path.exists():
|
||||
with open(config_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
# Handle 'export VAR=value' format
|
||||
if line.startswith("export "):
|
||||
line = line[7:]
|
||||
key, value = line.split("=", 1)
|
||||
if key not in os.environ: # Don't override env vars
|
||||
os.environ[key] = value
|
||||
if CONFIG_FILE.exists():
|
||||
with open(CONFIG_FILE) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
# Handle 'export VAR=value' format
|
||||
if line.startswith("export "):
|
||||
line = line[7:]
|
||||
key, value = line.split("=", 1)
|
||||
if key not in os.environ: # Don't override env vars
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def get_config():
|
||||
@@ -83,141 +80,22 @@ def get_config():
|
||||
}
|
||||
|
||||
|
||||
# Provider defaults: (provider_id, default_model, env_key_name)
|
||||
PROVIDER_DEFAULTS = {
|
||||
"openai": ("openai", "o3-mini", "OPENAI_API_KEY"),
|
||||
"groq": ("groq", "openai/gpt-oss-20b", "GROQ_API_KEY"),
|
||||
"google": ("google", "gemini-2.0-flash", "GOOGLE_API_KEY"),
|
||||
"ollama": ("ollama", "llama3.2", None),
|
||||
}
|
||||
|
||||
|
||||
def do_configure(args):
|
||||
"""Interactive configuration setup."""
|
||||
# If stdin is not a terminal (e.g., running via curl | bash),
|
||||
# redirect stdin from /dev/tty for interactive prompts
|
||||
original_stdin = None
|
||||
if not sys.stdin.isatty():
|
||||
try:
|
||||
original_stdin = sys.stdin
|
||||
sys.stdin = open('/dev/tty', 'r')
|
||||
except OSError:
|
||||
# No terminal available - try non-interactive mode with env vars
|
||||
return _do_configure_from_env()
|
||||
"""Interactive configuration setup with beautiful TUI."""
|
||||
import questionary
|
||||
from questionary import Style
|
||||
|
||||
try:
|
||||
return _do_configure_interactive()
|
||||
finally:
|
||||
if original_stdin is not None:
|
||||
sys.stdin.close()
|
||||
sys.stdin = original_stdin
|
||||
# Custom style for the prompts
|
||||
custom_style = Style([
|
||||
('qmark', 'fg:cyan bold'),
|
||||
('question', 'fg:white bold'),
|
||||
('answer', 'fg:cyan'),
|
||||
('pointer', 'fg:cyan bold'),
|
||||
('highlighted', 'fg:cyan bold'),
|
||||
('selected', 'fg:green'),
|
||||
('text', 'fg:white'),
|
||||
])
|
||||
|
||||
|
||||
def _do_configure_from_env():
|
||||
"""Non-interactive configuration from environment variables (for CI)."""
|
||||
# Check for required environment variables
|
||||
api_key = os.environ.get("HINDSIGHT_EMBED_LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
||||
provider = os.environ.get("HINDSIGHT_EMBED_LLM_PROVIDER", "openai")
|
||||
|
||||
if provider not in PROVIDER_DEFAULTS:
|
||||
print(f"Error: Unknown provider '{provider}'. Supported: {', '.join(PROVIDER_DEFAULTS.keys())}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
_, default_model, env_key = PROVIDER_DEFAULTS[provider]
|
||||
|
||||
# Check for API key (required for non-ollama providers)
|
||||
if not api_key and provider != "ollama":
|
||||
print("Error: Cannot run interactive configuration without a terminal.", file=sys.stderr)
|
||||
print("", file=sys.stderr)
|
||||
print("For non-interactive (CI) mode, set environment variables:", file=sys.stderr)
|
||||
print(f" HINDSIGHT_EMBED_LLM_API_KEY=<your-api-key>", file=sys.stderr)
|
||||
print(f" HINDSIGHT_EMBED_LLM_PROVIDER={provider} # optional, default: openai", file=sys.stderr)
|
||||
print(f" HINDSIGHT_EMBED_LLM_MODEL=<model> # optional, default: {default_model}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
model = os.environ.get("HINDSIGHT_EMBED_LLM_MODEL", default_model)
|
||||
bank_id = os.environ.get("HINDSIGHT_EMBED_BANK_ID", "default")
|
||||
|
||||
print()
|
||||
print("\033[1m\033[36m Hindsight Embed - Non-interactive Configuration\033[0m")
|
||||
print()
|
||||
print(f" \033[2mProvider:\033[0m {provider}")
|
||||
print(f" \033[2mModel:\033[0m {model}")
|
||||
print(f" \033[2mBank ID:\033[0m {bank_id}")
|
||||
|
||||
# Save configuration
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
f.write("# Hindsight Embed Configuration\n")
|
||||
f.write(f"# Generated by hindsight-embed configure (non-interactive)\n\n")
|
||||
f.write(f"HINDSIGHT_EMBED_LLM_PROVIDER={provider}\n")
|
||||
f.write(f"HINDSIGHT_EMBED_LLM_MODEL={model}\n")
|
||||
f.write(f"HINDSIGHT_EMBED_BANK_ID={bank_id}\n")
|
||||
if api_key:
|
||||
f.write(f"HINDSIGHT_EMBED_LLM_API_KEY={api_key}\n")
|
||||
|
||||
CONFIG_FILE.chmod(0o600)
|
||||
|
||||
print()
|
||||
print("\033[32m ✓ Configuration saved!\033[0m")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _prompt_choice(prompt: str, choices: list[tuple[str, str]], default: int = 1) -> str | None:
|
||||
"""Simple choice prompt that works with /dev/tty."""
|
||||
print(f"\033[1m{prompt}\033[0m")
|
||||
print()
|
||||
for i, (label, _) in enumerate(choices, 1):
|
||||
print(f" \033[36m{i})\033[0m {label}")
|
||||
print()
|
||||
try:
|
||||
response = input(f"Enter choice [{default}]: ").strip()
|
||||
if not response:
|
||||
return choices[default - 1][1]
|
||||
idx = int(response)
|
||||
if 1 <= idx <= len(choices):
|
||||
return choices[idx - 1][1]
|
||||
return choices[default - 1][1]
|
||||
except (ValueError, EOFError, KeyboardInterrupt):
|
||||
return None
|
||||
|
||||
|
||||
def _prompt_text(prompt: str, default: str = "") -> str | None:
|
||||
"""Simple text prompt."""
|
||||
try:
|
||||
suffix = f" [{default}]" if default else ""
|
||||
response = input(f"\033[1m{prompt}\033[0m{suffix}: ").strip()
|
||||
return response if response else default
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return None
|
||||
|
||||
|
||||
def _prompt_password(prompt: str) -> str | None:
|
||||
"""Simple password prompt."""
|
||||
import getpass
|
||||
try:
|
||||
return getpass.getpass(f"\033[1m{prompt}\033[0m: ")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return None
|
||||
|
||||
|
||||
def _prompt_confirm(prompt: str, default: bool = True) -> bool | None:
|
||||
"""Simple yes/no prompt."""
|
||||
suffix = "[Y/n]" if default else "[y/N]"
|
||||
try:
|
||||
response = input(f"\033[1m{prompt}\033[0m {suffix}: ").strip().lower()
|
||||
if not response:
|
||||
return default
|
||||
return response in ('y', 'yes')
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
return None
|
||||
|
||||
|
||||
def _do_configure_interactive():
|
||||
"""Internal interactive configuration."""
|
||||
print()
|
||||
print("\033[1m\033[36m ╭─────────────────────────────────────╮\033[0m")
|
||||
print("\033[1m\033[36m │ Hindsight Embed Configuration │\033[0m")
|
||||
@@ -226,53 +104,82 @@ def _do_configure_interactive():
|
||||
|
||||
# Check existing config
|
||||
if CONFIG_FILE.exists():
|
||||
if not _prompt_confirm("Existing configuration found. Reconfigure?", default=False):
|
||||
if not questionary.confirm(
|
||||
"Existing configuration found. Reconfigure?",
|
||||
default=False,
|
||||
style=custom_style,
|
||||
).ask():
|
||||
print("\n\033[32m✓\033[0m Keeping existing configuration.")
|
||||
return 0
|
||||
print()
|
||||
|
||||
# Provider selection
|
||||
# Provider selection with descriptions
|
||||
providers = [
|
||||
("OpenAI (recommended)", "openai"),
|
||||
("Groq (fast & free tier)", "groq"),
|
||||
("Google Gemini", "google"),
|
||||
("Ollama (local, no API key)", "ollama"),
|
||||
questionary.Choice("OpenAI (recommended)", value=("openai", "o3-mini", "OpenAI")),
|
||||
questionary.Choice("Groq (fast & free tier)", value=("groq", "openai/gpt-oss-20b", "Groq")),
|
||||
questionary.Choice("Google Gemini", value=("google", "gemini-2.0-flash", "Google")),
|
||||
questionary.Choice("Ollama (local, no API key)", value=("ollama", "llama3.2", None)),
|
||||
]
|
||||
|
||||
provider = _prompt_choice("Select your LLM provider:", providers, default=1)
|
||||
if provider is None:
|
||||
result = questionary.select(
|
||||
"Select your LLM provider:",
|
||||
choices=providers,
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if result is None: # User cancelled
|
||||
print("\n\033[33m⚠\033[0m Configuration cancelled.")
|
||||
return 1
|
||||
|
||||
_, default_model, env_key = PROVIDER_DEFAULTS[provider]
|
||||
print()
|
||||
provider, default_model, key_name = result
|
||||
|
||||
# API key
|
||||
api_key = ""
|
||||
if env_key:
|
||||
if key_name:
|
||||
env_keys = {
|
||||
"OpenAI": "OPENAI_API_KEY",
|
||||
"Groq": "GROQ_API_KEY",
|
||||
"Google": "GOOGLE_API_KEY",
|
||||
}
|
||||
env_key = env_keys.get(key_name, "")
|
||||
existing = os.environ.get(env_key, "")
|
||||
|
||||
if existing:
|
||||
masked = existing[:8] + "..." + existing[-4:] if len(existing) > 12 else "***"
|
||||
if _prompt_confirm(f"Found API key in ${env_key} ({masked}). Use it?", default=True):
|
||||
if questionary.confirm(
|
||||
f"Found {key_name} key in ${env_key} ({masked}). Use it?",
|
||||
default=True,
|
||||
style=custom_style,
|
||||
).ask():
|
||||
api_key = existing
|
||||
print()
|
||||
|
||||
if not api_key:
|
||||
api_key = _prompt_password("Enter your API key")
|
||||
api_key = questionary.password(
|
||||
f"Enter your {key_name} API key:",
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if not api_key:
|
||||
print("\n\033[31m✗\033[0m API key is required.", file=sys.stderr)
|
||||
return 1
|
||||
print()
|
||||
|
||||
# Model selection
|
||||
model = _prompt_text("Model name", default=default_model)
|
||||
model = questionary.text(
|
||||
"Model name:",
|
||||
default=default_model,
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if model is None:
|
||||
return 1
|
||||
print()
|
||||
|
||||
# Bank ID
|
||||
bank_id = _prompt_text("Memory bank ID", default="default")
|
||||
bank_id = questionary.text(
|
||||
"Memory bank ID:",
|
||||
default="default",
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if bank_id is None:
|
||||
return 1
|
||||
|
||||
@@ -290,25 +197,6 @@ def _do_configure_interactive():
|
||||
|
||||
CONFIG_FILE.chmod(0o600)
|
||||
|
||||
# Stop existing daemon if running (it needs to pick up new config)
|
||||
from . import daemon_client
|
||||
|
||||
if daemon_client._is_daemon_running():
|
||||
print("\n \033[2mRestarting daemon with new configuration...\033[0m")
|
||||
daemon_client.stop_daemon()
|
||||
|
||||
# Start daemon with new config
|
||||
new_config = {
|
||||
"llm_api_key": api_key,
|
||||
"llm_provider": provider,
|
||||
"llm_model": model,
|
||||
"bank_id": bank_id,
|
||||
}
|
||||
if daemon_client.ensure_daemon_running(new_config):
|
||||
print(" \033[32m✓ Daemon started\033[0m")
|
||||
else:
|
||||
print(" \033[33m⚠ Failed to start daemon (will start on first command)\033[0m")
|
||||
|
||||
print()
|
||||
print("\033[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
|
||||
print("\033[32m ✓ Configuration saved!\033[0m")
|
||||
@@ -324,173 +212,211 @@ def _do_configure_interactive():
|
||||
return 0
|
||||
|
||||
|
||||
def do_daemon(args, config: dict, logger):
|
||||
"""Handle daemon subcommands."""
|
||||
from pathlib import Path
|
||||
from . import daemon_client
|
||||
async def _create_engine(config: dict, logger):
|
||||
"""Create and initialize the memory engine."""
|
||||
logger.debug("Setting up environment variables...")
|
||||
|
||||
daemon_log_path = Path.home() / ".hindsight" / "daemon.log"
|
||||
# Set hindsight-api environment variables from our config
|
||||
if config["llm_api_key"]:
|
||||
os.environ["HINDSIGHT_API_LLM_API_KEY"] = config["llm_api_key"]
|
||||
if config["llm_provider"]:
|
||||
os.environ["HINDSIGHT_API_LLM_PROVIDER"] = config["llm_provider"]
|
||||
if config["llm_model"]:
|
||||
os.environ["HINDSIGHT_API_LLM_MODEL"] = config["llm_model"]
|
||||
|
||||
if args.daemon_command == "start":
|
||||
if daemon_client._is_daemon_running():
|
||||
print("Daemon is already running")
|
||||
return 0
|
||||
logger.debug("Importing MemoryEngine...")
|
||||
|
||||
print("Starting daemon...")
|
||||
if daemon_client.ensure_daemon_running(config):
|
||||
print("Daemon started successfully")
|
||||
print(f" Port: {daemon_client.DAEMON_PORT}")
|
||||
print(f" Logs: {daemon_log_path}")
|
||||
return 0
|
||||
# Import after setting env vars
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.engine.task_backend import SyncTaskBackend
|
||||
|
||||
# Use pg0 embedded database
|
||||
db_name = f"hindsight-embed-{config['bank_id']}"
|
||||
logger.debug(f"Creating MemoryEngine with pg0://{db_name}")
|
||||
|
||||
# Use SyncTaskBackend to avoid background workers that prevent clean exit
|
||||
memory = MemoryEngine(
|
||||
db_url=f"pg0://{db_name}",
|
||||
task_backend=SyncTaskBackend(),
|
||||
)
|
||||
|
||||
logger.debug("Initializing engine...")
|
||||
await memory.initialize()
|
||||
|
||||
logger.debug("Engine initialized")
|
||||
return memory
|
||||
|
||||
|
||||
async def do_retain(args, config: dict, logger):
|
||||
"""Execute retain command."""
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
logger.info(f"Retaining memory: {args.content[:50]}...")
|
||||
|
||||
memory = await _create_engine(config, logger)
|
||||
|
||||
try:
|
||||
logger.debug("Calling retain_batch_async...")
|
||||
await memory.retain_batch_async(
|
||||
bank_id=config["bank_id"],
|
||||
contents=[{
|
||||
"content": args.content,
|
||||
"context": args.context or "general",
|
||||
}],
|
||||
request_context=RequestContext(),
|
||||
)
|
||||
msg = f"Stored memory: {args.content[:50]}..." if len(args.content) > 50 else f"Stored memory: {args.content}"
|
||||
print(msg, flush=True)
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Retain failed: {e}", exc_info=True)
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
async def do_recall(args, config: dict, logger):
|
||||
"""Execute recall command."""
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
logger.info(f"Recalling with query: {args.query}")
|
||||
|
||||
memory = await _create_engine(config, logger)
|
||||
|
||||
try:
|
||||
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
|
||||
budget_enum = budget_map.get(args.budget.lower(), Budget.LOW)
|
||||
|
||||
logger.debug(f"Calling recall_async with budget={budget_enum}...")
|
||||
result = await memory.recall_async(
|
||||
bank_id=config["bank_id"],
|
||||
query=args.query,
|
||||
fact_type=list(VALID_RECALL_FACT_TYPES),
|
||||
budget=budget_enum,
|
||||
max_tokens=args.max_tokens,
|
||||
request_context=RequestContext(),
|
||||
)
|
||||
|
||||
logger.debug(f"Recall returned {len(result.results)} results")
|
||||
|
||||
if result.results:
|
||||
print("Memories found:", flush=True)
|
||||
print("-" * 40, flush=True)
|
||||
for fact in result.results:
|
||||
print(f"- {fact.text}", flush=True)
|
||||
if args.verbose and fact.occurred_start:
|
||||
print(f" (Date: {fact.occurred_start})", flush=True)
|
||||
print("-" * 40, flush=True)
|
||||
print(f"Total: {len(result.results)} memories", flush=True)
|
||||
else:
|
||||
print("Failed to start daemon", file=sys.stderr)
|
||||
return 1
|
||||
print("No relevant memories found.", flush=True)
|
||||
|
||||
elif args.daemon_command == "stop":
|
||||
if not daemon_client._is_daemon_running():
|
||||
print("Daemon is not running")
|
||||
return 0
|
||||
|
||||
print("Stopping daemon...")
|
||||
if daemon_client.stop_daemon():
|
||||
print("Daemon stopped")
|
||||
return 0
|
||||
else:
|
||||
print("Failed to stop daemon", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
elif args.daemon_command == "status":
|
||||
if daemon_client._is_daemon_running():
|
||||
# Get PID from lockfile
|
||||
lockfile = Path.home() / ".hindsight" / "daemon.lock"
|
||||
pid = "unknown"
|
||||
if lockfile.exists():
|
||||
try:
|
||||
pid = lockfile.read_text().strip()
|
||||
except Exception:
|
||||
pass
|
||||
print(f"Daemon is running (PID: {pid})")
|
||||
print(f" URL: http://127.0.0.1:{daemon_client.DAEMON_PORT}")
|
||||
print(f" Logs: {daemon_log_path}")
|
||||
return 0
|
||||
else:
|
||||
print("Daemon is not running")
|
||||
return 1
|
||||
|
||||
elif args.daemon_command == "logs":
|
||||
if not daemon_log_path.exists():
|
||||
print("No daemon logs found", file=sys.stderr)
|
||||
print(f" Expected at: {daemon_log_path}")
|
||||
return 1
|
||||
|
||||
if args.follow:
|
||||
# Follow mode - like tail -f
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(["tail", "-f", str(daemon_log_path)])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
else:
|
||||
# Show last N lines
|
||||
try:
|
||||
with open(daemon_log_path) as f:
|
||||
lines = f.readlines()
|
||||
for line in lines[-args.lines:]:
|
||||
print(line, end="")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error reading logs: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
else:
|
||||
print("Usage: hindsight-embed daemon {start|stop|status|logs}", file=sys.stderr)
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Recall failed: {e}", exc_info=True)
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
# Check for built-in commands first (before argparse)
|
||||
# This allows us to forward unknown commands to hindsight-cli
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Hindsight Embedded CLI - local memory operations without a server",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
hindsight-embed configure # Interactive setup
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
hindsight-embed retain "Meeting on Monday" -c work
|
||||
hindsight-embed recall "user preferences"
|
||||
hindsight-embed recall "meetings" --budget high
|
||||
"""
|
||||
)
|
||||
|
||||
# Handle configure
|
||||
if command == "configure":
|
||||
logger = setup_logging(False)
|
||||
exit_code = do_configure(None)
|
||||
sys.exit(exit_code)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Enable verbose/debug logging"
|
||||
)
|
||||
|
||||
# Handle daemon subcommands
|
||||
if command == "daemon":
|
||||
# Parse daemon subcommand
|
||||
parser = argparse.ArgumentParser(prog="hindsight-embed daemon")
|
||||
subparsers = parser.add_subparsers(dest="daemon_command")
|
||||
subparsers.add_parser("start", help="Start the daemon")
|
||||
subparsers.add_parser("stop", help="Stop the daemon")
|
||||
subparsers.add_parser("status", help="Check daemon status")
|
||||
logs_parser = subparsers.add_parser("logs", help="View daemon logs")
|
||||
logs_parser.add_argument("--follow", "-f", action="store_true")
|
||||
logs_parser.add_argument("--lines", "-n", type=int, default=50)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
args = parser.parse_args(sys.argv[2:])
|
||||
logger = setup_logging(False)
|
||||
config = get_config()
|
||||
exit_code = do_daemon(args, config, logger)
|
||||
sys.exit(exit_code)
|
||||
# Configure command
|
||||
subparsers.add_parser("configure", help="Interactive configuration setup")
|
||||
|
||||
# Handle --help / -h
|
||||
if command in ("--help", "-h"):
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
# Retain command
|
||||
retain_parser = subparsers.add_parser("retain", help="Store a memory")
|
||||
retain_parser.add_argument("content", help="The memory content to store")
|
||||
retain_parser.add_argument(
|
||||
"--context", "-c",
|
||||
help="Category for the memory (e.g., 'preferences', 'work')",
|
||||
default="general"
|
||||
)
|
||||
|
||||
# Forward all other commands to hindsight-cli
|
||||
config = get_config()
|
||||
# Recall command
|
||||
recall_parser = subparsers.add_parser("recall", help="Search memories")
|
||||
recall_parser.add_argument("query", help="Search query")
|
||||
recall_parser.add_argument(
|
||||
"--budget", "-b",
|
||||
choices=["low", "mid", "high"],
|
||||
default="low",
|
||||
help="Search budget level (default: low)"
|
||||
)
|
||||
recall_parser.add_argument(
|
||||
"--max-tokens", "-m",
|
||||
type=int,
|
||||
default=4096,
|
||||
help="Maximum tokens in results (default: 4096)"
|
||||
)
|
||||
recall_parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Show additional details"
|
||||
)
|
||||
|
||||
# Check for LLM API key
|
||||
if not config["llm_api_key"]:
|
||||
print("Error: LLM API key is required.", file=sys.stderr)
|
||||
print("Run 'hindsight-embed configure' to set up.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
args = parser.parse_args()
|
||||
|
||||
from . import daemon_client
|
||||
# Setup logging
|
||||
verbose = getattr(args, 'verbose', False)
|
||||
logger = setup_logging(verbose)
|
||||
|
||||
# Forward to hindsight-cli (handles daemon startup and CLI installation)
|
||||
exit_code = daemon_client.run_cli(sys.argv[1:], config)
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Handle configure separately (no config needed)
|
||||
if args.command == "configure":
|
||||
exit_code = do_configure(args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# No command - show help
|
||||
print_help()
|
||||
sys.exit(1)
|
||||
config = get_config()
|
||||
|
||||
# Check for LLM API key
|
||||
if not config["llm_api_key"]:
|
||||
print("Error: LLM API key is required.", file=sys.stderr)
|
||||
print("Run 'hindsight-embed configure' to set up.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def print_help():
|
||||
"""Print help message."""
|
||||
print("""Hindsight Embedded CLI - local memory operations with automatic daemon management.
|
||||
# Run the appropriate command
|
||||
exit_code = 1
|
||||
try:
|
||||
if args.command == "retain":
|
||||
exit_code = asyncio.run(do_retain(args, config, logger))
|
||||
elif args.command == "recall":
|
||||
exit_code = asyncio.run(do_recall(args, config, logger))
|
||||
else:
|
||||
parser.print_help()
|
||||
exit_code = 1
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Interrupted")
|
||||
exit_code = 130
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}", exc_info=True)
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
exit_code = 1
|
||||
|
||||
Usage: hindsight-embed <command> [options]
|
||||
|
||||
Built-in commands:
|
||||
configure Interactive configuration setup
|
||||
daemon start Start the background daemon
|
||||
daemon stop Stop the daemon
|
||||
daemon status Check daemon status
|
||||
daemon logs [-f] [-n] View daemon logs
|
||||
|
||||
CLI commands (forwarded to hindsight-cli):
|
||||
memory retain <bank> <content> Store a memory
|
||||
memory recall <bank> <query> Search memories
|
||||
memory reflect <bank> <query> Generate contextual answer
|
||||
bank list List memory banks
|
||||
... Run 'hindsight --help' for all commands
|
||||
|
||||
Examples:
|
||||
hindsight-embed configure
|
||||
hindsight-embed memory retain default "User prefers dark mode"
|
||||
hindsight-embed memory recall default "user preferences"
|
||||
hindsight-embed daemon status
|
||||
hindsight-embed daemon logs -f
|
||||
""")
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
"""
|
||||
Client for communicating with the Hindsight daemon.
|
||||
|
||||
Handles daemon lifecycle (start if needed) and API requests via the Python client.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx # Used only for health check
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DAEMON_PORT = 8889
|
||||
DAEMON_URL = f"http://127.0.0.1:{DAEMON_PORT}"
|
||||
DAEMON_STARTUP_TIMEOUT = 180 # seconds - needs to be long for first run (downloads dependencies)
|
||||
DAEMON_IDLE_TIMEOUT = 300 # 5 minutes - auto-exit after idle
|
||||
|
||||
# CLI paths - check multiple locations
|
||||
CLI_INSTALL_DIRS = [
|
||||
Path.home() / ".local" / "bin", # Standard location from get-cli installer
|
||||
Path.home() / ".hindsight" / "bin", # Alternative location
|
||||
]
|
||||
CLI_INSTALLER_URL = "https://hindsight.vectorize.io/get-cli"
|
||||
|
||||
|
||||
def _find_hindsight_api_command() -> list[str]:
|
||||
"""Find the command to run hindsight-api."""
|
||||
# Check if we're in development mode (local hindsight-api available)
|
||||
# Path: daemon_client.py -> hindsight_embed/ -> hindsight-embed/ -> memory-poc/
|
||||
dev_api_path = Path(__file__).parent.parent.parent / "hindsight-api"
|
||||
if dev_api_path.exists() and (dev_api_path / "pyproject.toml").exists():
|
||||
# Use uv run with the local project
|
||||
return ["uv", "run", "--project", str(dev_api_path), "hindsight-api"]
|
||||
|
||||
# Fall back to uvx for installed version
|
||||
return ["uvx", "hindsight-api"]
|
||||
|
||||
|
||||
def _is_daemon_running() -> bool:
|
||||
"""Check if daemon is running and responsive."""
|
||||
try:
|
||||
with httpx.Client(timeout=2) as client:
|
||||
response = client.get(f"{DAEMON_URL}/health")
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _start_daemon(config: dict) -> bool:
|
||||
"""
|
||||
Start the daemon in background.
|
||||
|
||||
Returns True if daemon started successfully.
|
||||
"""
|
||||
import sys
|
||||
|
||||
logger.info("Starting daemon...")
|
||||
|
||||
# Build environment with LLM config
|
||||
env = os.environ.copy()
|
||||
if config.get("llm_api_key"):
|
||||
env["HINDSIGHT_API_LLM_API_KEY"] = config["llm_api_key"]
|
||||
if config.get("llm_provider"):
|
||||
env["HINDSIGHT_API_LLM_PROVIDER"] = config["llm_provider"]
|
||||
if config.get("llm_model"):
|
||||
env["HINDSIGHT_API_LLM_MODEL"] = config["llm_model"]
|
||||
|
||||
# Use pg0 database specific to bank
|
||||
bank_id = config.get("bank_id", "default")
|
||||
env["HINDSIGHT_API_DATABASE_URL"] = f"pg0://hindsight-embed-{bank_id}"
|
||||
|
||||
# Optimization flags for faster startup
|
||||
env["HINDSIGHT_API_SKIP_LLM_VERIFICATION"] = "true"
|
||||
env["HINDSIGHT_API_LOG_LEVEL"] = "warning"
|
||||
|
||||
cmd = _find_hindsight_api_command() + ["--daemon", "--idle-timeout", str(DAEMON_IDLE_TIMEOUT)]
|
||||
|
||||
# Create log directory
|
||||
log_dir = Path.home() / ".hindsight"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
daemon_log = log_dir / "daemon.log"
|
||||
daemon_stderr = log_dir / "daemon.stderr"
|
||||
|
||||
print(f"Starting daemon with command: {' '.join(cmd)}", file=sys.stderr)
|
||||
print(f" Log file: {daemon_log}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
# Start daemon in background, but capture initial stderr for debugging
|
||||
with open(daemon_stderr, "w") as stderr_file:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=stderr_file,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
# Wait for daemon to be ready
|
||||
# Note: With --daemon flag, the parent process forks and exits immediately (code 0).
|
||||
# The child process (actual daemon) continues running. So we can't rely on process.poll()
|
||||
# to detect failures - we must use the health check.
|
||||
start_time = time.time()
|
||||
last_check_time = start_time
|
||||
while time.time() - start_time < DAEMON_STARTUP_TIMEOUT:
|
||||
if _is_daemon_running():
|
||||
logger.info("Daemon started successfully")
|
||||
return True
|
||||
|
||||
# Periodically log progress
|
||||
if time.time() - last_check_time > 5:
|
||||
elapsed = int(time.time() - start_time)
|
||||
print(f" Still waiting for daemon... ({elapsed}s elapsed)", file=sys.stderr)
|
||||
last_check_time = time.time()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
logger.error("Daemon failed to start within timeout")
|
||||
# Show logs on timeout
|
||||
if daemon_log.exists():
|
||||
log_content = daemon_log.read_text()
|
||||
if log_content:
|
||||
print(f"Daemon log:\n{log_content[-2000:]}", file=sys.stderr) # Last 2000 chars
|
||||
if daemon_stderr.exists():
|
||||
stderr_content = daemon_stderr.read_text()
|
||||
if stderr_content:
|
||||
print(f"Daemon stderr:\n{stderr_content}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"Command not found: {cmd[0]}", file=sys.stderr)
|
||||
print(f" Full command: {' '.join(cmd)}", file=sys.stderr)
|
||||
logger.error("hindsight-api command not found. Install with: pip install hindsight-api")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to start daemon: {e}", file=sys.stderr)
|
||||
logger.error(f"Failed to start daemon: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def ensure_daemon_running(config: dict) -> bool:
|
||||
"""
|
||||
Ensure daemon is running, starting it if needed.
|
||||
|
||||
Returns True if daemon is running.
|
||||
"""
|
||||
if _is_daemon_running():
|
||||
logger.debug("Daemon already running")
|
||||
return True
|
||||
|
||||
return _start_daemon(config)
|
||||
|
||||
|
||||
def stop_daemon() -> bool:
|
||||
"""Stop the running daemon."""
|
||||
# Try to kill by PID from lockfile
|
||||
lockfile = Path.home() / ".hindsight" / "daemon.lock"
|
||||
if lockfile.exists():
|
||||
try:
|
||||
pid = int(lockfile.read_text().strip())
|
||||
os.kill(pid, 15) # SIGTERM
|
||||
# Wait for process to exit
|
||||
for _ in range(50):
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
return True
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
return not _is_daemon_running()
|
||||
|
||||
|
||||
def find_cli_binary() -> Path | None:
|
||||
"""Find the hindsight CLI binary in known locations or PATH."""
|
||||
import shutil
|
||||
|
||||
# Check standard install locations
|
||||
for install_dir in CLI_INSTALL_DIRS:
|
||||
binary = install_dir / "hindsight"
|
||||
if binary.exists() and os.access(binary, os.X_OK):
|
||||
return binary
|
||||
|
||||
# Check PATH
|
||||
path_binary = shutil.which("hindsight")
|
||||
if path_binary:
|
||||
return Path(path_binary)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_cli_installed() -> bool:
|
||||
"""Check if the hindsight CLI is installed."""
|
||||
return find_cli_binary() is not None
|
||||
|
||||
|
||||
def install_cli() -> bool:
|
||||
"""
|
||||
Install the hindsight CLI using the official installer.
|
||||
|
||||
Returns True if installation succeeded.
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
print("Installing hindsight CLI...")
|
||||
print(f" Installer URL: {CLI_INSTALLER_URL}")
|
||||
|
||||
try:
|
||||
# Download and run installer
|
||||
result = subprocess.run(
|
||||
["bash", "-c", f"curl -fsSL {CLI_INSTALLER_URL} | bash"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"CLI installation failed (exit code {result.returncode}):", file=sys.stderr)
|
||||
if result.stdout:
|
||||
print(f" stdout: {result.stdout}", file=sys.stderr)
|
||||
if result.stderr:
|
||||
print(f" stderr: {result.stderr}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
cli_binary = find_cli_binary()
|
||||
if cli_binary:
|
||||
print(f"CLI installed to {cli_binary}")
|
||||
return True
|
||||
else:
|
||||
print("CLI installation completed but binary not found", file=sys.stderr)
|
||||
print(f" stdout: {result.stdout}", file=sys.stderr)
|
||||
print(f" stderr: {result.stderr}", file=sys.stderr)
|
||||
# Check known locations
|
||||
for install_dir in CLI_INSTALL_DIRS:
|
||||
binary = install_dir / "hindsight"
|
||||
print(f" Checking {binary}: exists={binary.exists()}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"CLI installation failed: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def ensure_cli_installed() -> bool:
|
||||
"""Ensure CLI is installed, installing if needed."""
|
||||
if is_cli_installed():
|
||||
return True
|
||||
return install_cli()
|
||||
|
||||
|
||||
def run_cli(args: list[str], config: dict) -> int:
|
||||
"""
|
||||
Run the hindsight CLI with the given arguments.
|
||||
|
||||
Ensures daemon is running and passes the API URL.
|
||||
|
||||
Args:
|
||||
args: CLI arguments (e.g., ["memory", "retain", "bank", "content"])
|
||||
config: Configuration dict with llm settings
|
||||
|
||||
Returns:
|
||||
Exit code from CLI
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Ensure CLI is installed
|
||||
if not ensure_cli_installed():
|
||||
return 1
|
||||
|
||||
cli_binary = find_cli_binary()
|
||||
if not cli_binary:
|
||||
print("Error: hindsight CLI not found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Ensure daemon is running
|
||||
if not ensure_daemon_running(config):
|
||||
print("Error: Failed to start daemon", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Build environment with API URL pointing to daemon
|
||||
env = os.environ.copy()
|
||||
env["HINDSIGHT_API_URL"] = DAEMON_URL
|
||||
|
||||
# Run CLI
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(cli_binary)] + args,
|
||||
env=env,
|
||||
)
|
||||
return result.returncode
|
||||
except Exception as e:
|
||||
print(f"Error running CLI: {e}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -9,7 +9,8 @@ description = "Hindsight embedded CLI - local memory operations without a server
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"httpx>=0.27.0",
|
||||
"hindsight-api>=0.1.11",
|
||||
"questionary>=2.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -17,3 +18,6 @@ hindsight-embed = "hindsight_embed.cli:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_embed"]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-api = { workspace = true }
|
||||
|
||||
+26
-194
@@ -1,236 +1,68 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Smoke test for hindsight-embed CLI with daemon mode
|
||||
# Tests retain and recall operations via the background daemon
|
||||
# Simple smoke test for hindsight-embed CLI
|
||||
# Tests retain and recall operations with embedded PostgreSQL
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
API_DIR="$(cd "$SCRIPT_DIR/../hindsight-api" && pwd)"
|
||||
|
||||
echo "=== Hindsight Embed Smoke Test (Daemon Mode) ==="
|
||||
echo "=== Hindsight Embed Smoke Test ==="
|
||||
|
||||
# Check required environment (load from config if not set)
|
||||
if [ -f ~/.hindsight/config.env ]; then
|
||||
source ~/.hindsight/config.env
|
||||
fi
|
||||
|
||||
if [ -z "$HINDSIGHT_EMBED_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
|
||||
echo "Error: HINDSIGHT_EMBED_LLM_API_KEY or OPENAI_API_KEY is required"
|
||||
# Check required environment
|
||||
if [ -z "$HINDSIGHT_EMBED_LLM_API_KEY" ]; then
|
||||
echo "Error: HINDSIGHT_EMBED_LLM_API_KEY is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use a unique bank ID for this test run
|
||||
BANK_ID="test-$$-$(date +%s)"
|
||||
echo "Using bank ID: $BANK_ID"
|
||||
echo "Script dir: $SCRIPT_DIR"
|
||||
echo "API dir: $API_DIR"
|
||||
export HINDSIGHT_EMBED_BANK_ID="test-$$-$(date +%s)"
|
||||
echo "Using bank ID: $HINDSIGHT_EMBED_BANK_ID"
|
||||
|
||||
# Debug: Check if hindsight CLI is available
|
||||
# Test 1: Retain a memory
|
||||
echo ""
|
||||
echo "Checking hindsight CLI availability..."
|
||||
if command -v hindsight &> /dev/null; then
|
||||
echo " hindsight CLI found at: $(which hindsight)"
|
||||
else
|
||||
echo " hindsight CLI not in PATH"
|
||||
if [ -f ~/.local/bin/hindsight ]; then
|
||||
echo " Found at ~/.local/bin/hindsight"
|
||||
else
|
||||
echo " Not found at ~/.local/bin/hindsight - attempting installation..."
|
||||
# Try to install the CLI
|
||||
if curl -fsSL https://hindsight.vectorize.io/get-cli | bash; then
|
||||
echo " CLI installation completed"
|
||||
if [ -f ~/.local/bin/hindsight ]; then
|
||||
echo " CLI now available at ~/.local/bin/hindsight"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
else
|
||||
echo " WARNING: CLI still not found after installation"
|
||||
ls -la ~/.local/bin/ 2>/dev/null || echo " ~/.local/bin does not exist"
|
||||
fi
|
||||
else
|
||||
echo " WARNING: CLI installation failed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show environment info for debugging
|
||||
echo ""
|
||||
echo "Environment:"
|
||||
echo " HINDSIGHT_EMBED_LLM_PROVIDER: ${HINDSIGHT_EMBED_LLM_PROVIDER:-not set}"
|
||||
echo " HINDSIGHT_EMBED_LLM_MODEL: ${HINDSIGHT_EMBED_LLM_MODEL:-not set}"
|
||||
echo " HINDSIGHT_EMBED_LLM_API_KEY: ${HINDSIGHT_EMBED_LLM_API_KEY:+set (hidden)}"
|
||||
echo " PATH includes ~/.local/bin: $(echo $PATH | grep -q "$HOME/.local/bin" && echo yes || echo no)"
|
||||
|
||||
# Final check that CLI is available before proceeding
|
||||
echo ""
|
||||
echo "Final CLI check before tests..."
|
||||
CLI_PATH=""
|
||||
if [ -f ~/.local/bin/hindsight ]; then
|
||||
CLI_PATH="$HOME/.local/bin/hindsight"
|
||||
elif command -v hindsight &> /dev/null; then
|
||||
CLI_PATH="$(which hindsight)"
|
||||
fi
|
||||
|
||||
if [ -n "$CLI_PATH" ]; then
|
||||
echo " CLI found at: $CLI_PATH"
|
||||
echo " CLI version: $($CLI_PATH --version 2>&1 || echo 'unknown')"
|
||||
else
|
||||
echo " ERROR: hindsight CLI not found. Tests cannot proceed."
|
||||
echo " The hindsight-embed package forwards commands to the hindsight CLI."
|
||||
echo " Please ensure the CLI is installed or check the get-cli installer output above."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop any existing daemon
|
||||
echo ""
|
||||
echo "Stopping any existing daemon..."
|
||||
uv run --project "$SCRIPT_DIR" hindsight-embed daemon stop 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Test 1: Retain (this should start the daemon)
|
||||
echo ""
|
||||
echo "Test 1: Retaining a memory (first call - daemon will start)..."
|
||||
START_TIME=$(python3 -c "import time; print(time.time())")
|
||||
set +e # Temporarily disable exit on error to capture output
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory retain "$BANK_ID" "The user's favorite color is blue" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
END_TIME=$(python3 -c "import time; print(time.time())")
|
||||
DURATION=$(python3 -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
||||
echo "Test 1: Retaining a memory..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed retain "The user's favorite color is blue" 2>&1)
|
||||
echo "$OUTPUT"
|
||||
echo "Duration: ${DURATION}s"
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo "FAIL: Command exited with code $EXIT_CODE"
|
||||
echo ""
|
||||
echo "Checking daemon logs..."
|
||||
if [ -f ~/.hindsight/daemon.log ]; then
|
||||
echo "=== daemon.log ==="
|
||||
tail -50 ~/.hindsight/daemon.log
|
||||
else
|
||||
echo "No daemon.log found"
|
||||
fi
|
||||
if [ -f ~/.hindsight/daemon.stderr ]; then
|
||||
echo ""
|
||||
echo "=== daemon.stderr ==="
|
||||
cat ~/.hindsight/daemon.stderr
|
||||
else
|
||||
echo "No daemon.stderr found"
|
||||
fi
|
||||
echo ""
|
||||
echo "Checking for hindsight-api..."
|
||||
which hindsight-api 2>/dev/null || echo "hindsight-api not in PATH"
|
||||
which uvx 2>/dev/null || echo "uvx not in PATH"
|
||||
which uv 2>/dev/null || echo "uv not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$OUTPUT" | grep -qi "retained"; then
|
||||
echo "FAIL: Expected 'retained' in output"
|
||||
if ! echo "$OUTPUT" | grep -q "Stored memory"; then
|
||||
echo "FAIL: Expected 'Stored memory' in output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory retained successfully"
|
||||
|
||||
# Test 2: Recall (daemon already running - should be faster)
|
||||
# Test 2: Recall the memory
|
||||
echo ""
|
||||
echo "Test 2: Recalling memories (daemon already running)..."
|
||||
START_TIME=$(python3 -c "import time; print(time.time())")
|
||||
set +e
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory recall "$BANK_ID" "What is the user's favorite color?" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
END_TIME=$(python3 -c "import time; print(time.time())")
|
||||
DURATION=$(python3 -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
||||
echo "Test 2: Recalling memories..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed recall "What is the user's favorite color?" 2>&1)
|
||||
echo "$OUTPUT"
|
||||
echo "Duration: ${DURATION}s"
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo "FAIL: Command exited with code $EXIT_CODE"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -qi "blue"; then
|
||||
echo "FAIL: Expected 'blue' in recall output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory recalled successfully"
|
||||
|
||||
# Test 3: Retain with context (daemon should still be running)
|
||||
# Test 3: Retain with context
|
||||
echo ""
|
||||
echo "Test 3: Retaining memory with context..."
|
||||
START_TIME=$(python3 -c "import time; print(time.time())")
|
||||
set +e
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory retain "$BANK_ID" "User prefers Python over JavaScript" --context work 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
END_TIME=$(python3 -c "import time; print(time.time())")
|
||||
DURATION=$(python3 -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed retain "User prefers Python over JavaScript" --context work 2>&1)
|
||||
echo "$OUTPUT"
|
||||
echo "Duration: ${DURATION}s"
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo "FAIL: Command exited with code $EXIT_CODE"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$OUTPUT" | grep -qi "retained"; then
|
||||
echo "FAIL: Expected 'retained' in output"
|
||||
if ! echo "$OUTPUT" | grep -q "Stored memory"; then
|
||||
echo "FAIL: Expected 'Stored memory' in output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory with context retained successfully"
|
||||
|
||||
# Test 4: Recall with JSON output
|
||||
# Test 4: Recall with budget
|
||||
echo ""
|
||||
echo "Test 4: Recalling with JSON output..."
|
||||
START_TIME=$(python3 -c "import time; print(time.time())")
|
||||
set +e
|
||||
JSON_OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed memory recall "$BANK_ID" "programming preferences" -o json 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
END_TIME=$(python3 -c "import time; print(time.time())")
|
||||
DURATION=$(python3 -c "print(f'{$END_TIME - $START_TIME:.2f}')")
|
||||
echo "$JSON_OUTPUT"
|
||||
echo "Duration: ${DURATION}s"
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo "FAIL: Command exited with code $EXIT_CODE"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$JSON_OUTPUT" | grep -qi "python"; then
|
||||
echo "Test 4: Recalling with budget..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed recall "programming preferences" --budget mid 2>&1)
|
||||
echo "$OUTPUT"
|
||||
if ! echo "$OUTPUT" | grep -qi "python"; then
|
||||
echo "FAIL: Expected 'Python' in recall output"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$JSON_OUTPUT" | python3 -c "import sys, json; json.load(sys.stdin)" 2>/dev/null; then
|
||||
echo "FAIL: Expected valid JSON output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory recalled with JSON format successfully"
|
||||
|
||||
# Test 5: Check daemon is running
|
||||
echo ""
|
||||
echo "Test 5: Verifying daemon is running..."
|
||||
if curl -s http://127.0.0.1:8889/health | grep -q "healthy"; then
|
||||
echo "PASS: Daemon is running and healthy"
|
||||
else
|
||||
echo "FAIL: Daemon is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: Daemon status command
|
||||
echo ""
|
||||
echo "Test 6: Testing daemon status command..."
|
||||
STATUS_OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed daemon status 2>&1)
|
||||
echo "$STATUS_OUTPUT"
|
||||
if ! echo "$STATUS_OUTPUT" | grep -qi "running"; then
|
||||
echo "FAIL: Expected 'running' in daemon status output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Daemon status command works"
|
||||
|
||||
# Cleanup: Stop daemon
|
||||
echo ""
|
||||
echo "Stopping daemon..."
|
||||
uv run --project "$SCRIPT_DIR" hindsight-embed daemon stop 2>/dev/null || true
|
||||
echo "PASS: Memory recalled with budget successfully"
|
||||
|
||||
echo ""
|
||||
echo "=== All tests passed! ==="
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "hindsight-litellm"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
description = "Universal LLM memory integration via LiteLLM - works with 100+ providers"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-all"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
description = "Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1292,7 +1292,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-all"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
source = { editable = "hindsight" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -1316,7 +1316,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-api"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
source = { editable = "hindsight-api" }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
@@ -1422,7 +1422,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
source = { editable = "hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1456,7 +1456,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-dev"
|
||||
version = "0.1.13"
|
||||
version = "0.1.12"
|
||||
source = { editable = "hindsight-dev" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -1494,11 +1494,15 @@ name = "hindsight-embed"
|
||||
version = "0.1.0"
|
||||
source = { editable = "hindsight-embed" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "hindsight-api" },
|
||||
{ name = "questionary" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "httpx", specifier = ">=0.27.0" }]
|
||||
requires-dist = [
|
||||
{ name = "hindsight-api", editable = "hindsight-api" },
|
||||
{ name = "questionary", specifier = ">=2.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
@@ -2911,6 +2915,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
@@ -3616,6 +3632,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "questionary"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.1.0"
|
||||
@@ -4937,6 +4965,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
|
||||
Reference in New Issue
Block a user