Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f397bc8fc | ||
|
|
6d982f9cea |
@@ -749,9 +749,9 @@ jobs:
|
||||
test-embed:
|
||||
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
|
||||
HINDSIGHT_EMBED_LLM_PROVIDER: groq
|
||||
HINDSIGHT_EMBED_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_EMBED_LLM_MODEL: openai/gpt-oss-20b
|
||||
# Prefer CPU-only PyTorch in CI
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
@@ -782,10 +782,6 @@ jobs:
|
||||
${{ runner.os }}-huggingface-embed-
|
||||
${{ runner.os }}-huggingface-
|
||||
|
||||
- name: Run unit and integration tests
|
||||
working-directory: ./hindsight-embed
|
||||
run: uv run pytest tests/ -v
|
||||
|
||||
- name: Run smoke test
|
||||
working-directory: ./hindsight-embed
|
||||
run: ./test.sh
|
||||
|
||||
@@ -11,7 +11,6 @@ from collections.abc import Sequence
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@@ -24,21 +23,8 @@ depends_on: str | Sequence[str] | None = None
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema - create all tables from scratch."""
|
||||
|
||||
# Note: pgvector extension is installed globally BEFORE migrations run
|
||||
# See migrations.py:run_migrations() - this ensures the extension is available
|
||||
# to all schemas, not just the one being migrated
|
||||
|
||||
# We keep this here as a fallback for backwards compatibility
|
||||
# This may fail if user lacks permissions, which is fine if extension already exists
|
||||
try:
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
except Exception:
|
||||
# Extension might already exist or user lacks permissions - verify it exists
|
||||
conn = op.get_bind()
|
||||
result = conn.execute(text("SELECT 1 FROM pg_extension WHERE extname = 'vector'")).fetchone()
|
||||
if not result:
|
||||
# Extension truly doesn't exist - re-raise the error
|
||||
raise
|
||||
# Enable required extensions
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
|
||||
# Create banks table
|
||||
op.create_table(
|
||||
|
||||
@@ -1410,7 +1410,7 @@ def create_app(
|
||||
poll_interval_ms=config.worker_poll_interval_ms,
|
||||
max_retries=config.worker_max_retries,
|
||||
schema=schema,
|
||||
tenant_extension=memory._tenant_extension,
|
||||
tenant_extension=getattr(memory, "_tenant_extension", None),
|
||||
max_slots=config.worker_max_slots,
|
||||
consolidation_max_slots=config.worker_consolidation_max_slots,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
Daemon mode support for Hindsight API.
|
||||
|
||||
Provides idle timeout for running as a background daemon.
|
||||
Provides idle timeout and lockfile management for running as a background daemon.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
@@ -16,9 +17,8 @@ logger = logging.getLogger(__name__)
|
||||
# Default daemon configuration
|
||||
DEFAULT_DAEMON_PORT = 8888
|
||||
DEFAULT_IDLE_TIMEOUT = 0 # 0 = no auto-exit (hindsight-embed passes its own timeout)
|
||||
|
||||
# Allow override via environment variable for profile-specific logs
|
||||
DAEMON_LOG_PATH = Path(os.getenv("HINDSIGHT_API_DAEMON_LOG", str(Path.home() / ".hindsight" / "daemon.log")))
|
||||
LOCKFILE_PATH = Path.home() / ".hindsight" / "daemon.lock"
|
||||
DAEMON_LOG_PATH = Path.home() / ".hindsight" / "daemon.log"
|
||||
|
||||
|
||||
class IdleTimeoutMiddleware:
|
||||
@@ -58,27 +58,97 @@ class IdleTimeoutMiddleware:
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
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 - detach from parent
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as e:
|
||||
sys.stderr.write(f"fork #1 failed: {e}\n")
|
||||
sys.exit(1)
|
||||
# First fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
# Parent exits
|
||||
sys.exit(0)
|
||||
|
||||
# Decouple from parent environment
|
||||
os.chdir("/")
|
||||
# Create new session
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
# Second fork - prevent zombie
|
||||
# Second fork to prevent zombie processes
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
@@ -111,3 +181,27 @@ def check_daemon_running(port: int = DEFAULT_DAEMON_PORT) -> bool:
|
||||
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
|
||||
|
||||
@@ -459,11 +459,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
# Store operation validator extension (optional)
|
||||
self._operation_validator = operation_validator
|
||||
|
||||
# Store tenant extension (always set, use default if none provided)
|
||||
if tenant_extension is None:
|
||||
from ..extensions.builtin.tenant import DefaultTenantExtension
|
||||
|
||||
tenant_extension = DefaultTenantExtension(config={})
|
||||
# Store tenant extension (optional)
|
||||
self._tenant_extension = tenant_extension
|
||||
|
||||
async def _validate_operation(self, validation_coro) -> None:
|
||||
@@ -501,18 +497,22 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
Raises:
|
||||
AuthenticationError: If authentication fails or request_context is missing when required.
|
||||
"""
|
||||
if self._tenant_extension is None:
|
||||
_current_schema.set("public")
|
||||
return "public"
|
||||
|
||||
from hindsight_api.extensions import AuthenticationError
|
||||
|
||||
if request_context is None:
|
||||
raise AuthenticationError("RequestContext is required")
|
||||
raise AuthenticationError("RequestContext is required when tenant extension is configured")
|
||||
|
||||
# For internal/background operations (e.g., worker tasks), skip extension authentication.
|
||||
# The task was already authenticated at submission time, and execute_task sets _current_schema
|
||||
# from the task's _schema field.
|
||||
# from the task's _schema field. For public schema tasks, _current_schema keeps its default "public".
|
||||
if request_context.internal:
|
||||
return _current_schema.get()
|
||||
|
||||
# Authenticate through tenant extension (always set, may be default no-auth extension)
|
||||
# Let AuthenticationError propagate - HTTP layer will convert to 401
|
||||
tenant_context = await self._tenant_extension.authenticate(request_context)
|
||||
|
||||
_current_schema.set(tenant_context.schema_name)
|
||||
@@ -939,34 +939,30 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
|
||||
if not self.db_url:
|
||||
raise ValueError("Database URL is required for migrations")
|
||||
|
||||
# Migrate all schemas from the tenant extension
|
||||
# The tenant extension is the single source of truth for which schemas exist
|
||||
logger.info("Running database migrations...")
|
||||
try:
|
||||
tenants = await self._tenant_extension.list_tenants()
|
||||
if tenants:
|
||||
logger.info(f"Running migrations on {len(tenants)} schema(s)...")
|
||||
for tenant in tenants:
|
||||
schema = tenant.schema
|
||||
if schema:
|
||||
try:
|
||||
run_migrations(self.db_url, schema=schema)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to migrate schema {schema}: {e}")
|
||||
logger.info("Schema migrations completed")
|
||||
# Use configured database schema for migrations (defaults to "public")
|
||||
run_migrations(self.db_url, schema=get_config().database_schema)
|
||||
|
||||
# Ensure embedding column dimension matches the model's dimension
|
||||
# This is done after migrations and after embeddings.initialize()
|
||||
for tenant in tenants:
|
||||
schema = tenant.schema
|
||||
if schema:
|
||||
try:
|
||||
ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=schema)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to ensure embedding dimension for schema {schema}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to run schema migrations: {e}")
|
||||
# Migrate all existing tenant schemas (if multi-tenant)
|
||||
if self._tenant_extension is not None:
|
||||
try:
|
||||
tenants = await self._tenant_extension.list_tenants()
|
||||
if tenants:
|
||||
logger.info(f"Running migrations on {len(tenants)} tenant schemas...")
|
||||
for tenant in tenants:
|
||||
schema = tenant.schema
|
||||
if schema and schema != "public":
|
||||
try:
|
||||
run_migrations(self.db_url, schema=schema)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to migrate tenant schema {schema}: {e}")
|
||||
logger.info("Tenant schema migrations completed")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to run tenant schema migrations: {e}")
|
||||
|
||||
# Ensure embedding column dimension matches the model's dimension
|
||||
# This is done after migrations and after embeddings.initialize()
|
||||
ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=get_config().database_schema)
|
||||
|
||||
logger.info(f"Connecting to PostgreSQL at {self.db_url}")
|
||||
|
||||
|
||||
@@ -5,42 +5,6 @@ from hindsight_api.extensions.tenant import AuthenticationError, Tenant, TenantC
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
|
||||
class DefaultTenantExtension(TenantExtension):
|
||||
"""
|
||||
Default single-tenant extension with no authentication.
|
||||
|
||||
This is the default extension used when no tenant extension is configured.
|
||||
It provides single-tenant behavior using the configured schema from
|
||||
HINDSIGHT_API_DATABASE_SCHEMA (defaults to 'public').
|
||||
|
||||
Features:
|
||||
- No authentication required (passes all requests)
|
||||
- Uses configured schema from environment
|
||||
- Perfect for single-tenant deployments without auth
|
||||
|
||||
Configuration:
|
||||
HINDSIGHT_API_DATABASE_SCHEMA=your-schema (optional, defaults to 'public')
|
||||
|
||||
This is automatically enabled by default. To use custom authentication,
|
||||
configure a different tenant extension:
|
||||
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict[str, str]):
|
||||
super().__init__(config)
|
||||
# Cache the schema at initialization for consistency
|
||||
# Support explicit schema override via config, otherwise use environment
|
||||
self._schema = config.get("schema", get_config().database_schema)
|
||||
|
||||
async def authenticate(self, context: RequestContext) -> TenantContext:
|
||||
"""Return configured schema without any authentication."""
|
||||
return TenantContext(schema_name=self._schema)
|
||||
|
||||
async def list_tenants(self) -> list[Tenant]:
|
||||
"""Return configured schema for single-tenant setup."""
|
||||
return [Tenant(schema=self._schema)]
|
||||
|
||||
|
||||
class ApiKeyTenantExtension(TenantExtension):
|
||||
"""
|
||||
Built-in tenant extension that validates API key against an environment variable.
|
||||
|
||||
@@ -27,6 +27,7 @@ from .config import DEFAULT_WORKERS, ENV_WORKERS, HindsightConfig, get_config
|
||||
from .daemon import (
|
||||
DEFAULT_DAEMON_PORT,
|
||||
DEFAULT_IDLE_TIMEOUT,
|
||||
DaemonLock,
|
||||
IdleTimeoutMiddleware,
|
||||
daemonize,
|
||||
)
|
||||
@@ -135,15 +136,30 @@ def main():
|
||||
|
||||
# Daemon mode handling
|
||||
if args.daemon:
|
||||
# Use port from args (may be custom for profiles)
|
||||
if args.port == config.port: # No custom port specified
|
||||
args.port = DEFAULT_DAEMON_PORT
|
||||
# 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
|
||||
# No lockfile needed - port binding prevents duplicate daemons
|
||||
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()
|
||||
|
||||
@@ -165,81 +165,6 @@ def run_migrations(
|
||||
logger.debug("Migration advisory lock acquired")
|
||||
|
||||
try:
|
||||
# Ensure pgvector extension is installed globally BEFORE schema migrations
|
||||
# This is critical: the extension must exist database-wide before any schema
|
||||
# migrations run, otherwise custom schemas won't have access to vector types
|
||||
logger.debug("Checking pgvector extension availability...")
|
||||
|
||||
# First, check if extension already exists
|
||||
ext_check = conn.execute(
|
||||
text(
|
||||
"SELECT extname, nspname FROM pg_extension e "
|
||||
"JOIN pg_namespace n ON e.extnamespace = n.oid "
|
||||
"WHERE extname = 'vector'"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if ext_check:
|
||||
# Extension exists - check if in correct schema
|
||||
ext_schema = ext_check[1]
|
||||
if ext_schema == "public":
|
||||
logger.info("pgvector extension found in public schema - ready to use")
|
||||
else:
|
||||
# Extension in wrong schema - try to fix if we have permissions
|
||||
logger.warning(
|
||||
f"pgvector extension found in schema '{ext_schema}' instead of 'public'. "
|
||||
f"Attempting to relocate..."
|
||||
)
|
||||
try:
|
||||
conn.execute(text("DROP EXTENSION vector CASCADE"))
|
||||
conn.execute(text("SET search_path TO public"))
|
||||
conn.execute(text("CREATE EXTENSION vector"))
|
||||
conn.commit()
|
||||
logger.info("pgvector extension relocated to public schema")
|
||||
except Exception as e:
|
||||
# Failed to relocate - log but don't fail if extension exists somewhere
|
||||
logger.warning(
|
||||
f"Could not relocate pgvector extension to public schema: {e}. "
|
||||
f"Continuing with extension in '{ext_schema}' schema."
|
||||
)
|
||||
conn.rollback()
|
||||
else:
|
||||
# Extension doesn't exist - try to install
|
||||
logger.info("pgvector extension not found, attempting to install...")
|
||||
try:
|
||||
conn.execute(text("SET search_path TO public"))
|
||||
conn.execute(text("CREATE EXTENSION vector"))
|
||||
conn.commit()
|
||||
logger.info("pgvector extension installed in public schema")
|
||||
except Exception as e:
|
||||
# Installation failed - this is only fatal if extension truly doesn't exist
|
||||
# Check one more time in case another process installed it
|
||||
conn.rollback()
|
||||
ext_recheck = conn.execute(
|
||||
text(
|
||||
"SELECT nspname FROM pg_extension e "
|
||||
"JOIN pg_namespace n ON e.extnamespace = n.oid "
|
||||
"WHERE extname = 'vector'"
|
||||
)
|
||||
).fetchone()
|
||||
|
||||
if ext_recheck:
|
||||
logger.warning(
|
||||
f"Could not install pgvector extension (permission denied?), "
|
||||
f"but extension exists in '{ext_recheck[0]}' schema. Continuing..."
|
||||
)
|
||||
else:
|
||||
# Extension truly doesn't exist and we can't install it
|
||||
logger.error(
|
||||
f"pgvector extension is not installed and cannot be installed: {e}. "
|
||||
f"Please ensure pgvector is installed by a database administrator. "
|
||||
f"See: https://github.com/pgvector/pgvector#installation"
|
||||
)
|
||||
raise RuntimeError(
|
||||
"pgvector extension is required but not installed. "
|
||||
"Please install it with: CREATE EXTENSION vector;"
|
||||
) from e
|
||||
|
||||
# Run migrations while holding the lock
|
||||
_run_migrations_internal(database_url, script_location, schema=schema)
|
||||
finally:
|
||||
|
||||
@@ -176,7 +176,7 @@ def main():
|
||||
nonlocal memory, poller
|
||||
import uvicorn
|
||||
|
||||
from ..extensions import OperationValidatorExtension, TenantExtension, load_extension
|
||||
from ..extensions import TenantExtension, load_extension
|
||||
|
||||
# Load tenant extension BEFORE creating MemoryEngine so it can
|
||||
# set correct schema context during task execution. Without this,
|
||||
@@ -184,12 +184,6 @@ def main():
|
||||
# causing worker writes to land in the wrong schema.
|
||||
tenant_extension = load_extension("TENANT", TenantExtension)
|
||||
|
||||
# Load operation validator so workers can record usage metering
|
||||
# for async operations (e.g. refresh_mental_model after consolidation)
|
||||
operation_validator = load_extension("OPERATION_VALIDATOR", OperationValidatorExtension)
|
||||
if operation_validator:
|
||||
logger.info(f"Loaded operation validator: {operation_validator.__class__.__name__}")
|
||||
|
||||
# Initialize MemoryEngine
|
||||
# Workers use SyncTaskBackend because they execute tasks directly,
|
||||
# they don't need to store tasks (they poll from DB)
|
||||
@@ -197,7 +191,6 @@ def main():
|
||||
run_migrations=False, # Workers don't run migrations
|
||||
task_backend=SyncTaskBackend(),
|
||||
tenant_extension=tenant_extension,
|
||||
operation_validator=operation_validator,
|
||||
)
|
||||
|
||||
await memory.initialize()
|
||||
@@ -229,30 +222,15 @@ def main():
|
||||
# Create the HTTP app for metrics/health
|
||||
app = create_worker_app(poller, memory)
|
||||
|
||||
# Setup signal handlers for graceful shutdown using asyncio
|
||||
# Setup signal handlers for graceful shutdown
|
||||
shutdown_requested = asyncio.Event()
|
||||
force_exit = False
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
def signal_handler(signum, frame):
|
||||
print(f"\nReceived signal {signum}, initiating graceful shutdown...")
|
||||
shutdown_requested.set()
|
||||
|
||||
def signal_handler():
|
||||
nonlocal force_exit
|
||||
if shutdown_requested.is_set():
|
||||
# Second signal = force exit
|
||||
print("\nReceived second signal, forcing immediate exit...")
|
||||
force_exit = True
|
||||
# Restore default handler so third signal kills process
|
||||
loop.remove_signal_handler(signal.SIGINT)
|
||||
loop.remove_signal_handler(signal.SIGTERM)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\nReceived shutdown signal, initiating graceful shutdown...")
|
||||
print("(Press Ctrl+C again to force immediate exit)")
|
||||
shutdown_requested.set()
|
||||
|
||||
# Use asyncio's signal handlers which work properly with the event loop
|
||||
loop.add_signal_handler(signal.SIGINT, signal_handler)
|
||||
loop.add_signal_handler(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Create uvicorn config and server
|
||||
uvicorn_config = uvicorn.Config(
|
||||
@@ -271,10 +249,7 @@ def main():
|
||||
print(f"Worker started. Metrics available at http://{args.http_host}:{args.http_port}/metrics")
|
||||
|
||||
# Wait for shutdown signal
|
||||
try:
|
||||
await shutdown_requested.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("\nReceived interrupt, initiating graceful shutdown...")
|
||||
await shutdown_requested.wait()
|
||||
|
||||
# Graceful shutdown
|
||||
print("Shutting down HTTP server...")
|
||||
|
||||
@@ -72,9 +72,9 @@ class WorkerPoller:
|
||||
executor: Async function to execute tasks (typically MemoryEngine.execute_task)
|
||||
poll_interval_ms: Interval between polls when no tasks found (milliseconds)
|
||||
max_retries: Maximum retry attempts before marking task as failed
|
||||
schema: Database schema for single-tenant support (deprecated, use tenant_extension)
|
||||
tenant_extension: Extension for dynamic multi-tenant discovery. If None, creates a
|
||||
DefaultTenantExtension with the configured schema.
|
||||
schema: Database schema for single-tenant support (ignored if tenant_extension is set)
|
||||
tenant_extension: Extension for dynamic multi-tenant discovery. If set, list_tenants()
|
||||
is called on each poll cycle to discover schemas dynamically.
|
||||
max_slots: Maximum concurrent tasks per worker
|
||||
consolidation_max_slots: Maximum concurrent consolidation tasks per worker
|
||||
"""
|
||||
@@ -84,13 +84,6 @@ class WorkerPoller:
|
||||
self._poll_interval_ms = poll_interval_ms
|
||||
self._max_retries = max_retries
|
||||
self._schema = schema
|
||||
# Always set tenant extension (use DefaultTenantExtension if none provided)
|
||||
if tenant_extension is None:
|
||||
from ..extensions.builtin.tenant import DefaultTenantExtension
|
||||
|
||||
# Pass schema parameter to DefaultTenantExtension if explicitly provided
|
||||
config = {"schema": schema} if schema else {}
|
||||
tenant_extension = DefaultTenantExtension(config=config)
|
||||
self._tenant_extension = tenant_extension
|
||||
self._max_slots = max_slots
|
||||
self._consolidation_max_slots = consolidation_max_slots
|
||||
@@ -107,11 +100,14 @@ class WorkerPoller:
|
||||
|
||||
async def _get_schemas(self) -> list[str | None]:
|
||||
"""Get list of schemas to poll. Returns [None] for default schema (no prefix)."""
|
||||
from ..config import DEFAULT_DATABASE_SCHEMA
|
||||
if self._tenant_extension is not None:
|
||||
from ..config import DEFAULT_DATABASE_SCHEMA
|
||||
|
||||
tenants = await self._tenant_extension.list_tenants()
|
||||
# Convert default schema to None for SQL compatibility (no prefix), keep others as-is
|
||||
return [t.schema if t.schema != DEFAULT_DATABASE_SCHEMA else None for t in tenants]
|
||||
tenants = await self._tenant_extension.list_tenants()
|
||||
# Convert default schema to None for SQL compatibility (no prefix), keep others as-is
|
||||
return [t.schema if t.schema != DEFAULT_DATABASE_SCHEMA else None for t in tenants]
|
||||
# Single schema mode
|
||||
return [self._schema]
|
||||
|
||||
async def _get_available_slots(self) -> tuple[int, int]:
|
||||
"""
|
||||
|
||||
@@ -26,12 +26,6 @@ export GEMINI_API_KEY="your-key"
|
||||
|
||||
# Option D: Groq (uses openai/gpt-oss-20b for memory extraction)
|
||||
export GROQ_API_KEY="your-key"
|
||||
|
||||
# Option E: Claude Code (uses claude-sonnet-4-20250514, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
|
||||
# Option F: OpenAI Codex (uses o3-mini, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
```
|
||||
|
||||
**Step 2: Install the plugin**
|
||||
@@ -47,7 +41,7 @@ openclaw gateway
|
||||
```
|
||||
|
||||
The plugin will automatically:
|
||||
- Start a local Hindsight daemon (port 9077)
|
||||
- Start a local Hindsight daemon (port 8888)
|
||||
- Capture conversations after each turn
|
||||
- Inject relevant memories before agent responses
|
||||
|
||||
@@ -74,7 +68,6 @@ Optional settings in `~/.openclaw/openclaw.json`:
|
||||
"hindsight-openclaw": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"apiPort": 9077,
|
||||
"daemonIdleTimeout": 0,
|
||||
"embedVersion": "latest"
|
||||
}
|
||||
@@ -85,7 +78,6 @@ Optional settings in `~/.openclaw/openclaw.json`:
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `apiPort` - Port for the openclaw profile daemon (default: `9077`)
|
||||
- `daemonIdleTimeout` - Seconds before daemon shuts down from inactivity (default: `0` = never)
|
||||
- `embedVersion` - hindsight-embed version (default: `"latest"`)
|
||||
- `bankMission` - Custom context for the memory bank (optional)
|
||||
@@ -94,14 +86,12 @@ Optional settings in `~/.openclaw/openclaw.json`:
|
||||
|
||||
The plugin auto-detects your LLM provider from these environment variables:
|
||||
|
||||
| Provider | Env Var | Default Model | Notes |
|
||||
|----------|---------|---------------|-------|
|
||||
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` | |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` | |
|
||||
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` | |
|
||||
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` | |
|
||||
| Claude Code | `HINDSIGHT_API_LLM_PROVIDER=claude-code` | `claude-sonnet-4-20250514` | No API key needed |
|
||||
| OpenAI Codex | `HINDSIGHT_API_LLM_PROVIDER=openai-codex` | `o3-mini` | No API key needed |
|
||||
| Provider | Env Var | Default Model |
|
||||
|----------|---------|---------------|
|
||||
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` |
|
||||
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
|
||||
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
|
||||
|
||||
**Override with explicit config:**
|
||||
|
||||
@@ -143,32 +133,32 @@ Useful for shared memory across multiple OpenClaw instances or production deploy
|
||||
View the daemon config that was written by the plugin:
|
||||
|
||||
```bash
|
||||
cat ~/.hindsight/profiles/openclaw.env
|
||||
cat ~/.hindsight/embed
|
||||
```
|
||||
|
||||
This shows the LLM provider, model, port, and other settings the daemon is using.
|
||||
This shows the LLM provider, model, and other settings the daemon is using.
|
||||
|
||||
### Check Daemon Status
|
||||
|
||||
```bash
|
||||
# Check if daemon is running
|
||||
uvx hindsight-embed@latest -p openclaw daemon status
|
||||
uvx hindsight-embed@latest daemon status
|
||||
|
||||
# View daemon logs
|
||||
tail -f ~/.hindsight/profiles/openclaw.log
|
||||
tail -f ~/.hindsight/daemon.log
|
||||
```
|
||||
|
||||
### Query Memories
|
||||
|
||||
```bash
|
||||
# Search memories
|
||||
uvx hindsight-embed@latest -p openclaw memory recall openclaw "user preferences"
|
||||
uvx hindsight-embed@latest memory recall openclaw "user preferences"
|
||||
|
||||
# View recent memories
|
||||
uvx hindsight-embed@latest -p openclaw memory list openclaw --limit 10
|
||||
uvx hindsight-embed@latest memory list openclaw --limit 10
|
||||
|
||||
# Open web UI (uses openclaw profile's daemon)
|
||||
uvx hindsight-embed@latest -p openclaw ui
|
||||
# Open web UI
|
||||
uvx hindsight-embed@latest ui
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
@@ -186,40 +176,27 @@ openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
### Daemon not starting
|
||||
|
||||
```bash
|
||||
# Check daemon status (note: -p openclaw uses the openclaw profile)
|
||||
uvx hindsight-embed@latest -p openclaw daemon status
|
||||
# Check daemon status
|
||||
uvx hindsight-embed@latest daemon status
|
||||
|
||||
# View logs for errors
|
||||
tail -f ~/.hindsight/profiles/openclaw.log
|
||||
tail -f ~/.hindsight/daemon.log
|
||||
|
||||
# Check configuration
|
||||
cat ~/.hindsight/profiles/openclaw.env
|
||||
|
||||
# List all profiles
|
||||
uvx hindsight-embed@latest profile list
|
||||
cat ~/.hindsight/embed
|
||||
```
|
||||
|
||||
### No API key error
|
||||
|
||||
Make sure you've set one of the provider API keys (or use a provider that doesn't require one):
|
||||
Make sure you've set one of the provider API keys:
|
||||
|
||||
```bash
|
||||
# Option 1: OpenAI
|
||||
export OPENAI_API_KEY="sk-your-key"
|
||||
|
||||
# Option 2: Anthropic
|
||||
# or
|
||||
export ANTHROPIC_API_KEY="your-key"
|
||||
|
||||
# Option 3: Claude Code (no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
|
||||
# Option 4: OpenAI Codex (no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
|
||||
# Verify it's set
|
||||
echo $OPENAI_API_KEY
|
||||
# or
|
||||
echo $HINDSIGHT_API_LLM_PROVIDER
|
||||
```
|
||||
|
||||
### Verify it's working
|
||||
@@ -231,8 +208,6 @@ tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
|
||||
|
||||
# Should see on startup:
|
||||
# [Hindsight] ✓ Using provider: openai, model: gpt-4o-mini
|
||||
# or
|
||||
# [Hindsight] ✓ Using provider: claude-code, model: claude-sonnet-4-20250514
|
||||
|
||||
# Should see after conversations:
|
||||
# [Hindsight] Retained X messages for session ...
|
||||
|
||||
+6
-107
@@ -25,7 +25,7 @@ uvx hindsight-embed --help
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Interactive setup (configures default profile)
|
||||
# Interactive setup (recommended)
|
||||
hindsight-embed configure
|
||||
|
||||
# Or set your LLM API key manually
|
||||
@@ -38,25 +38,14 @@ hindsight-embed memory retain default "User prefers dark mode"
|
||||
hindsight-embed memory recall default "What are user preferences?"
|
||||
```
|
||||
|
||||
All commands use the "default" profile unless you specify a different one with `--profile` or `HINDSIGHT_EMBED_PROFILE`.
|
||||
|
||||
## Commands
|
||||
|
||||
### configure
|
||||
|
||||
Configure the default profile or create/update named profiles:
|
||||
Interactive setup wizard:
|
||||
|
||||
```bash
|
||||
# Interactive setup for default profile
|
||||
hindsight-embed configure
|
||||
|
||||
# Create/update named profile with single command
|
||||
hindsight-embed configure --profile my-app \
|
||||
--env HINDSIGHT_EMBED_LLM_PROVIDER=openai \
|
||||
--env HINDSIGHT_EMBED_LLM_API_KEY=sk-xxx
|
||||
|
||||
# Create/update named profile interactively
|
||||
hindsight-embed configure --profile staging
|
||||
```
|
||||
|
||||
This will:
|
||||
@@ -105,27 +94,6 @@ List all memory banks:
|
||||
hindsight-embed bank list
|
||||
```
|
||||
|
||||
### profile
|
||||
|
||||
Manage configuration profiles:
|
||||
|
||||
```bash
|
||||
# List all profiles with status
|
||||
hindsight-embed profile list
|
||||
|
||||
# Show current active profile
|
||||
hindsight-embed profile show
|
||||
|
||||
# Set active profile (persists across commands)
|
||||
hindsight-embed profile set-active my-app
|
||||
|
||||
# Clear active profile (revert to default)
|
||||
hindsight-embed profile set-active --none
|
||||
|
||||
# Delete a profile
|
||||
hindsight-embed profile delete my-app
|
||||
```
|
||||
|
||||
### daemon
|
||||
|
||||
Manage the background daemon:
|
||||
@@ -149,7 +117,6 @@ Run `hindsight-embed configure` for a guided setup that saves to `~/.hindsight/e
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_EMBED_PROFILE` | Profile name to use (overrides active profile) | None (uses default profile) |
|
||||
| `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_MODEL` | LLM model | `gpt-4o-mini` |
|
||||
@@ -180,82 +147,14 @@ hindsight-embed daemon start
|
||||
|
||||
**Note:** All banks share a single database. Bank isolation happens within the database via the `bank_id` parameter passed to CLI commands.
|
||||
|
||||
### Configuration Profiles
|
||||
|
||||
Profiles let you maintain multiple independent configurations (e.g., different API endpoints, LLM providers, or projects). Each profile runs its own daemon on a unique port (8889-9888).
|
||||
|
||||
**The Default Profile:**
|
||||
|
||||
When you run `hindsight-embed configure` without specifying a profile, it configures the "default" profile. This uses the backward-compatible configuration at `~/.hindsight/embed` and runs on port 8888.
|
||||
|
||||
**Creating Named Profiles:**
|
||||
|
||||
```bash
|
||||
# Create a profile with single command
|
||||
hindsight-embed configure --profile my-app \
|
||||
--env HINDSIGHT_EMBED_LLM_PROVIDER=openai \
|
||||
--env HINDSIGHT_EMBED_LLM_API_KEY=sk-xxx \
|
||||
--env HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini
|
||||
|
||||
# Create a profile interactively
|
||||
hindsight-embed configure --profile staging
|
||||
```
|
||||
|
||||
**Using Profiles:**
|
||||
|
||||
```bash
|
||||
# Option 1: Environment variable (recommended for apps)
|
||||
HINDSIGHT_EMBED_PROFILE=my-app hindsight-embed memory retain default "text"
|
||||
|
||||
# Option 2: CLI flag
|
||||
hindsight-embed --profile my-app memory recall default "query"
|
||||
|
||||
# Option 3: Set as active (persists across commands)
|
||||
hindsight-embed profile set-active my-app
|
||||
hindsight-embed memory recall default "query" # Uses my-app profile
|
||||
|
||||
# Clear active profile (revert to default)
|
||||
hindsight-embed profile set-active --none
|
||||
```
|
||||
|
||||
**Profile Management:**
|
||||
|
||||
```bash
|
||||
# List all profiles with status
|
||||
hindsight-embed profile list
|
||||
|
||||
# Show active profile
|
||||
hindsight-embed profile show
|
||||
|
||||
# Delete a profile
|
||||
hindsight-embed profile delete my-app
|
||||
```
|
||||
|
||||
**Profile Resolution Priority:**
|
||||
1. `HINDSIGHT_EMBED_PROFILE` environment variable (highest)
|
||||
2. `--profile` CLI flag
|
||||
3. Active profile from `~/.hindsight/active_profile` file
|
||||
4. Default profile (lowest)
|
||||
|
||||
**Note:** If a profile is specified but doesn't exist, the command will fail with an error. Profiles must be explicitly created using `hindsight-embed configure --profile <name>`.
|
||||
|
||||
### Files
|
||||
|
||||
**Default Profile:**
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `~/.hindsight/embed` | Configuration file for default profile |
|
||||
| `~/.hindsight/daemon.log` | Daemon logs for default profile |
|
||||
| `~/.hindsight/daemon.lock` | Daemon lock file (PID) for default profile |
|
||||
|
||||
**Named Profiles:**
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `~/.hindsight/profiles/<name>.env` | Configuration file for profile |
|
||||
| `~/.hindsight/profiles/<name>.log` | Daemon logs for profile |
|
||||
| `~/.hindsight/profiles/<name>.lock` | Daemon lock file (PID) for profile |
|
||||
| `~/.hindsight/profiles/metadata.json` | Profile metadata (ports, timestamps) |
|
||||
| `~/.hindsight/active_profile` | Active profile name (when set with `profile set-active`) |
|
||||
| `~/.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
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,65 +6,21 @@ Handles daemon lifecycle (start if needed) and API requests via the Python clien
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx # Used only for health check
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from .profile_manager import ProfileManager, resolve_active_profile
|
||||
|
||||
console = Console(stderr=True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Suppress noisy httpx logs
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
# Default port for default profile
|
||||
DEFAULT_DAEMON_PORT = 8888
|
||||
DAEMON_PORT = DEFAULT_DAEMON_PORT # Backward compatibility
|
||||
DAEMON_PORT = 8888
|
||||
DAEMON_URL = f"http://127.0.0.1:{DAEMON_PORT}"
|
||||
DAEMON_STARTUP_TIMEOUT = 180 # seconds - needs to be long for first run (downloads dependencies)
|
||||
# Default idle timeout: 5 minutes - users can override with HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT env var
|
||||
DEFAULT_DAEMON_IDLE_TIMEOUT = 300
|
||||
|
||||
|
||||
def get_daemon_port(profile: str | None = None) -> int:
|
||||
"""Get daemon port for a profile.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
|
||||
Returns:
|
||||
Port number for daemon.
|
||||
"""
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
|
||||
pm = ProfileManager()
|
||||
paths = pm.resolve_profile_paths(profile)
|
||||
return paths.port
|
||||
|
||||
|
||||
def get_daemon_url(profile: str | None = None) -> str:
|
||||
"""Get daemon URL for a profile.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
|
||||
Returns:
|
||||
URL for daemon.
|
||||
"""
|
||||
port = get_daemon_port(profile)
|
||||
return f"http://127.0.0.1:{port}"
|
||||
|
||||
|
||||
# CLI paths - check multiple locations
|
||||
CLI_INSTALL_DIRS = [
|
||||
Path.home() / ".local" / "bin", # Standard location from get-cli installer
|
||||
@@ -90,47 +46,25 @@ def _find_hindsight_api_command() -> list[str]:
|
||||
return ["uvx", f"hindsight-api@{api_version}"]
|
||||
|
||||
|
||||
def _is_daemon_running(profile: str | None = None) -> bool:
|
||||
"""Check if daemon is running and responsive.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
|
||||
Returns:
|
||||
True if daemon is running and responsive.
|
||||
"""
|
||||
daemon_url = get_daemon_url(profile)
|
||||
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")
|
||||
response = client.get(f"{DAEMON_URL}/health")
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _start_daemon(config: dict, profile: str | None = None) -> bool:
|
||||
def _start_daemon(config: dict) -> bool:
|
||||
"""
|
||||
Start the daemon in background.
|
||||
|
||||
Args:
|
||||
config: Configuration dict with LLM settings.
|
||||
profile: Profile name (None = resolve from priority).
|
||||
|
||||
Returns:
|
||||
True if daemon started successfully.
|
||||
Returns True if daemon started successfully.
|
||||
"""
|
||||
import sys
|
||||
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
|
||||
# Get profile-specific paths
|
||||
pm = ProfileManager()
|
||||
paths = pm.resolve_profile_paths(profile)
|
||||
|
||||
profile_label = f"profile '{profile}'" if profile else "default profile"
|
||||
daemon_log = paths.log
|
||||
port = paths.port
|
||||
logger.info("Starting daemon...")
|
||||
|
||||
# Build environment with LLM config
|
||||
env = os.environ.copy()
|
||||
@@ -141,21 +75,14 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
|
||||
if config.get("llm_model"):
|
||||
env["HINDSIGHT_API_LLM_MODEL"] = config["llm_model"]
|
||||
|
||||
# Use profile-specific pg0 database for isolation
|
||||
# Use single shared pg0 database for all banks (banks are isolated within the database)
|
||||
# Allow override via HINDSIGHT_EMBED_API_DATABASE_URL for external PostgreSQL
|
||||
# (e.g. when running as root where embedded pg0 cannot use initdb)
|
||||
if "HINDSIGHT_EMBED_API_DATABASE_URL" not in env:
|
||||
# Sanitize profile name for use in database name (allow only alphanumeric, dash, underscore)
|
||||
safe_profile = re.sub(r"[^a-zA-Z0-9_-]", "-", profile or "default")
|
||||
env["HINDSIGHT_API_DATABASE_URL"] = f"pg0://hindsight-embed-{safe_profile}"
|
||||
env["HINDSIGHT_API_DATABASE_URL"] = "pg0://hindsight-embed"
|
||||
else:
|
||||
# Pass through the embed-specific env var to the daemon as the standard API env var
|
||||
env["HINDSIGHT_API_DATABASE_URL"] = env["HINDSIGHT_EMBED_API_DATABASE_URL"]
|
||||
|
||||
# Store database URL for display later
|
||||
database_url = env["HINDSIGHT_API_DATABASE_URL"]
|
||||
is_pg0 = database_url.startswith("pg0://")
|
||||
|
||||
env["HINDSIGHT_API_LOG_LEVEL"] = "info"
|
||||
|
||||
# On macOS, force CPU for embeddings/reranker to avoid MPS/Metal/XPC issues in daemon mode
|
||||
@@ -171,25 +98,23 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
|
||||
# Get idle timeout from environment or use default
|
||||
idle_timeout = int(os.getenv("HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT", str(DEFAULT_DAEMON_IDLE_TIMEOUT)))
|
||||
|
||||
# Use profile-specific log file
|
||||
daemon_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
cmd = _find_hindsight_api_command() + ["--daemon", "--idle-timeout", str(idle_timeout)]
|
||||
|
||||
# Tell hindsight-api daemon where to write its logs
|
||||
env["HINDSIGHT_API_DAEMON_LOG"] = str(daemon_log)
|
||||
# Create log directory
|
||||
log_dir = Path.home() / ".hindsight"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
daemon_log = log_dir / "daemon.log"
|
||||
|
||||
# Pass profile-specific port (no lockfile - we use port-based discovery)
|
||||
cmd = _find_hindsight_api_command() + [
|
||||
"--daemon",
|
||||
"--idle-timeout",
|
||||
str(idle_timeout),
|
||||
"--port",
|
||||
str(paths.port),
|
||||
]
|
||||
print(f"Starting daemon with command: {' '.join(cmd)}", file=sys.stderr)
|
||||
print(f" Log file: {daemon_log}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
# Start daemon directly (hindsight-api handles its own log redirection via HINDSIGHT_API_DAEMON_LOG)
|
||||
# Start daemon with shell redirection (works across forks)
|
||||
# The >> appends to log file, 2>&1 redirects stderr to stdout
|
||||
shell_cmd = f"{' '.join(cmd)} >> {shlex.quote(str(daemon_log))} 2>&1"
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
shell_cmd,
|
||||
shell=True,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
)
|
||||
@@ -200,253 +125,70 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
|
||||
# to detect failures - we must use the health check.
|
||||
start_time = time.time()
|
||||
last_check_time = start_time
|
||||
last_log_position = 0 # Track position in log file for tailing
|
||||
log_lines = [f"Starting daemon for {profile_label}...", ""] # Accumulate log lines for display
|
||||
while time.time() - start_time < DAEMON_STARTUP_TIMEOUT:
|
||||
if _is_daemon_running():
|
||||
# Health check passed - but daemon might crash during initialization
|
||||
# Wait a moment and verify it's still healthy (stability check)
|
||||
print(" Daemon responding, verifying stability...", file=sys.stderr)
|
||||
time.sleep(2)
|
||||
if _is_daemon_running():
|
||||
logger.info("Daemon started successfully")
|
||||
return True
|
||||
else:
|
||||
# Daemon crashed after initial health check
|
||||
print(" Daemon crashed during initialization", file=sys.stderr)
|
||||
break
|
||||
|
||||
# Build title with profile and port info
|
||||
if profile:
|
||||
title = f"[bold cyan]Starting Daemon[/bold cyan] [dim]({profile} @ :{port})[/dim]"
|
||||
else:
|
||||
title = f"[bold cyan]Starting Daemon[/bold cyan] [dim](:{port})[/dim]"
|
||||
# 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()
|
||||
|
||||
# Use Rich Live display for beautiful real-time updates
|
||||
with Live(console=console, auto_refresh=False) as live:
|
||||
# Show initial panel
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
panel = Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
)
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
|
||||
while time.time() - start_time < DAEMON_STARTUP_TIMEOUT:
|
||||
# Tail daemon logs if available
|
||||
if daemon_log.exists():
|
||||
try:
|
||||
with open(daemon_log, "r") as f:
|
||||
f.seek(last_log_position)
|
||||
new_lines = f.readlines()
|
||||
last_log_position = f.tell()
|
||||
|
||||
# Add new log lines (keep last 4 for display)
|
||||
for line in new_lines:
|
||||
line = line.rstrip()
|
||||
if line:
|
||||
log_lines.append(line)
|
||||
# Keep only last 4 lines
|
||||
log_lines = log_lines[-4:]
|
||||
except Exception:
|
||||
pass # Silently ignore log read errors
|
||||
|
||||
if _is_daemon_running(profile):
|
||||
# Health check passed - but daemon might crash during initialization
|
||||
# Add status message to logs
|
||||
log_lines.append("")
|
||||
log_lines.append("✓ Daemon responding, verifying stability...")
|
||||
|
||||
# Update display with success status
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
panel = Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
)
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
|
||||
time.sleep(2)
|
||||
if _is_daemon_running(profile):
|
||||
log_lines.append("✓ Daemon started successfully!")
|
||||
log_lines.append("")
|
||||
log_lines.append(f"Logs: {daemon_log}")
|
||||
|
||||
# Show pg0 location if using pg0
|
||||
if is_pg0:
|
||||
# pg0 stores data in ~/.pg0/instances/<database_name>
|
||||
pg0_name = database_url.replace("pg0://", "")
|
||||
pg0_path = Path.home() / ".pg0" / "instances" / pg0_name
|
||||
log_lines.append(f"Database: {pg0_path}")
|
||||
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
|
||||
# Build success title with profile and port
|
||||
if profile:
|
||||
success_title = (
|
||||
f"[bold green]✓ Daemon Started[/bold green] [dim]({profile} @ :{port})[/dim]"
|
||||
)
|
||||
else:
|
||||
success_title = f"[bold green]✓ Daemon Started[/bold green] [dim](:{port})[/dim]"
|
||||
|
||||
panel = Panel(
|
||||
content,
|
||||
title=success_title,
|
||||
border_style="green",
|
||||
padding=(1, 2),
|
||||
)
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
console.print() # Add newline after panel
|
||||
return True
|
||||
else:
|
||||
# Daemon crashed after initial health check
|
||||
log_lines.append("")
|
||||
log_lines.append("✗ Daemon crashed during initialization")
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
|
||||
# Build failure title with profile and port
|
||||
if profile:
|
||||
fail_title = f"[bold red]✗ Daemon Failed[/bold red] [dim]({profile} @ :{port})[/dim]"
|
||||
else:
|
||||
fail_title = f"[bold red]✗ Daemon Failed[/bold red] [dim](:{port})[/dim]"
|
||||
|
||||
panel = Panel(
|
||||
content,
|
||||
title=fail_title,
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
console.print()
|
||||
break
|
||||
|
||||
# Periodically log progress
|
||||
if time.time() - last_check_time > 3:
|
||||
elapsed = int(time.time() - start_time)
|
||||
# Update last status line or add new one
|
||||
status_msg = f"⏳ Waiting for daemon... ({elapsed}s elapsed)"
|
||||
if log_lines and log_lines[-1].startswith("⏳"):
|
||||
log_lines[-1] = status_msg
|
||||
else:
|
||||
log_lines.append(status_msg)
|
||||
last_check_time = time.time()
|
||||
|
||||
# Update the live display
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
panel = Panel(
|
||||
content,
|
||||
title=title,
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
)
|
||||
live.update(panel)
|
||||
live.refresh()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Timeout - show failure
|
||||
log_lines.append("")
|
||||
log_lines.append("✗ Daemon failed to start (timeout)")
|
||||
log_lines.append("")
|
||||
log_lines.append(f"See full log: {daemon_log}")
|
||||
|
||||
content = Text("\n".join(log_lines), style="dim")
|
||||
|
||||
# Build timeout title with profile and port
|
||||
if profile:
|
||||
timeout_title = f"[bold red]✗ Daemon Failed (Timeout)[/bold red] [dim]({profile} @ :{port})[/dim]"
|
||||
else:
|
||||
timeout_title = f"[bold red]✗ Daemon Failed (Timeout)[/bold red] [dim](:{port})[/dim]"
|
||||
|
||||
panel = Panel(
|
||||
content,
|
||||
title=timeout_title,
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel)
|
||||
console.print()
|
||||
time.sleep(0.5)
|
||||
|
||||
logger.error("Daemon failed to start")
|
||||
# Show logs on failure
|
||||
if daemon_log.exists():
|
||||
log_content = daemon_log.read_text()
|
||||
if log_content:
|
||||
# Show last 3000 chars of log
|
||||
print(f"\n Daemon log ({daemon_log}):", file=sys.stderr)
|
||||
print(f"{log_content[-3000:]}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
except FileNotFoundError as e:
|
||||
error_msg = f"Command not found: {cmd[0]}\nFull command: {' '.join(cmd)}\n\nInstall hindsight-api with: pip install hindsight-api"
|
||||
error_panel = Panel(
|
||||
Text(error_msg, style="red"),
|
||||
title="[bold red]✗ Command Not Found[/bold red]",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(error_panel)
|
||||
console.print()
|
||||
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:
|
||||
error_msg = f"Failed to start daemon: {e}\n\nCommand: {' '.join(cmd)}\nLog file: {daemon_log}"
|
||||
error_panel = Panel(
|
||||
Text(error_msg, style="red"),
|
||||
title="[bold red]✗ Startup Error[/bold red]",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(error_panel)
|
||||
console.print()
|
||||
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, profile: str | None = None) -> bool:
|
||||
def ensure_daemon_running(config: dict) -> bool:
|
||||
"""
|
||||
Ensure daemon is running, starting it if needed.
|
||||
|
||||
Args:
|
||||
config: Configuration dict with LLM settings.
|
||||
profile: Profile name (None = resolve from priority).
|
||||
|
||||
Returns:
|
||||
True if daemon is running.
|
||||
Returns True if daemon is running.
|
||||
"""
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
|
||||
if _is_daemon_running(profile):
|
||||
logger.debug(f"Daemon already running for profile '{profile or 'default'}'")
|
||||
if _is_daemon_running():
|
||||
logger.debug("Daemon already running")
|
||||
return True
|
||||
|
||||
return _start_daemon(config, profile)
|
||||
return _start_daemon(config)
|
||||
|
||||
|
||||
def stop_daemon(profile: str | None = None) -> bool:
|
||||
"""Stop the running daemon and wait for it to fully stop.
|
||||
|
||||
Args:
|
||||
profile: Profile name (None = resolve from priority).
|
||||
|
||||
Returns:
|
||||
True if daemon stopped successfully.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
|
||||
# Check if daemon is actually running via health check
|
||||
if not _is_daemon_running(profile):
|
||||
logger.debug(f"Daemon not running for profile '{profile or 'default'}'")
|
||||
return True
|
||||
|
||||
# Get profile-specific port
|
||||
pm = ProfileManager()
|
||||
paths = pm.resolve_profile_paths(profile)
|
||||
port = paths.port
|
||||
|
||||
# Find PID by port using lsof (works on macOS/Linux, handles stale lockfiles)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["lsof", "-ti", f":{port}", "-sTCP:LISTEN"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
pid = int(result.stdout.strip().split()[0])
|
||||
logger.debug(f"Found daemon PID {pid} on port {port}")
|
||||
|
||||
# Send SIGTERM
|
||||
os.kill(pid, 15)
|
||||
|
||||
def stop_daemon() -> bool:
|
||||
"""Stop the running daemon and wait for it to fully stop."""
|
||||
# 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)
|
||||
@@ -454,18 +196,16 @@ def stop_daemon(profile: str | None = None) -> bool:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
break # Process exited
|
||||
else:
|
||||
logger.warning(f"Could not find PID for port {port}")
|
||||
except (subprocess.TimeoutExpired, ValueError, OSError, FileNotFoundError) as e:
|
||||
logger.warning(f"Could not find/kill daemon by port: {e}")
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
# Wait for health check to fail (daemon fully stopped)
|
||||
for _ in range(30): # Wait up to 3 seconds
|
||||
if not _is_daemon_running(profile):
|
||||
if not _is_daemon_running():
|
||||
return True
|
||||
time.sleep(0.1)
|
||||
|
||||
return not _is_daemon_running(profile)
|
||||
return not _is_daemon_running()
|
||||
|
||||
|
||||
def find_cli_binary() -> Path | None:
|
||||
@@ -554,7 +294,7 @@ def ensure_cli_installed() -> bool:
|
||||
return install_cli()
|
||||
|
||||
|
||||
def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
|
||||
def run_cli(args: list[str], config: dict) -> int:
|
||||
"""
|
||||
Run the hindsight CLI with the given arguments.
|
||||
|
||||
@@ -563,7 +303,6 @@ def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
|
||||
Args:
|
||||
args: CLI arguments (e.g., ["memory", "retain", "bank", "content"])
|
||||
config: Configuration dict with llm settings
|
||||
profile: Profile name (None = resolve from priority)
|
||||
|
||||
Returns:
|
||||
Exit code from CLI
|
||||
@@ -571,9 +310,6 @@ def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if profile is None:
|
||||
profile = resolve_active_profile()
|
||||
|
||||
# Ensure CLI is installed
|
||||
if not ensure_cli_installed():
|
||||
return 1
|
||||
@@ -591,10 +327,10 @@ def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
|
||||
|
||||
if not api_url:
|
||||
# No external API specified - ensure our daemon is running
|
||||
if not ensure_daemon_running(config, profile):
|
||||
if not ensure_daemon_running(config):
|
||||
print("Error: Failed to start daemon", file=sys.stderr)
|
||||
return 1
|
||||
api_url = get_daemon_url(profile)
|
||||
api_url = DAEMON_URL
|
||||
else:
|
||||
# Using external API - skip daemon startup
|
||||
logger.debug(f"Using external API at {api_url}")
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
"""Profile management for hindsight-embed.
|
||||
|
||||
Handles creation, deletion, and management of configuration profiles.
|
||||
Each profile has its own config, daemon lock, log file, and port.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
# Configuration paths
|
||||
CONFIG_DIR = Path.home() / ".hindsight"
|
||||
PROFILES_DIR = CONFIG_DIR / "profiles"
|
||||
METADATA_FILE = PROFILES_DIR / "metadata.json"
|
||||
ACTIVE_PROFILE_FILE = CONFIG_DIR / "active_profile"
|
||||
|
||||
# Port allocation
|
||||
DEFAULT_PORT = 8888
|
||||
PROFILE_PORT_BASE = 8889
|
||||
PROFILE_PORT_RANGE = 1000 # 8889-9888
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfilePaths:
|
||||
"""Paths and port for a profile."""
|
||||
|
||||
config: Path
|
||||
lock: Path
|
||||
log: Path
|
||||
port: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileInfo:
|
||||
"""Profile information including metadata."""
|
||||
|
||||
name: str
|
||||
port: int
|
||||
created_at: str
|
||||
last_used: Optional[str] = None
|
||||
is_active: bool = False
|
||||
daemon_running: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileMetadata:
|
||||
"""Metadata for all profiles."""
|
||||
|
||||
version: int = 1
|
||||
profiles: dict[str, dict] = field(default_factory=dict)
|
||||
|
||||
|
||||
class ProfileManager:
|
||||
"""Manages configuration profiles for hindsight-embed."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the profile manager."""
|
||||
self._ensure_directories()
|
||||
|
||||
def _ensure_directories(self):
|
||||
"""Ensure profile directories exist."""
|
||||
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def list_profiles(self) -> list[ProfileInfo]:
|
||||
"""List all profiles with their status.
|
||||
|
||||
Returns:
|
||||
List of ProfileInfo objects with daemon status.
|
||||
"""
|
||||
metadata = self._load_metadata()
|
||||
active_profile = self.get_active_profile()
|
||||
profiles = []
|
||||
|
||||
# Add default profile if config exists
|
||||
default_config = CONFIG_DIR / "embed"
|
||||
if default_config.exists():
|
||||
profiles.append(
|
||||
ProfileInfo(
|
||||
name="", # Empty name = default
|
||||
port=DEFAULT_PORT,
|
||||
created_at="", # Don't track for default
|
||||
last_used=None,
|
||||
is_active=active_profile == "",
|
||||
daemon_running=self._check_daemon_running(DEFAULT_PORT),
|
||||
)
|
||||
)
|
||||
|
||||
# Add named profiles
|
||||
for name, info in metadata.profiles.items():
|
||||
profiles.append(
|
||||
ProfileInfo(
|
||||
name=name,
|
||||
port=info["port"],
|
||||
created_at=info.get("created_at", ""),
|
||||
last_used=info.get("last_used"),
|
||||
is_active=active_profile == name,
|
||||
daemon_running=self._check_daemon_running(info["port"]),
|
||||
)
|
||||
)
|
||||
|
||||
return sorted(profiles, key=lambda p: (p.name != "", p.name))
|
||||
|
||||
def profile_exists(self, name: str) -> bool:
|
||||
"""Check if a profile exists.
|
||||
|
||||
Args:
|
||||
name: Profile name (empty string for default).
|
||||
|
||||
Returns:
|
||||
True if profile exists.
|
||||
"""
|
||||
if not name:
|
||||
# Default profile exists if config file exists
|
||||
return (CONFIG_DIR / "embed").exists()
|
||||
|
||||
# Named profile exists if config file exists
|
||||
config_path = PROFILES_DIR / f"{name}.env"
|
||||
return config_path.exists()
|
||||
|
||||
def get_profile(self, name: str) -> Optional[ProfileInfo]:
|
||||
"""Get profile information.
|
||||
|
||||
Args:
|
||||
name: Profile name (empty string for default).
|
||||
|
||||
Returns:
|
||||
ProfileInfo if profile exists, None otherwise.
|
||||
"""
|
||||
profiles = self.list_profiles()
|
||||
for profile in profiles:
|
||||
if profile.name == name:
|
||||
return profile
|
||||
return None
|
||||
|
||||
def create_profile(self, name: str, port_or_config: int | dict[str, str], config: dict[str, str] | None = None):
|
||||
"""Create or update a profile.
|
||||
|
||||
Args:
|
||||
name: Profile name.
|
||||
port_or_config: Port number (int) or configuration dict. For backward compatibility,
|
||||
if this is a dict, it's treated as config and port is auto-allocated.
|
||||
config: Configuration dict (KEY=VALUE pairs). Only used if port_or_config is an int.
|
||||
|
||||
Raises:
|
||||
ValueError: If profile name is invalid or port is invalid.
|
||||
"""
|
||||
# Handle backward compatibility - allow (name, config) or (name, port, config)
|
||||
if isinstance(port_or_config, dict):
|
||||
# Called with (name, config) - auto-allocate port
|
||||
port = None
|
||||
config = port_or_config
|
||||
else:
|
||||
# Called with (name, port, config)
|
||||
port = port_or_config
|
||||
if config is None:
|
||||
raise ValueError("Config must be provided when port is specified")
|
||||
|
||||
if not name:
|
||||
raise ValueError("Profile name cannot be empty")
|
||||
|
||||
if not name.replace("-", "").replace("_", "").isalnum():
|
||||
raise ValueError(f"Invalid profile name '{name}'. Use alphanumeric chars, hyphens, and underscores.")
|
||||
|
||||
if port is not None and (port < 1024 or port > 65535):
|
||||
raise ValueError(f"Invalid port {port}. Must be between 1024-65535.")
|
||||
|
||||
# Ensure profile directory exists
|
||||
self._ensure_directories()
|
||||
|
||||
# Load metadata to check if profile already exists
|
||||
metadata = self._load_metadata()
|
||||
|
||||
# Determine port: use provided port, preserve existing, or allocate new
|
||||
if port is None:
|
||||
if name in metadata.profiles and "port" in metadata.profiles[name]:
|
||||
port = metadata.profiles[name]["port"]
|
||||
else:
|
||||
port = self._allocate_port(name)
|
||||
|
||||
# Write config file
|
||||
config_path = PROFILES_DIR / f"{name}.env"
|
||||
config_lines = [f"{key}={value}" for key, value in config.items()]
|
||||
config_path.write_text("\n".join(config_lines) + "\n")
|
||||
|
||||
# Update metadata (reuse metadata loaded earlier to avoid race conditions)
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
if name in metadata.profiles:
|
||||
# Update existing profile
|
||||
metadata.profiles[name]["last_used"] = now_iso
|
||||
metadata.profiles[name]["port"] = port
|
||||
else:
|
||||
# Create new profile
|
||||
metadata.profiles[name] = {
|
||||
"port": port,
|
||||
"created_at": now_iso,
|
||||
"last_used": now_iso,
|
||||
}
|
||||
|
||||
self._save_metadata(metadata)
|
||||
|
||||
def delete_profile(self, name: str):
|
||||
"""Delete a profile.
|
||||
|
||||
Args:
|
||||
name: Profile name.
|
||||
|
||||
Raises:
|
||||
ValueError: If profile name is invalid or doesn't exist.
|
||||
"""
|
||||
if not name:
|
||||
raise ValueError("Cannot delete default profile")
|
||||
|
||||
if not self.profile_exists(name):
|
||||
raise ValueError(f"Profile '{name}' does not exist")
|
||||
|
||||
# Remove config file
|
||||
config_path = PROFILES_DIR / f"{name}.env"
|
||||
if config_path.exists():
|
||||
config_path.unlink()
|
||||
|
||||
# Remove lock file
|
||||
lock_path = PROFILES_DIR / f"{name}.lock"
|
||||
if lock_path.exists():
|
||||
lock_path.unlink()
|
||||
|
||||
# Remove log file
|
||||
log_path = PROFILES_DIR / f"{name}.log"
|
||||
if log_path.exists():
|
||||
log_path.unlink()
|
||||
|
||||
# Update metadata
|
||||
metadata = self._load_metadata()
|
||||
if name in metadata.profiles:
|
||||
del metadata.profiles[name]
|
||||
self._save_metadata(metadata)
|
||||
|
||||
# Clear active profile if it was deleted
|
||||
if self.get_active_profile() == name:
|
||||
self.set_active_profile(None)
|
||||
|
||||
def set_active_profile(self, name: Optional[str]):
|
||||
"""Set the active profile.
|
||||
|
||||
Args:
|
||||
name: Profile name to activate, or None to clear.
|
||||
|
||||
Raises:
|
||||
ValueError: If profile doesn't exist.
|
||||
"""
|
||||
if name and not self.profile_exists(name):
|
||||
raise ValueError(f"Profile '{name}' does not exist")
|
||||
|
||||
if name:
|
||||
ACTIVE_PROFILE_FILE.write_text(name)
|
||||
else:
|
||||
# Clear active profile
|
||||
if ACTIVE_PROFILE_FILE.exists():
|
||||
ACTIVE_PROFILE_FILE.unlink()
|
||||
|
||||
def get_active_profile(self) -> str:
|
||||
"""Get the currently active profile name.
|
||||
|
||||
Returns:
|
||||
Profile name, or empty string if no active profile.
|
||||
"""
|
||||
if ACTIVE_PROFILE_FILE.exists():
|
||||
return ACTIVE_PROFILE_FILE.read_text().strip()
|
||||
return ""
|
||||
|
||||
def resolve_profile_paths(self, name: str) -> ProfilePaths:
|
||||
"""Resolve paths for a profile.
|
||||
|
||||
Args:
|
||||
name: Profile name (empty string for default).
|
||||
|
||||
Returns:
|
||||
ProfilePaths with config, lock, log, and port.
|
||||
"""
|
||||
if not name:
|
||||
# Default profile
|
||||
return ProfilePaths(
|
||||
config=CONFIG_DIR / "embed",
|
||||
lock=CONFIG_DIR / "daemon.lock",
|
||||
log=CONFIG_DIR / "daemon.log",
|
||||
port=DEFAULT_PORT,
|
||||
)
|
||||
|
||||
# Named profile
|
||||
metadata = self._load_metadata()
|
||||
port = metadata.profiles.get(name, {}).get("port", self._allocate_port(name))
|
||||
|
||||
return ProfilePaths(
|
||||
config=PROFILES_DIR / f"{name}.env",
|
||||
lock=PROFILES_DIR / f"{name}.lock",
|
||||
log=PROFILES_DIR / f"{name}.log",
|
||||
port=port,
|
||||
)
|
||||
|
||||
def _allocate_port(self, name: str) -> int:
|
||||
"""Allocate a port for a profile using hash-based strategy.
|
||||
|
||||
Args:
|
||||
name: Profile name.
|
||||
|
||||
Returns:
|
||||
Port number (8889-9888).
|
||||
"""
|
||||
# Hash profile name to get consistent port
|
||||
hash_val = int(hashlib.sha256(name.encode()).hexdigest(), 16)
|
||||
port = PROFILE_PORT_BASE + (hash_val % PROFILE_PORT_RANGE)
|
||||
|
||||
# Check if port is already allocated in metadata
|
||||
metadata = self._load_metadata()
|
||||
allocated_ports = {info["port"] for info in metadata.profiles.values() if info.get("port")}
|
||||
|
||||
# If collision, find next available port
|
||||
attempt = 0
|
||||
while port in allocated_ports and attempt < PROFILE_PORT_RANGE:
|
||||
port = PROFILE_PORT_BASE + ((hash_val + attempt) % PROFILE_PORT_RANGE)
|
||||
attempt += 1
|
||||
|
||||
if attempt >= PROFILE_PORT_RANGE:
|
||||
# Fallback: find first available port
|
||||
for p in range(PROFILE_PORT_BASE, PROFILE_PORT_BASE + PROFILE_PORT_RANGE):
|
||||
if p not in allocated_ports:
|
||||
return p
|
||||
raise RuntimeError("No available ports for profile")
|
||||
|
||||
return port
|
||||
|
||||
def _check_daemon_running(self, port: int) -> bool:
|
||||
"""Check if daemon is running on a port.
|
||||
|
||||
Args:
|
||||
port: Port number to check.
|
||||
|
||||
Returns:
|
||||
True if daemon is responding.
|
||||
"""
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(f"http://127.0.0.1:{port}/health", timeout=1)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _load_metadata(self) -> ProfileMetadata:
|
||||
"""Load profile metadata from disk.
|
||||
|
||||
Returns:
|
||||
ProfileMetadata object.
|
||||
"""
|
||||
if not METADATA_FILE.exists():
|
||||
return ProfileMetadata()
|
||||
|
||||
try:
|
||||
with open(METADATA_FILE) as f:
|
||||
data = json.load(f)
|
||||
return ProfileMetadata(version=data.get("version", 1), profiles=data.get("profiles", {}))
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
print(
|
||||
f"Warning: Failed to load metadata: {e}. Using empty metadata.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# Backup corrupted metadata
|
||||
backup_path = METADATA_FILE.with_suffix(".json.bak")
|
||||
if METADATA_FILE.exists():
|
||||
METADATA_FILE.rename(backup_path)
|
||||
return ProfileMetadata()
|
||||
|
||||
def _save_metadata(self, metadata: ProfileMetadata):
|
||||
"""Save profile metadata to disk with file locking.
|
||||
|
||||
Args:
|
||||
metadata: ProfileMetadata to save.
|
||||
"""
|
||||
self._ensure_directories()
|
||||
|
||||
# Use atomic write with temp file
|
||||
temp_file = METADATA_FILE.with_suffix(".json.tmp")
|
||||
|
||||
with open(temp_file, "w") as f:
|
||||
# Acquire exclusive lock
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||||
try:
|
||||
json.dump(
|
||||
{"version": metadata.version, "profiles": metadata.profiles},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
finally:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
# Atomic rename
|
||||
temp_file.rename(METADATA_FILE)
|
||||
|
||||
|
||||
def resolve_active_profile() -> str:
|
||||
"""Resolve which profile to use based on priority.
|
||||
|
||||
Priority (highest to lowest):
|
||||
1. HINDSIGHT_EMBED_PROFILE environment variable
|
||||
2. CLI --profile flag (from global context)
|
||||
3. Active profile from file
|
||||
4. Default (empty string)
|
||||
|
||||
Returns:
|
||||
Profile name to use (empty string for default).
|
||||
"""
|
||||
# 1. Environment variable
|
||||
if env_profile := os.getenv("HINDSIGHT_EMBED_PROFILE"):
|
||||
return env_profile
|
||||
|
||||
# 2. CLI flag (set by caller before invoking commands)
|
||||
from . import cli
|
||||
|
||||
if cli_profile := cli.get_cli_profile_override():
|
||||
return cli_profile
|
||||
|
||||
# 3. Active profile file
|
||||
pm = ProfileManager()
|
||||
if active_profile := pm.get_active_profile():
|
||||
return active_profile
|
||||
|
||||
# 4. Default
|
||||
return ""
|
||||
|
||||
|
||||
def validate_profile_exists(profile: str):
|
||||
"""Validate that a profile exists, exit if not.
|
||||
|
||||
Args:
|
||||
profile: Profile name to validate.
|
||||
|
||||
Exits:
|
||||
If profile doesn't exist, prints error and exits.
|
||||
"""
|
||||
if not profile:
|
||||
# Default profile - always valid
|
||||
return
|
||||
|
||||
pm = ProfileManager()
|
||||
if not pm.profile_exists(profile):
|
||||
print(
|
||||
f"Error: Profile '{profile}' not found.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f"Create it with: hindsight-embed configure --profile {profile}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -10,7 +10,6 @@ readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"httpx>=0.27.0",
|
||||
"rich>=13.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
+53
-8
@@ -2,7 +2,6 @@
|
||||
#
|
||||
# Smoke test for hindsight-embed CLI with daemon mode
|
||||
# Tests retain and recall operations via the background daemon
|
||||
# Verifies daemon lifecycle, memory retention, and recall functionality
|
||||
#
|
||||
|
||||
set -e
|
||||
@@ -17,8 +16,8 @@ if [ -f ~/.hindsight/config.env ]; then
|
||||
source ~/.hindsight/config.env
|
||||
fi
|
||||
|
||||
if [ -z "$HINDSIGHT_API_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
|
||||
echo "Error: HINDSIGHT_API_LLM_API_KEY or OPENAI_API_KEY is required"
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -28,14 +27,60 @@ echo "Using bank ID: $BANK_ID"
|
||||
echo "Script dir: $SCRIPT_DIR"
|
||||
echo "API dir: $API_DIR"
|
||||
|
||||
# Debug: Check if hindsight CLI is available
|
||||
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_API_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-not set}"
|
||||
echo " HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-not set}"
|
||||
echo " HINDSIGHT_API_LLM_API_KEY: ${HINDSIGHT_API_LLM_API_KEY:+set (hidden)}"
|
||||
echo " Python: $(python3 --version 2>&1)"
|
||||
echo " uv: $(uv --version 2>&1)"
|
||||
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 ""
|
||||
|
||||
@@ -96,7 +96,7 @@ class TestRunCli:
|
||||
call_args = mock_subprocess_run.call_args
|
||||
|
||||
# Verify environment contains the local daemon URL
|
||||
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == daemon_client.get_daemon_url()
|
||||
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == daemon_client.DAEMON_URL
|
||||
|
||||
# Verify exit code
|
||||
assert exit_code == 0
|
||||
@@ -246,8 +246,8 @@ class TestStartDaemon:
|
||||
# Start daemon (will fail health check, but we just want to verify env)
|
||||
daemon_client._start_daemon(config)
|
||||
|
||||
# Verify the default database URL was set (profile-specific)
|
||||
assert captured_env.get("HINDSIGHT_API_DATABASE_URL") == "pg0://hindsight-embed-default"
|
||||
# Verify the default database URL was set
|
||||
assert captured_env.get("HINDSIGHT_API_DATABASE_URL") == "pg0://hindsight-embed"
|
||||
|
||||
|
||||
class TestIsDaemonRunning:
|
||||
|
||||
@@ -1,388 +0,0 @@
|
||||
"""Integration tests for profile functionality."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def strip_ansi(text):
|
||||
"""Remove ANSI escape sequences from text."""
|
||||
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
return ansi_escape.sub("", text)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_home(tmp_path, monkeypatch):
|
||||
"""Create a temporary home directory for integration tests."""
|
||||
temp_home = tmp_path / "home"
|
||||
temp_home.mkdir()
|
||||
monkeypatch.setenv("HOME", str(temp_home))
|
||||
return temp_home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hindsight_embed_cmd():
|
||||
"""Get the hindsight-embed command."""
|
||||
return ["uv", "run", "hindsight-embed"]
|
||||
|
||||
|
||||
class TestProfileIntegration:
|
||||
"""Integration tests for profile workflows."""
|
||||
|
||||
def test_create_and_list_profile(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test creating a profile and listing it."""
|
||||
# Create profile
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Profile 'test-app' configured successfully" in result.stdout
|
||||
|
||||
# Verify profile file was created
|
||||
profile_path = temp_home / ".hindsight" / "profiles" / "test-app.env"
|
||||
assert profile_path.exists()
|
||||
|
||||
config_content = profile_path.read_text()
|
||||
assert "HINDSIGHT_EMBED_LLM_PROVIDER=openai" in config_content
|
||||
assert "HINDSIGHT_EMBED_LLM_API_KEY=sk-test" in config_content
|
||||
|
||||
# List profiles
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "test-app" in result.stdout
|
||||
assert "Port:" in result.stdout
|
||||
|
||||
def test_profile_show_default(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test showing the default profile."""
|
||||
# Create default config
|
||||
config_dir = temp_home / ".hindsight"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
(config_dir / "embed").write_text("KEY=value")
|
||||
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "show"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
output = strip_ansi(result.stdout)
|
||||
assert "Active profile: default" in output
|
||||
assert "Source: Default" in output
|
||||
assert "Port: 8888" in output
|
||||
|
||||
def test_profile_show_with_env_var(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test profile resolution with HINDSIGHT_EMBED_PROFILE env var."""
|
||||
# Create profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
# Show profile with env var
|
||||
env = {**os.environ, "HOME": str(temp_home), "HINDSIGHT_EMBED_PROFILE": "test-app"}
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "show"], capture_output=True, text=True, env=env
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
output = strip_ansi(result.stdout)
|
||||
assert "Active profile: test-app" in output
|
||||
assert "Source: HINDSIGHT_EMBED_PROFILE environment variable" in output
|
||||
|
||||
def test_set_active_profile(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test setting and using active profile."""
|
||||
# Create profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
# Set as active
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "set-active", "test-app"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
output = strip_ansi(result.stdout)
|
||||
assert "Active profile set to 'test-app'" in output
|
||||
|
||||
# Verify active_profile file
|
||||
active_file = temp_home / ".hindsight" / "active_profile"
|
||||
assert active_file.exists()
|
||||
assert active_file.read_text() == "test-app"
|
||||
|
||||
# Show profile (should show as active)
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "show"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
output = strip_ansi(result.stdout)
|
||||
assert "Active profile: test-app" in output
|
||||
|
||||
def test_delete_profile(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test deleting a profile."""
|
||||
# Create profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Delete profile (with 'y' confirmation)
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "delete", "test-app"],
|
||||
input="y\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0 or "deleted" in result.stdout.lower()
|
||||
|
||||
# Verify profile file is gone
|
||||
profile_path = temp_home / ".hindsight" / "profiles" / "test-app.env"
|
||||
# Note: Test may still have file if daemon was running and user cancelled
|
||||
|
||||
def test_multiple_profiles_different_ports(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test that multiple profiles get different ports."""
|
||||
# Create first profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"app1",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Create second profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"app2",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Read metadata
|
||||
metadata_path = temp_home / ".hindsight" / "profiles" / "metadata.json"
|
||||
assert metadata_path.exists()
|
||||
|
||||
metadata = json.loads(metadata_path.read_text())
|
||||
app1_port = metadata["profiles"]["app1"]["port"]
|
||||
app2_port = metadata["profiles"]["app2"]["port"]
|
||||
|
||||
# Ports should be different
|
||||
assert app1_port != app2_port
|
||||
assert 8889 <= app1_port <= 9888
|
||||
assert 8889 <= app2_port <= 9888
|
||||
|
||||
def test_profile_validation_fails_for_nonexistent(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test that using non-existent profile fails."""
|
||||
# Try to use non-existent profile
|
||||
env = {**os.environ, "HOME": str(temp_home), "HINDSIGHT_EMBED_PROFILE": "nonexistent"}
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "show"], capture_output=True, text=True, env=env
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "Profile 'nonexistent' not found" in result.stderr
|
||||
|
||||
def test_backward_compatibility_default_profile(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test that default profile works without any profile commands."""
|
||||
# Create default config manually (simulating old behavior)
|
||||
config_dir = temp_home / ".hindsight"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
(config_dir / "embed").write_text(
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai\n"
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test\n"
|
||||
"HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini\n"
|
||||
)
|
||||
|
||||
# Show profile should work
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "show"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "HOME": str(temp_home)},
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
output = strip_ansi(result.stdout)
|
||||
assert "Active profile: default" in output
|
||||
assert "Port: 8888" in output
|
||||
|
||||
def test_configure_without_profile_flag(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test that configure without --profile still works (backward compatibility)."""
|
||||
# Configure without profile flag (should configure default)
|
||||
# Use non-interactive mode by providing env vars
|
||||
env = {
|
||||
**os.environ,
|
||||
"HOME": str(temp_home),
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER": "openai",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY": "sk-test",
|
||||
"HINDSIGHT_EMBED_LLM_MODEL": "gpt-4o-mini",
|
||||
}
|
||||
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["configure"], capture_output=True, text=True, env=env
|
||||
)
|
||||
|
||||
# Should succeed
|
||||
assert result.returncode == 0
|
||||
|
||||
# Verify default config was created
|
||||
config_path = temp_home / ".hindsight" / "embed"
|
||||
assert config_path.exists()
|
||||
|
||||
def test_clear_active_profile(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test clearing the active profile."""
|
||||
# Create and set active profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "set-active", "test-app"],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Clear active profile
|
||||
result = subprocess.run(
|
||||
hindsight_embed_cmd + ["profile", "set-active", "--none"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Active profile cleared" in result.stdout
|
||||
|
||||
# Verify active_profile file is gone
|
||||
active_file = temp_home / ".hindsight" / "active_profile"
|
||||
assert not active_file.exists()
|
||||
|
||||
def test_profile_port_persistence(self, temp_home, hindsight_embed_cmd):
|
||||
"""Test that profile port is persistent across recreations."""
|
||||
# Create profile
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Get port
|
||||
metadata_path = temp_home / ".hindsight" / "profiles" / "metadata.json"
|
||||
metadata1 = json.loads(metadata_path.read_text())
|
||||
port1 = metadata1["profiles"]["test-app"]["port"]
|
||||
|
||||
# Update profile (recreate with different config)
|
||||
subprocess.run(
|
||||
hindsight_embed_cmd
|
||||
+ [
|
||||
"configure",
|
||||
"--profile",
|
||||
"test-app",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER=groq",
|
||||
"--env",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY=gsk-test",
|
||||
],
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Get port again
|
||||
metadata2 = json.loads(metadata_path.read_text())
|
||||
port2 = metadata2["profiles"]["test-app"]["port"]
|
||||
|
||||
# Port should be the same
|
||||
assert port1 == port2
|
||||
@@ -1,405 +0,0 @@
|
||||
"""Tests for profile_manager module."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_embed.profile_manager import (
|
||||
CONFIG_DIR,
|
||||
PROFILES_DIR,
|
||||
ProfileInfo,
|
||||
ProfileManager,
|
||||
ProfilePaths,
|
||||
resolve_active_profile,
|
||||
validate_profile_exists,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_hindsight_dir(tmp_path, monkeypatch):
|
||||
"""Create a temporary hindsight directory for tests."""
|
||||
temp_config = tmp_path / ".hindsight"
|
||||
temp_config.mkdir()
|
||||
monkeypatch.setattr("hindsight_embed.profile_manager.CONFIG_DIR", temp_config)
|
||||
monkeypatch.setattr("hindsight_embed.profile_manager.PROFILES_DIR", temp_config / "profiles")
|
||||
monkeypatch.setattr(
|
||||
"hindsight_embed.profile_manager.METADATA_FILE", temp_config / "profiles" / "metadata.json"
|
||||
)
|
||||
monkeypatch.setattr("hindsight_embed.profile_manager.ACTIVE_PROFILE_FILE", temp_config / "active_profile")
|
||||
return temp_config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def profile_manager(temp_hindsight_dir):
|
||||
"""Create a ProfileManager with temp directory."""
|
||||
return ProfileManager()
|
||||
|
||||
|
||||
class TestProfileManager:
|
||||
"""Tests for ProfileManager class."""
|
||||
|
||||
def test_create_profile_success(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test creating a new profile."""
|
||||
config = {
|
||||
"HINDSIGHT_EMBED_LLM_PROVIDER": "openai",
|
||||
"HINDSIGHT_EMBED_LLM_API_KEY": "sk-test",
|
||||
"HINDSIGHT_EMBED_LLM_MODEL": "gpt-4o-mini",
|
||||
}
|
||||
|
||||
profile_manager.create_profile("test-profile", config)
|
||||
|
||||
# Verify config file was created
|
||||
config_path = temp_hindsight_dir / "profiles" / "test-profile.env"
|
||||
assert config_path.exists()
|
||||
|
||||
# Verify config contents
|
||||
config_content = config_path.read_text()
|
||||
assert "HINDSIGHT_EMBED_LLM_PROVIDER=openai" in config_content
|
||||
assert "HINDSIGHT_EMBED_LLM_API_KEY=sk-test" in config_content
|
||||
assert "HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini" in config_content
|
||||
|
||||
# Verify metadata was created
|
||||
metadata_path = temp_hindsight_dir / "profiles" / "metadata.json"
|
||||
assert metadata_path.exists()
|
||||
|
||||
metadata = json.loads(metadata_path.read_text())
|
||||
assert "test-profile" in metadata["profiles"]
|
||||
assert "port" in metadata["profiles"]["test-profile"]
|
||||
assert "created_at" in metadata["profiles"]["test-profile"]
|
||||
|
||||
def test_create_profile_invalid_name(self, profile_manager):
|
||||
"""Test creating profile with invalid name fails."""
|
||||
config = {"KEY": "value"}
|
||||
|
||||
# Empty name
|
||||
with pytest.raises(ValueError, match="Profile name cannot be empty"):
|
||||
profile_manager.create_profile("", config)
|
||||
|
||||
# Invalid characters
|
||||
with pytest.raises(ValueError, match="Invalid profile name"):
|
||||
profile_manager.create_profile("test profile", config)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid profile name"):
|
||||
profile_manager.create_profile("test@profile", config)
|
||||
|
||||
def test_profile_exists(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test checking if a profile exists."""
|
||||
# Default profile doesn't exist initially
|
||||
assert not profile_manager.profile_exists("")
|
||||
|
||||
# Create default config
|
||||
(temp_hindsight_dir / "embed").write_text("KEY=value")
|
||||
assert profile_manager.profile_exists("")
|
||||
|
||||
# Named profile doesn't exist
|
||||
assert not profile_manager.profile_exists("test")
|
||||
|
||||
# Create named profile
|
||||
profile_manager.create_profile("test", {"KEY": "value"})
|
||||
assert profile_manager.profile_exists("test")
|
||||
|
||||
def test_delete_profile(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test deleting a profile."""
|
||||
# Create profile
|
||||
profile_manager.create_profile("test-profile", {"KEY": "value"})
|
||||
assert profile_manager.profile_exists("test-profile")
|
||||
|
||||
# Delete profile
|
||||
profile_manager.delete_profile("test-profile")
|
||||
assert not profile_manager.profile_exists("test-profile")
|
||||
|
||||
# Verify all files are removed
|
||||
profiles_dir = temp_hindsight_dir / "profiles"
|
||||
assert not (profiles_dir / "test-profile.env").exists()
|
||||
assert not (profiles_dir / "test-profile.lock").exists()
|
||||
assert not (profiles_dir / "test-profile.log").exists()
|
||||
|
||||
# Verify metadata no longer contains profile
|
||||
metadata_path = profiles_dir / "metadata.json"
|
||||
if metadata_path.exists():
|
||||
metadata = json.loads(metadata_path.read_text())
|
||||
assert "test-profile" not in metadata.get("profiles", {})
|
||||
|
||||
def test_delete_nonexistent_profile(self, profile_manager):
|
||||
"""Test deleting a non-existent profile fails."""
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
profile_manager.delete_profile("nonexistent")
|
||||
|
||||
def test_delete_default_profile_fails(self, profile_manager):
|
||||
"""Test deleting default profile fails."""
|
||||
with pytest.raises(ValueError, match="Cannot delete default profile"):
|
||||
profile_manager.delete_profile("")
|
||||
|
||||
def test_set_active_profile(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test setting active profile."""
|
||||
# Create a profile
|
||||
profile_manager.create_profile("test-profile", {"KEY": "value"})
|
||||
|
||||
# Set as active
|
||||
profile_manager.set_active_profile("test-profile")
|
||||
|
||||
# Verify active profile file was created
|
||||
active_file = temp_hindsight_dir / "active_profile"
|
||||
assert active_file.exists()
|
||||
assert active_file.read_text() == "test-profile"
|
||||
|
||||
# Get active profile
|
||||
assert profile_manager.get_active_profile() == "test-profile"
|
||||
|
||||
def test_clear_active_profile(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test clearing active profile."""
|
||||
# Create and set active profile
|
||||
profile_manager.create_profile("test-profile", {"KEY": "value"})
|
||||
profile_manager.set_active_profile("test-profile")
|
||||
assert profile_manager.get_active_profile() == "test-profile"
|
||||
|
||||
# Clear active profile
|
||||
profile_manager.set_active_profile(None)
|
||||
assert profile_manager.get_active_profile() == ""
|
||||
|
||||
# Verify file was removed
|
||||
active_file = temp_hindsight_dir / "active_profile"
|
||||
assert not active_file.exists()
|
||||
|
||||
def test_set_active_nonexistent_profile_fails(self, profile_manager):
|
||||
"""Test setting non-existent profile as active fails."""
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
profile_manager.set_active_profile("nonexistent")
|
||||
|
||||
def test_list_profiles(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test listing profiles."""
|
||||
# Initially no profiles
|
||||
profiles = profile_manager.list_profiles()
|
||||
assert len(profiles) == 0
|
||||
|
||||
# Create default config
|
||||
(temp_hindsight_dir / "embed").write_text("KEY=value")
|
||||
|
||||
# List profiles - should show default
|
||||
profiles = profile_manager.list_profiles()
|
||||
assert len(profiles) == 1
|
||||
assert profiles[0].name == ""
|
||||
assert profiles[0].port == 8888
|
||||
|
||||
# Create named profiles
|
||||
profile_manager.create_profile("profile1", {"KEY": "value"})
|
||||
profile_manager.create_profile("profile2", {"KEY": "value"})
|
||||
|
||||
# List all profiles
|
||||
profiles = profile_manager.list_profiles()
|
||||
assert len(profiles) == 3
|
||||
|
||||
# Verify sorting (default first, then alphabetical)
|
||||
assert profiles[0].name == ""
|
||||
assert profiles[1].name == "profile1"
|
||||
assert profiles[2].name == "profile2"
|
||||
|
||||
def test_list_profiles_with_active(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test listing profiles shows active status."""
|
||||
profile_manager.create_profile("profile1", {"KEY": "value"})
|
||||
profile_manager.create_profile("profile2", {"KEY": "value"})
|
||||
|
||||
# Set profile1 as active
|
||||
profile_manager.set_active_profile("profile1")
|
||||
|
||||
# List profiles
|
||||
profiles = profile_manager.list_profiles()
|
||||
|
||||
# Find profile1
|
||||
profile1 = next(p for p in profiles if p.name == "profile1")
|
||||
profile2 = next(p for p in profiles if p.name == "profile2")
|
||||
|
||||
assert profile1.is_active is True
|
||||
assert profile2.is_active is False
|
||||
|
||||
def test_resolve_profile_paths_default(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test resolving paths for default profile."""
|
||||
paths = profile_manager.resolve_profile_paths("")
|
||||
|
||||
assert paths.config == temp_hindsight_dir / "embed"
|
||||
assert paths.lock == temp_hindsight_dir / "daemon.lock"
|
||||
assert paths.log == temp_hindsight_dir / "daemon.log"
|
||||
assert paths.port == 8888
|
||||
|
||||
def test_resolve_profile_paths_named(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test resolving paths for named profile."""
|
||||
# Create profile first
|
||||
profile_manager.create_profile("test-profile", {"KEY": "value"})
|
||||
|
||||
paths = profile_manager.resolve_profile_paths("test-profile")
|
||||
|
||||
assert paths.config == temp_hindsight_dir / "profiles" / "test-profile.env"
|
||||
assert paths.lock == temp_hindsight_dir / "profiles" / "test-profile.lock"
|
||||
assert paths.log == temp_hindsight_dir / "profiles" / "test-profile.log"
|
||||
assert 8889 <= paths.port <= 9888 # Port in valid range
|
||||
|
||||
def test_port_allocation_deterministic(self, profile_manager):
|
||||
"""Test that port allocation is deterministic for same profile name."""
|
||||
profile_manager.create_profile("test1", {"KEY": "value"})
|
||||
paths1 = profile_manager.resolve_profile_paths("test1")
|
||||
|
||||
# Delete and recreate
|
||||
profile_manager.delete_profile("test1")
|
||||
profile_manager.create_profile("test1", {"KEY": "value"})
|
||||
paths2 = profile_manager.resolve_profile_paths("test1")
|
||||
|
||||
# Port should be the same
|
||||
assert paths1.port == paths2.port
|
||||
|
||||
def test_port_allocation_unique(self, profile_manager):
|
||||
"""Test that different profiles get different ports."""
|
||||
profile_manager.create_profile("profile1", {"KEY": "value"})
|
||||
profile_manager.create_profile("profile2", {"KEY": "value"})
|
||||
|
||||
paths1 = profile_manager.resolve_profile_paths("profile1")
|
||||
paths2 = profile_manager.resolve_profile_paths("profile2")
|
||||
|
||||
# Ports should be different
|
||||
assert paths1.port != paths2.port
|
||||
|
||||
def test_daemon_running_status(self, profile_manager):
|
||||
"""Test that daemon running status is checked correctly."""
|
||||
with patch("httpx.Client") as mock_client:
|
||||
# Mock successful health check
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
|
||||
|
||||
profile_manager.create_profile("test", {"KEY": "value"})
|
||||
profiles = profile_manager.list_profiles()
|
||||
|
||||
test_profile = next(p for p in profiles if p.name == "test")
|
||||
assert test_profile.daemon_running is True
|
||||
|
||||
def test_update_existing_profile(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test updating an existing profile."""
|
||||
# Create profile
|
||||
profile_manager.create_profile("test", {"KEY": "old_value"})
|
||||
|
||||
# Update profile
|
||||
profile_manager.create_profile("test", {"KEY": "new_value"})
|
||||
|
||||
# Verify config was updated
|
||||
config_path = temp_hindsight_dir / "profiles" / "test.env"
|
||||
config_content = config_path.read_text()
|
||||
assert "KEY=new_value" in config_content
|
||||
assert "KEY=old_value" not in config_content
|
||||
|
||||
def test_metadata_persistence(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test that metadata persists across ProfileManager instances."""
|
||||
# Create profile with first manager
|
||||
profile_manager.create_profile("test", {"KEY": "value"})
|
||||
|
||||
# Create new manager and verify it sees the profile
|
||||
new_manager = ProfileManager()
|
||||
assert new_manager.profile_exists("test")
|
||||
|
||||
profiles = new_manager.list_profiles()
|
||||
test_profile = next(p for p in profiles if p.name == "test")
|
||||
assert test_profile.port > 0
|
||||
|
||||
def test_delete_active_profile_clears_active_file(self, profile_manager, temp_hindsight_dir):
|
||||
"""Test that deleting active profile clears the active_profile file."""
|
||||
profile_manager.create_profile("test", {"KEY": "value"})
|
||||
profile_manager.set_active_profile("test")
|
||||
|
||||
# Verify active
|
||||
assert profile_manager.get_active_profile() == "test"
|
||||
|
||||
# Delete profile
|
||||
profile_manager.delete_profile("test")
|
||||
|
||||
# Active profile should be cleared
|
||||
assert profile_manager.get_active_profile() == ""
|
||||
|
||||
|
||||
class TestResolveActiveProfile:
|
||||
"""Tests for resolve_active_profile function."""
|
||||
|
||||
def test_priority_env_var(self, monkeypatch, profile_manager):
|
||||
"""Test that HINDSIGHT_EMBED_PROFILE env var has highest priority."""
|
||||
# Set up all possible sources
|
||||
monkeypatch.setenv("HINDSIGHT_EMBED_PROFILE", "from-env")
|
||||
|
||||
profile_manager.create_profile("from-file", {"KEY": "value"})
|
||||
profile_manager.set_active_profile("from-file")
|
||||
|
||||
# Import cli to set override
|
||||
from hindsight_embed import cli
|
||||
|
||||
cli.set_cli_profile_override("from-flag")
|
||||
|
||||
# Env var should win
|
||||
assert resolve_active_profile() == "from-env"
|
||||
|
||||
def test_priority_cli_flag(self, monkeypatch, profile_manager):
|
||||
"""Test that CLI flag has second highest priority."""
|
||||
# No env var
|
||||
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
|
||||
|
||||
profile_manager.create_profile("from-file", {"KEY": "value"})
|
||||
profile_manager.set_active_profile("from-file")
|
||||
|
||||
# Import cli to set override
|
||||
from hindsight_embed import cli
|
||||
|
||||
cli.set_cli_profile_override("from-flag")
|
||||
|
||||
# CLI flag should win
|
||||
assert resolve_active_profile() == "from-flag"
|
||||
|
||||
def test_priority_active_file(self, monkeypatch, profile_manager):
|
||||
"""Test that active file has third priority."""
|
||||
# No env var or CLI flag
|
||||
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
|
||||
|
||||
from hindsight_embed import cli
|
||||
|
||||
cli.set_cli_profile_override(None)
|
||||
|
||||
profile_manager.create_profile("from-file", {"KEY": "value"})
|
||||
profile_manager.set_active_profile("from-file")
|
||||
|
||||
# Active file should be used
|
||||
assert resolve_active_profile() == "from-file"
|
||||
|
||||
def test_priority_default(self, monkeypatch, profile_manager):
|
||||
"""Test that default is used when no sources are set."""
|
||||
# No env var, CLI flag, or active file
|
||||
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
|
||||
|
||||
from hindsight_embed import cli
|
||||
|
||||
cli.set_cli_profile_override(None)
|
||||
|
||||
# Default should be used
|
||||
assert resolve_active_profile() == ""
|
||||
|
||||
|
||||
class TestValidateProfileExists:
|
||||
"""Tests for validate_profile_exists function."""
|
||||
|
||||
def test_validate_default_profile_always_passes(self):
|
||||
"""Test that default profile always passes validation."""
|
||||
# Default profile (empty string) should never fail
|
||||
validate_profile_exists("") # Should not raise
|
||||
|
||||
def test_validate_existing_profile_passes(self, profile_manager):
|
||||
"""Test that existing profile passes validation."""
|
||||
profile_manager.create_profile("test", {"KEY": "value"})
|
||||
validate_profile_exists("test") # Should not raise
|
||||
|
||||
def test_validate_nonexistent_profile_fails(self, profile_manager, capsys):
|
||||
"""Test that non-existent profile fails validation."""
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
validate_profile_exists("nonexistent")
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
# Check error message
|
||||
captured = capsys.readouterr()
|
||||
assert "Profile 'nonexistent' not found" in captured.err
|
||||
assert "hindsight-embed configure --profile nonexistent" in captured.err
|
||||
@@ -5,15 +5,9 @@ Biomimetic long-term memory for [OpenClaw](https://openclaw.ai) using [Hindsight
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Configure your LLM provider for memory extraction
|
||||
# Option A: OpenAI
|
||||
# 1. Configure your LLM provider
|
||||
export OPENAI_API_KEY="sk-your-key"
|
||||
|
||||
# Option B: Claude Code (no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
|
||||
# Option C: OpenAI Codex (no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
|
||||
|
||||
# 2. Install and enable the plugin
|
||||
openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
@@ -30,40 +24,6 @@ For full documentation, configuration options, troubleshooting, and development
|
||||
|
||||
**[OpenClaw Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/openclaw)**
|
||||
|
||||
## Development
|
||||
|
||||
To test local changes to the Hindsight package before publishing:
|
||||
|
||||
1. Add `embedPackagePath` to your plugin config in `~/.openclaw/openclaw.json`:
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"hindsight-openclaw": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"embedPackagePath": "/path/to/hindsight-wt3/hindsight-embed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. The plugin will use `uv run --directory <path> hindsight-embed` instead of `uvx hindsight-embed@latest`
|
||||
|
||||
3. To use a specific profile for testing:
|
||||
```bash
|
||||
# Check daemon status
|
||||
uvx hindsight-embed@latest -p openclaw daemon status
|
||||
|
||||
# View logs
|
||||
tail -f ~/.hindsight/profiles/openclaw.log
|
||||
|
||||
# List profiles
|
||||
uvx hindsight-embed@latest profile list
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Hindsight Documentation](https://vectorize.io/hindsight)
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
},
|
||||
"llmProvider": {
|
||||
"type": "string",
|
||||
"description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama', 'openai-codex', 'claude-code'). Takes priority over auto-detection but not over HINDSIGHT_API_LLM_PROVIDER env var.",
|
||||
"enum": ["openai", "anthropic", "gemini", "groq", "ollama", "openai-codex", "claude-code"]
|
||||
"description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama'). Takes priority over auto-detection but not over HINDSIGHT_API_LLM_PROVIDER env var.",
|
||||
"enum": ["openai", "anthropic", "gemini", "groq", "ollama"]
|
||||
},
|
||||
"llmModel": {
|
||||
"type": "string",
|
||||
@@ -37,15 +37,6 @@
|
||||
"llmApiKeyEnv": {
|
||||
"type": "string",
|
||||
"description": "Name of the env var holding the API key (e.g. 'MY_CUSTOM_KEY'). If not set, uses the standard env var for the chosen provider."
|
||||
},
|
||||
"embedPackagePath": {
|
||||
"type": "string",
|
||||
"description": "Local path to hindsight package for development (e.g. '/path/to/hindsight'). When set, uses 'uv run --directory <path>' instead of 'uvx hindsight-embed@latest'."
|
||||
},
|
||||
"apiPort": {
|
||||
"type": "number",
|
||||
"description": "Port for the openclaw profile daemon (default: 9077)",
|
||||
"default": 9077
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -78,14 +69,6 @@
|
||||
"llmApiKeyEnv": {
|
||||
"label": "API Key Env Var",
|
||||
"placeholder": "e.g. MY_CUSTOM_API_KEY (optional)"
|
||||
},
|
||||
"embedPackagePath": {
|
||||
"label": "Local Package Path (Dev)",
|
||||
"placeholder": "/path/to/hindsight (for local development)"
|
||||
},
|
||||
"apiPort": {
|
||||
"label": "API Port",
|
||||
"placeholder": "9077 (default)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,28 +16,12 @@ export class HindsightClient {
|
||||
private llmApiKey: string;
|
||||
private llmModel?: string;
|
||||
private embedVersion: string;
|
||||
private embedPackagePath?: string;
|
||||
|
||||
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion: string = 'latest', embedPackagePath?: string) {
|
||||
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion: string = 'latest') {
|
||||
this.llmProvider = llmProvider;
|
||||
this.llmApiKey = llmApiKey;
|
||||
this.llmModel = llmModel;
|
||||
this.embedVersion = embedVersion || 'latest';
|
||||
this.embedPackagePath = embedPackagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command prefix to run hindsight-embed (either local or from PyPI)
|
||||
*/
|
||||
private getEmbedCommandPrefix(): string {
|
||||
if (this.embedPackagePath) {
|
||||
// Local package: uv run --directory <path> hindsight-embed
|
||||
return `uv run --directory ${this.embedPackagePath} hindsight-embed`;
|
||||
} else {
|
||||
// PyPI package: uvx hindsight-embed@version
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
return `uvx ${embedPackage}`;
|
||||
}
|
||||
}
|
||||
|
||||
setBankId(bankId: string): void {
|
||||
@@ -50,11 +34,11 @@ export class HindsightClient {
|
||||
}
|
||||
|
||||
const escapedMission = mission.replace(/'/g, "'\\''"); // Escape single quotes
|
||||
const embedCmd = this.getEmbedCommandPrefix();
|
||||
const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
const cmd = `uvx ${embedPackage} bank mission ${this.bankId} '${escapedMission}'`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
|
||||
console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
|
||||
} catch (error) {
|
||||
// Don't fail if mission set fails - bank might not exist yet, will be created on first retain
|
||||
@@ -62,15 +46,29 @@ export class HindsightClient {
|
||||
}
|
||||
}
|
||||
|
||||
private getEnv(): Record<string, string> {
|
||||
const env: Record<string, string> = {
|
||||
...process.env,
|
||||
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
|
||||
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
|
||||
};
|
||||
|
||||
if (this.llmModel) {
|
||||
env.HINDSIGHT_EMBED_LLM_MODEL = this.llmModel;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
async retain(request: RetainRequest): Promise<RetainResponse> {
|
||||
const content = request.content.replace(/'/g, "'\\''"); // Escape single quotes
|
||||
const docId = request.document_id || 'conversation';
|
||||
|
||||
const embedCmd = this.getEmbedCommandPrefix();
|
||||
const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
const cmd = `uvx ${embedPackage} memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
|
||||
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
|
||||
|
||||
// Return a simple response
|
||||
@@ -88,11 +86,11 @@ export class HindsightClient {
|
||||
const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
|
||||
const maxTokens = request.max_tokens || 1024;
|
||||
|
||||
const embedCmd = this.getEmbedCommandPrefix();
|
||||
const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
const cmd = `uvx ${embedPackage} memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(cmd);
|
||||
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
|
||||
|
||||
// Parse JSON output - returns { entities: {...}, results: [...] }
|
||||
const response = JSON.parse(stdout);
|
||||
|
||||
@@ -15,7 +15,6 @@ export class HindsightEmbedManager {
|
||||
private llmBaseUrl?: string;
|
||||
private daemonIdleTimeout: number;
|
||||
private embedVersion: string;
|
||||
private embedPackagePath?: string;
|
||||
|
||||
constructor(
|
||||
port: number,
|
||||
@@ -24,12 +23,10 @@ export class HindsightEmbedManager {
|
||||
llmModel?: string,
|
||||
llmBaseUrl?: string,
|
||||
daemonIdleTimeout: number = 0, // Default: never timeout
|
||||
embedVersion: string = 'latest', // Default: latest
|
||||
embedPackagePath?: string // Local path to hindsight package
|
||||
embedVersion: string = 'latest' // Default: latest
|
||||
) {
|
||||
// Use the configured port (default: 9077 from config)
|
||||
this.port = port;
|
||||
this.baseUrl = `http://127.0.0.1:${port}`;
|
||||
this.port = 8888; // hindsight-embed daemon uses same port as API
|
||||
this.baseUrl = `http://127.0.0.1:8888`;
|
||||
this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
|
||||
this.llmProvider = llmProvider;
|
||||
this.llmApiKey = llmApiKey;
|
||||
@@ -37,36 +34,21 @@ export class HindsightEmbedManager {
|
||||
this.llmBaseUrl = llmBaseUrl;
|
||||
this.daemonIdleTimeout = daemonIdleTimeout;
|
||||
this.embedVersion = embedVersion || 'latest';
|
||||
this.embedPackagePath = embedPackagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command to run hindsight-embed (either local or from PyPI)
|
||||
*/
|
||||
private getEmbedCommand(): string[] {
|
||||
if (this.embedPackagePath) {
|
||||
// Local package: uv run --directory <path> hindsight-embed
|
||||
return ['uv', 'run', '--directory', this.embedPackagePath, 'hindsight-embed'];
|
||||
} else {
|
||||
// PyPI package: uvx hindsight-embed@version
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
return ['uvx', embedPackage];
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
console.log(`[Hindsight] Starting hindsight-embed daemon...`);
|
||||
|
||||
// Build environment variables using standard HINDSIGHT_API_LLM_* variables
|
||||
// Build environment variables
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
HINDSIGHT_API_LLM_PROVIDER: this.llmProvider,
|
||||
HINDSIGHT_API_LLM_API_KEY: this.llmApiKey,
|
||||
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
|
||||
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
|
||||
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: this.daemonIdleTimeout.toString(),
|
||||
};
|
||||
|
||||
if (this.llmModel) {
|
||||
env['HINDSIGHT_API_LLM_MODEL'] = this.llmModel;
|
||||
env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
|
||||
}
|
||||
|
||||
// Pass through base URL for OpenAI-compatible providers (OpenRouter, etc.)
|
||||
@@ -80,16 +62,16 @@ export class HindsightEmbedManager {
|
||||
env['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
|
||||
}
|
||||
|
||||
// Configure "openclaw" profile using hindsight-embed configure (non-interactive)
|
||||
console.log('[Hindsight] Configuring "openclaw" profile...');
|
||||
await this.configureProfile(env);
|
||||
// Write env vars to ~/.hindsight/config.env for daemon persistence
|
||||
await this.writeConfigEnv(env);
|
||||
|
||||
// Start hindsight-embed daemon with openclaw profile
|
||||
const embedCmd = this.getEmbedCommand();
|
||||
// Start hindsight-embed daemon (it manages itself)
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
const startDaemon = spawn(
|
||||
embedCmd[0],
|
||||
[...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'start'],
|
||||
'uvx',
|
||||
[embedPackage, 'daemon', 'start'],
|
||||
{
|
||||
env,
|
||||
stdio: 'pipe',
|
||||
}
|
||||
);
|
||||
@@ -132,8 +114,8 @@ export class HindsightEmbedManager {
|
||||
async stop(): Promise<void> {
|
||||
console.log('[Hindsight] Stopping hindsight-embed daemon...');
|
||||
|
||||
const embedCmd = this.getEmbedCommand();
|
||||
const stopDaemon = spawn(embedCmd[0], [...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'stop'], {
|
||||
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
|
||||
const stopDaemon = spawn('uvx', [embedPackage, 'daemon', 'stop'], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
@@ -190,60 +172,75 @@ export class HindsightEmbedManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async configureProfile(env: NodeJS.ProcessEnv): Promise<void> {
|
||||
// Build profile create command args with --merge, --port and --env flags
|
||||
// Use --merge to allow updating existing profile
|
||||
const createArgs = ['profile', 'create', 'openclaw', '--merge', '--port', this.port.toString()];
|
||||
private async writeConfigEnv(env: NodeJS.ProcessEnv): Promise<void> {
|
||||
const hindsightDir = join(homedir(), '.hindsight');
|
||||
const embedConfigPath = join(hindsightDir, 'embed');
|
||||
|
||||
// Add all environment variables as --env flags
|
||||
const envVars = [
|
||||
'HINDSIGHT_API_LLM_PROVIDER',
|
||||
'HINDSIGHT_API_LLM_MODEL',
|
||||
'HINDSIGHT_API_LLM_API_KEY',
|
||||
'HINDSIGHT_API_LLM_BASE_URL',
|
||||
'HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT',
|
||||
'HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU',
|
||||
'HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU',
|
||||
];
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(hindsightDir, { recursive: true });
|
||||
|
||||
for (const envVar of envVars) {
|
||||
if (env[envVar]) {
|
||||
createArgs.push('--env', `${envVar}=${env[envVar]}`);
|
||||
// Read existing config to preserve extra settings
|
||||
let existingContent = '';
|
||||
let extraSettings: string[] = [];
|
||||
try {
|
||||
existingContent = await fs.readFile(embedConfigPath, 'utf-8');
|
||||
// Extract non-LLM settings (like FORCE_CPU flags)
|
||||
const lines = existingContent.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#') &&
|
||||
!trimmed.startsWith('HINDSIGHT_EMBED_LLM_') &&
|
||||
!trimmed.startsWith('HINDSIGHT_API_LLM_') &&
|
||||
!trimmed.startsWith('HINDSIGHT_EMBED_BANK_ID') &&
|
||||
!trimmed.startsWith('HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT')) {
|
||||
extraSettings.push(line);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist yet, that's fine
|
||||
}
|
||||
|
||||
// Run profile create command (non-interactive, overwrites if exists)
|
||||
const embedCmd = this.getEmbedCommand();
|
||||
const create = spawn(embedCmd[0], [...embedCmd.slice(1), ...createArgs], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
// Build config file with header
|
||||
const configLines: string[] = [
|
||||
'# Hindsight Embed Configuration',
|
||||
'# Generated by OpenClaw Hindsight plugin',
|
||||
'',
|
||||
];
|
||||
|
||||
let output = '';
|
||||
create.stdout?.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
console.log(`[Hindsight] ${text.trim()}`);
|
||||
});
|
||||
// Add LLM config
|
||||
if (env.HINDSIGHT_EMBED_LLM_PROVIDER) {
|
||||
configLines.push(`HINDSIGHT_EMBED_LLM_PROVIDER=${env.HINDSIGHT_EMBED_LLM_PROVIDER}`);
|
||||
}
|
||||
if (env.HINDSIGHT_EMBED_LLM_MODEL) {
|
||||
configLines.push(`HINDSIGHT_EMBED_LLM_MODEL=${env.HINDSIGHT_EMBED_LLM_MODEL}`);
|
||||
}
|
||||
if (env.HINDSIGHT_EMBED_LLM_API_KEY) {
|
||||
configLines.push(`HINDSIGHT_EMBED_LLM_API_KEY=${env.HINDSIGHT_EMBED_LLM_API_KEY}`);
|
||||
}
|
||||
if (env.HINDSIGHT_API_LLM_BASE_URL) {
|
||||
configLines.push(`HINDSIGHT_API_LLM_BASE_URL=${env.HINDSIGHT_API_LLM_BASE_URL}`);
|
||||
}
|
||||
if (env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT) {
|
||||
configLines.push(`HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=${env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT}`);
|
||||
}
|
||||
|
||||
create.stderr?.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
console.error(`[Hindsight] ${text.trim()}`);
|
||||
});
|
||||
// Add platform-specific config (macOS FORCE_CPU flags)
|
||||
if (env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU) {
|
||||
configLines.push(`HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU}`);
|
||||
}
|
||||
if (env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU) {
|
||||
configLines.push(`HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU}`);
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
create.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('[Hindsight] Profile "openclaw" configured successfully');
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Profile create failed with code ${code}: ${output}`));
|
||||
}
|
||||
});
|
||||
// Add extra settings if they exist
|
||||
if (extraSettings.length > 0) {
|
||||
configLines.push('');
|
||||
configLines.push('# Additional settings');
|
||||
configLines.push(...extraSettings);
|
||||
}
|
||||
|
||||
create.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
// Write to file
|
||||
await fs.writeFile(embedConfigPath, configLines.join('\n') + '\n', 'utf-8');
|
||||
console.log(`[Hindsight] Wrote config to ${embedConfigPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ const PROVIDER_DETECTION = [
|
||||
{ name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
|
||||
{ name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
|
||||
{ name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
|
||||
{ name: 'openai-codex', keyEnv: '', defaultModel: 'gpt-5.2-codex' },
|
||||
{ name: 'claude-code', keyEnv: '', defaultModel: 'claude-sonnet-4-5-20250929' },
|
||||
];
|
||||
|
||||
function detectLLMConfig(pluginConfig?: PluginConfig): {
|
||||
@@ -54,9 +52,7 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
|
||||
|
||||
// Priority 1: If provider is explicitly set via env var, use that
|
||||
if (overrideProvider) {
|
||||
// Providers that don't require an API key (use OAuth or local models)
|
||||
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
|
||||
if (!overrideKey && !noKeyRequired.includes(overrideProvider)) {
|
||||
if (!overrideKey && overrideProvider !== 'ollama') {
|
||||
throw new Error(
|
||||
`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
|
||||
`Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`
|
||||
@@ -85,9 +81,7 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
|
||||
apiKey = process.env[providerInfo.keyEnv] || '';
|
||||
}
|
||||
|
||||
// Providers that don't require an API key (use OAuth or local models)
|
||||
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
|
||||
if (!apiKey && !noKeyRequired.includes(pluginConfig.llmProvider)) {
|
||||
if (!apiKey && pluginConfig.llmProvider !== 'ollama') {
|
||||
const keySource = pluginConfig.llmApiKeyEnv || providerInfo?.keyEnv || 'unknown';
|
||||
throw new Error(
|
||||
`Plugin config llmProvider is set to "${pluginConfig.llmProvider}" but no API key found.\n` +
|
||||
@@ -109,9 +103,8 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
|
||||
for (const providerInfo of PROVIDER_DETECTION) {
|
||||
const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
|
||||
|
||||
// Skip providers that don't use API keys in auto-detection (must be explicitly requested)
|
||||
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
|
||||
if (noKeyRequired.includes(providerInfo.name)) {
|
||||
// Skip ollama in auto-detection (must be explicitly requested)
|
||||
if (providerInfo.name === 'ollama') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -132,14 +125,11 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
|
||||
`Option 1: Set a standard provider API key (auto-detect):\n` +
|
||||
` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
|
||||
` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
|
||||
` export GEMINI_API_KEY=your-key # Uses gemini-2.5-flash\n` +
|
||||
` export GROQ_API_KEY=your-key # Uses openai/gpt-oss-20b\n\n` +
|
||||
`Option 2: Use Codex or Claude Code (no API key needed):\n` +
|
||||
` export HINDSIGHT_API_LLM_PROVIDER=openai-codex # Requires 'codex auth login'\n` +
|
||||
` export HINDSIGHT_API_LLM_PROVIDER=claude-code # Requires Claude Code CLI\n\n` +
|
||||
`Option 3: Set llmProvider in openclaw.json plugin config:\n` +
|
||||
` export GEMINI_API_KEY=your-key # Uses gemini-2.0-flash-exp\n` +
|
||||
` export GROQ_API_KEY=your-key # Uses llama-3.3-70b-versatile\n\n` +
|
||||
`Option 2: Set llmProvider in openclaw.json plugin config:\n` +
|
||||
` "llmProvider": "openai", "llmModel": "gpt-4o-mini"\n\n` +
|
||||
`Option 4: Override with Hindsight-specific env vars:\n` +
|
||||
`Option 3: Override with Hindsight-specific env vars:\n` +
|
||||
` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
|
||||
` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
|
||||
` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
|
||||
@@ -157,7 +147,6 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
|
||||
embedPort: config.embedPort || 0,
|
||||
daemonIdleTimeout: config.daemonIdleTimeout !== undefined ? config.daemonIdleTimeout : 0,
|
||||
embedVersion: config.embedVersion || 'latest',
|
||||
embedPackagePath: config.embedPackagePath,
|
||||
llmProvider: config.llmProvider,
|
||||
llmModel: config.llmModel,
|
||||
llmApiKeyEnv: config.llmApiKeyEnv,
|
||||
@@ -189,9 +178,9 @@ export default function (api: MoltbotPluginAPI) {
|
||||
}
|
||||
console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
|
||||
|
||||
// Get API port from config (default: 9077)
|
||||
const apiPort = pluginConfig.apiPort || 9077;
|
||||
console.log(`[Hindsight] API Port: ${apiPort}`);
|
||||
// Determine port
|
||||
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
|
||||
console.log(`[Hindsight] Port: ${port}`);
|
||||
|
||||
// Initialize in background (non-blocking)
|
||||
console.log('[Hindsight] Starting initialization in background...');
|
||||
@@ -200,14 +189,13 @@ export default function (api: MoltbotPluginAPI) {
|
||||
// Initialize embed manager
|
||||
console.log('[Hindsight] Creating HindsightEmbedManager...');
|
||||
embedManager = new HindsightEmbedManager(
|
||||
apiPort,
|
||||
port,
|
||||
llmConfig.provider,
|
||||
llmConfig.apiKey,
|
||||
llmConfig.model,
|
||||
llmConfig.baseUrl,
|
||||
pluginConfig.daemonIdleTimeout,
|
||||
pluginConfig.embedVersion,
|
||||
pluginConfig.embedPackagePath
|
||||
pluginConfig.embedVersion
|
||||
);
|
||||
|
||||
// Start the embedded server
|
||||
@@ -216,7 +204,7 @@ export default function (api: MoltbotPluginAPI) {
|
||||
|
||||
// Initialize client
|
||||
console.log('[Hindsight] Creating HindsightClient...');
|
||||
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
|
||||
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
|
||||
|
||||
// Use openclaw bank
|
||||
console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
|
||||
@@ -275,22 +263,21 @@ export default function (api: MoltbotPluginAPI) {
|
||||
console.log('[Hindsight] Reinitializing daemon...');
|
||||
const pluginConfig = getPluginConfig(api);
|
||||
const llmConfig = detectLLMConfig(pluginConfig);
|
||||
const apiPort = pluginConfig.apiPort || 9077;
|
||||
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
|
||||
|
||||
embedManager = new HindsightEmbedManager(
|
||||
apiPort,
|
||||
port,
|
||||
llmConfig.provider,
|
||||
llmConfig.apiKey,
|
||||
llmConfig.model,
|
||||
llmConfig.baseUrl,
|
||||
pluginConfig.daemonIdleTimeout,
|
||||
pluginConfig.embedVersion,
|
||||
pluginConfig.embedPackagePath
|
||||
pluginConfig.embedVersion
|
||||
);
|
||||
|
||||
await embedManager.start();
|
||||
|
||||
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
|
||||
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
|
||||
client.setBankId(BANK_NAME);
|
||||
|
||||
if (pluginConfig.bankMission) {
|
||||
|
||||
@@ -32,11 +32,9 @@ export interface PluginConfig {
|
||||
embedPort?: number;
|
||||
daemonIdleTimeout?: number; // Seconds before daemon shuts down (0 = never)
|
||||
embedVersion?: string; // hindsight-embed version (default: "latest")
|
||||
embedPackagePath?: string; // Local path to hindsight package (e.g. '/path/to/hindsight')
|
||||
llmProvider?: string; // LLM provider override (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama')
|
||||
llmModel?: string; // LLM model override (e.g. 'gpt-4o-mini', 'claude-3-5-haiku-20241022')
|
||||
llmApiKeyEnv?: string; // Env var name holding the API key (e.g. 'MY_CUSTOM_KEY')
|
||||
apiPort?: number; // Port for openclaw profile daemon (default: 9077)
|
||||
}
|
||||
|
||||
export interface ServiceConfig {
|
||||
|
||||
@@ -1688,7 +1688,6 @@ version = "0.4.7"
|
||||
source = { editable = "hindsight-embed" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -1699,10 +1698,7 @@ dev = [
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
]
|
||||
requires-dist = [{ name = "httpx", specifier = ">=0.27.0" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -1786,7 +1782,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "1.3.7"
|
||||
version = "1.3.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -1800,9 +1796,9 @@ dependencies = [
|
||||
{ name = "typer-slim" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/3f/352efd52136bfd8aa9280c6d4a445869226ae2ccd49ddad4f62e90cfd168/huggingface_hub-1.3.7.tar.gz", hash = "sha256:5f86cd48f27131cdbf2882699cbdf7a67dd4cbe89a81edfdc31211f42e4a5fd1", size = 627537 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/e9/2658cb9bc4c72a67b7f87650e827266139befaf499095883d30dabc4d49f/huggingface_hub-1.3.5.tar.gz", hash = "sha256:8045aca8ddab35d937138f3c386c6d43a275f53437c5c64cdc9aa8408653b4ed", size = 627456 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/89/bfbfde252d649fae8d5f09b14a2870e5672ed160c1a6629301b3e5302621/huggingface_hub-1.3.7-py3-none-any.whl", hash = "sha256:8155ce937038fa3d0cb4347d752708079bc85e6d9eb441afb44c84bcf48620d2", size = 536728 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/84/a579b95c46fe8e319f89dc700c087596f665141575f4dcf136aaa97d856f/huggingface_hub-1.3.5-py3-none-any.whl", hash = "sha256:fe332d7f86a8af874768452295c22cd3f37730fb2463cf6cc3295e26036f8ef9", size = 536675 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1915,87 +1911,87 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.13.0"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2099,7 +2095,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.2.8"
|
||||
version = "1.2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -2111,9 +2107,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/cc/55bf57b83cbc164cbf84cbf0c5e4fb640d673546af131db70797b97b125b/langchain_core-1.2.8.tar.gz", hash = "sha256:76d933c3f4cfd8484d8131c39bf25f562e2df4d0d5fe3218e05ff773210713b6", size = 814506 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/d4/37fef9639b701c1fb1eea9e68447b72d86852ca3dc3253cdfd9c0afe228d/langchain_core-1.2.8-py3-none-any.whl", hash = "sha256:c732301272d63cfbcd75d114540257678627878f11b87046241272a25ba12ea7", size = 495753 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2130,7 +2126,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.6.8"
|
||||
version = "0.6.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -2143,9 +2139,9 @@ dependencies = [
|
||||
{ name = "xxhash" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/15/35f49a0b2efd33002fdcb9a7b0bdb65d77e40b4739104ffe843a3479874a/langsmith-0.6.8.tar.gz", hash = "sha256:3a7eb7155f2839dc729a5aa5b0bfc4aa1cb617b09a2290cf77031041271a7cdf", size = 973475 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/bb/b8a196c9b9a7ca8b8845eaec7dbae35bcfcb0da3068794e76b29211eae2b/langsmith-0.6.7.tar.gz", hash = "sha256:d89c604a18fc606b7835d8e7924f7cdbe130ca2207bdff8f989590e50d65b802", size = 963940 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/2d/2389e65522ebeab17489df72b4fabcfc661fced8af178aa6c2bc3b9afff5/langsmith-0.6.8-py3-none-any.whl", hash = "sha256:d17da18aeef15fdb4c3baec348bad64056591d785629cd5ba4846fd93cab166b", size = 319165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/9a/5bc17ea3c746363e73c11df5e89068fea1ed175ca9c00fdb6886efc35b21/langsmith-0.6.7-py3-none-any.whl", hash = "sha256:4bd4372b8bf724b86314f64644562b5598407614e04e74b536c09490d153bd61", size = 309369 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2893,79 +2889,79 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.11.7"
|
||||
version = "3.11.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/fd/d6b0a36854179b93ed77839f107c4089d91cccc9f9ba1b752b6e3bac5f34/orjson-3.11.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e259e85a81d76d9665f03d6129e09e4435531870de5961ddcd0bf6e3a7fde7d7", size = 250029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/bb/22902619826641cf3b627c24aab62e2ad6b571bdd1d34733abb0dd57f67a/orjson-3.11.6-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:52263949f41b4a4822c6b1353bcc5ee2f7109d53a3b493501d3369d6d0e7937a", size = 134518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/90/7a818da4bba1de711a9653c420749c0ac95ef8f8651cbc1dca551f462fe0/orjson-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6439e742fa7834a24698d358a27346bb203bff356ae0402e7f5df8f749c621a8", size = 137917 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/0f/02846c1cac8e205cb3822dd8aa8f9114acda216f41fd1999ace6b543418d/orjson-3.11.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b81ffd68f084b4e993e3867acb554a049fa7787cc8710bbcc1e26965580d99be", size = 134923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cf/aeaf683001b474bb3c3c757073a4231dfdfe8467fceaefa5bfd40902c99f/orjson-3.11.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5a5468e5e60f7ef6d7f9044b06c8f94a3c56ba528c6e4f7f06ae95164b595ec", size = 140752 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/fe/dad52d8315a65f084044a0819d74c4c9daf9ebe0681d30f525b0d29a31f0/orjson-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72c5005eb45bd2535632d4f3bec7ad392832cfc46b62a3021da3b48a67734b45", size = 144201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/bc/ab070dd421565b831801077f1e390c4d4af8bfcecafc110336680a33866b/orjson-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b14dd49f3462b014455a28a4d810d3549bf990567653eb43765cd847df09145", size = 142380 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/d8/4b581c725c3a308717f28bf45a9fdac210bca08b67e8430143699413ff06/orjson-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bb2c1ea30ef302f0f89f9bf3e7f9ab5e2af29dc9f80eb87aa99788e4e2d65", size = 145582 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/a2/09aab99b39f9a7f175ea8fa29adb9933a3d01e7d5d603cdee7f1c40c8da2/orjson-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:825e0a85d189533c6bff7e2fc417a28f6fcea53d27125c4551979aecd6c9a197", size = 147270 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2f/5ef8eaf7829dc50da3bf497c7775b21ee88437bc8c41f959aa3504ca6631/orjson-3.11.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b04575417a26530637f6ab4b1f7b4f666eb0433491091da4de38611f97f2fcf3", size = 421222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/b0/dd6b941294c2b5b13da5fdc7e749e58d0c55a5114ab37497155e83050e95/orjson-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b83eb2e40e8c4da6d6b340ee6b1d6125f5195eb1b0ebb7eac23c6d9d4f92d224", size = 155562 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/09/43924331a847476ae2f9a16bd6d3c9dab301265006212ba0d3d7fd58763a/orjson-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1f42da604ee65a6b87eef858c913ce3e5777872b19321d11e6fc6d21de89b64f", size = 147432 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/e9/d9865961081816909f6b49d880749dbbd88425afd7c5bbce0549e2290d77/orjson-3.11.6-cp311-cp311-win32.whl", hash = "sha256:5ae45df804f2d344cffb36c43fdf03c82fb6cd247f5faa41e21891b40dfbf733", size = 139623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/f9/6836edb92f76eec1082919101eb1145d2f9c33c8f2c5e6fa399b82a2aaa8/orjson-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:f4295948d65ace0a2d8f2c4ccc429668b7eb8af547578ec882e16bf79b0050b2", size = 136647 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/0c/4954082eea948c9ae52ee0bcbaa2f99da3216a71bcc314ab129bde22e565/orjson-3.11.6-cp311-cp311-win_arm64.whl", hash = "sha256:314e9c45e0b81b547e3a1cfa3df3e07a815821b3dac9fe8cb75014071d0c16a4", size = 135327 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/ba/759f2879f41910b7e5e0cdbd9cf82a4f017c527fb0e972e9869ca7fe4c8e/orjson-3.11.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6f03f30cd8953f75f2a439070c743c7336d10ee940da918d71c6f3556af3ddcf", size = 249988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/70/54cecb929e6c8b10104fcf580b0cc7dc551aa193e83787dd6f3daba28bb5/orjson-3.11.6-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:af44baae65ef386ad971469a8557a0673bb042b0b9fd4397becd9c2dfaa02588", size = 134445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6f/ec0309154457b9ba1ad05f11faa4441f76037152f75e1ac577db3ce7ca96/orjson-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c310a48542094e4f7dbb6ac076880994986dda8ca9186a58c3cb70a3514d3231", size = 137708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/52/3c71b80840f8bab9cb26417302707b7716b7d25f863f3a541bcfa232fe6e/orjson-3.11.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8dfa7a5d387f15ecad94cb6b2d2d5f4aeea64efd8d526bfc03c9812d01e1cc0", size = 134798 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/51/b490a43b22ff736282360bd02e6bded455cf31dfc3224e01cd39f919bbd2/orjson-3.11.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba8daee3e999411b50f8b50dbb0a3071dd1845f3f9a1a0a6fa6de86d1689d84d", size = 140839 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/bc/4bcfe4280c1bc63c5291bb96f98298845b6355da2226d3400e17e7b51e53/orjson-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89d104c974eafd7436d7a5fdbc57f7a1e776789959a2f4f1b2eab5c62a339f4", size = 144080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/74/22970f9ead9ab1f1b5f8c227a6c3aa8d71cd2c5acd005868a1d44f2362fa/orjson-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e2e2456788ca5ea75616c40da06fc885a7dc0389780e8a41bf7c5389ba257b", size = 142435 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/34/d564aff85847ab92c82ee43a7a203683566c2fca0723a5f50aebbe759603/orjson-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a42efebc45afabb1448001e90458c4020d5c64fbac8a8dc4045b777db76cb5a", size = 145631 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ef/016957a3890752c4aa2368326ea69fa53cdc1fdae0a94a542b6410dbdf52/orjson-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71b7cbef8471324966c3738c90ba38775563ef01b512feb5ad4805682188d1b9", size = 147058 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/cc/9a899c3972085645b3225569f91a30e221f441e5dc8126e6d060b971c252/orjson-3.11.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f8515e5910f454fe9a8e13c2bb9dc4bae4c1836313e967e72eb8a4ad874f0248", size = 421161 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a8/767d3fbd6d9b8fdee76974db40619399355fd49bf91a6dd2c4b6909ccf05/orjson-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:300360edf27c8c9bf7047345a94fddf3a8b8922df0ff69d71d854a170cb375cf", size = 155757 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0b/205cd69ac87e2272e13ef3f5f03a3d4657e317e38c1b08aaa2ef97060bbc/orjson-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:caaed4dad39e271adfadc106fab634d173b2bb23d9cf7e67bd645f879175ebfc", size = 147446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/c5/dd9f22aa9f27c54c7d05cc32f4580c9ac9b6f13811eeb81d6c4c3f50d6b1/orjson-3.11.6-cp312-cp312-win32.whl", hash = "sha256:955368c11808c89793e847830e1b1007503a5923ddadc108547d3b77df761044", size = 139717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/a1/e62fc50d904486970315a1654b8cfb5832eb46abb18cd5405118e7e1fc79/orjson-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:2c68de30131481150073d90a5d227a4a421982f42c025ecdfb66157f9579e06f", size = 136711 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/3d/b4fefad8bdf91e0fe212eb04975aeb36ea92997269d68857efcc7eb1dda3/orjson-3.11.6-cp312-cp312-win_arm64.whl", hash = "sha256:65dfa096f4e3a5e02834b681f539a87fbe85adc82001383c0db907557f666bfc", size = 135212 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/45/d9c71c8c321277bc1ceebf599bc55ba826ae538b7c61f287e9a7e71bd589/orjson-3.11.6-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e4ae1670caabb598a88d385798692ce2a1b2f078971b3329cfb85253c6097f5b", size = 249828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/7e/4afcf4cfa9c2f93846d70eee9c53c3c0123286edcbeb530b7e9bd2aea1b2/orjson-3.11.6-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:2c6b81f47b13dac2caa5d20fbc953c75eb802543abf48403a4703ed3bff225f0", size = 134339 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/10/6d2b8a064c8d2411d3d0ea6ab43125fae70152aef6bea77bb50fa54d4097/orjson-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:647d6d034e463764e86670644bdcaf8e68b076e6e74783383b01085ae9ab334f", size = 137662 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/50/5804ea7d586baf83ee88969eefda97a24f9a5bdba0727f73e16305175b26/orjson-3.11.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8523b9cc4ef174ae52414f7699e95ee657c16aa18b3c3c285d48d7966cce9081", size = 134626 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2e/f0492ed43e376722bb4afd648e06cc1e627fc7ec8ff55f6ee739277813ea/orjson-3.11.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313dfd7184cde50c733fc0d5c8c0e2f09017b573afd11dc36bd7476b30b4cb17", size = 140873 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/15/6f874857463421794a303a39ac5494786ad46a4ab46d92bda6705d78c5aa/orjson-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905ee036064ff1e1fd1fb800055ac477cdcb547a78c22c1bc2bbf8d5d1a6fb42", size = 144044 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/c7/b7223a3a70f1d0cc2d86953825de45f33877ee1b124a91ca1f79aa6e643f/orjson-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce374cb98411356ba906914441fc993f271a7a666d838d8de0e0900dd4a4bc12", size = 142396 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/e3/aa1b6d3ad3cd80f10394134f73ae92a1d11fdbe974c34aa199cc18bb5fcf/orjson-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cded072b9f65fcfd188aead45efa5bd528ba552add619b3ad2a81f67400ec450", size = 145600 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/cf/e4aac5a46cbd39d7e769ef8650efa851dfce22df1ba97ae2b33efe893b12/orjson-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ab85bdbc138e1f73a234db6bb2e4cc1f0fcec8f4bd2bd2430e957a01aadf746", size = 146967 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/04/975b86a4bcf6cfeda47aad15956d52fbeda280811206e9967380fa9355c8/orjson-3.11.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:351b96b614e3c37a27b8ab048239ebc1e0be76cc17481a430d70a77fb95d3844", size = 421003 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/d1/0369d0baf40eea5ff2300cebfe209883b2473ab4aa4c4974c8bd5ee42bb2/orjson-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f9959c85576beae5cdcaaf39510b15105f1ee8b70d5dacd90152617f57be8c83", size = 155695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/1f/d10c6d6ae26ff1d7c3eea6fd048280ef2e796d4fb260c5424fd021f68ecf/orjson-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75682d62b1b16b61a30716d7a2ec1f4c36195de4a1c61f6665aedd947b93a5d5", size = 147392 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/43/7479921c174441a0aa5277c313732e20713c0969ac303be9f03d88d3db5d/orjson-3.11.6-cp313-cp313-win32.whl", hash = "sha256:40dc277999c2ef227dcc13072be879b4cfd325502daeb5c35ed768f706f2bf30", size = 139718 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/bc/9ffe7dfbf8454bc4e75bb8bf3a405ed9e0598df1d3535bb4adcd46be07d0/orjson-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0f6e9f8ff7905660bc3c8a54cd4a675aa98f7f175cf00a59815e2ff42c0d916", size = 136635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/7e/51fa90b451470447ea5023b20d83331ec741ae28d1e6d8ed547c24e7de14/orjson-3.11.6-cp313-cp313-win_arm64.whl", hash = "sha256:1608999478664de848e5900ce41f25c4ecdfc4beacbc632b6fd55e1a586e5d38", size = 135175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/9f/46ca908abaeeec7560638ff20276ab327b980d73b3cc2f5b205b4a1c60b3/orjson-3.11.6-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6026db2692041d2a23fe2545606df591687787825ad5821971ef0974f2c47630", size = 249823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/78/ca478089818d18c9cd04f79c43f74ddd031b63c70fa2a946eb5e85414623/orjson-3.11.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:132b0ab2e20c73afa85cf142e547511feb3d2f5b7943468984658f3952b467d4", size = 134328 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/5e/cbb9d830ed4e47f4375ad8eef8e4fff1bf1328437732c3809054fc4e80be/orjson-3.11.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b376fb05f20a96ec117d47987dd3b39265c635725bda40661b4c5b73b77b5fde", size = 137651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/3a/35df6558c5bc3a65ce0961aefee7f8364e59af78749fc796ea255bfa0cf5/orjson-3.11.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:954dae4e080574672a1dfcf2a840eddef0f27bd89b0e94903dd0824e9c1db060", size = 134596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8e/3d32dd7b7f26a19cc4512d6ed0ae3429567c71feef720fe699ff43c5bc9e/orjson-3.11.6-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe515bb89d59e1e4b48637a964f480b35c0a2676de24e65e55310f6016cca7ce", size = 140923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9c/1efbf5c99b3304f25d6f0d493a8d1492ee98693637c10ce65d57be839d7b/orjson-3.11.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:380f9709c275917af28feb086813923251e11ee10687257cd7f1ea188bcd4485", size = 144068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/83/0d19eeb5be797de217303bbb55dde58dba26f996ed905d301d98fd2d4637/orjson-3.11.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8173e0d3f6081e7034c51cf984036d02f6bab2a2126de5a759d79f8e5a140e7", size = 142493 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a7/573fec3df4dc8fc259b7770dc6c0656f91adce6e19330c78d23f87945d1e/orjson-3.11.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dddf9ba706294906c56ef5150a958317b09aa3a8a48df1c52ccf22ec1907eac", size = 145616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/0e/23551b16f21690f7fd5122e3cf40fdca5d77052a434d0071990f97f5fe2f/orjson-3.11.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cbae5c34588dc79938dffb0b6fbe8c531f4dc8a6ad7f39759a9eb5d2da405ef2", size = 146951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/63/5e6c8f39805c39123a18e412434ea364349ee0012548d08aa586e2bd6aa9/orjson-3.11.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f75c318640acbddc419733b57f8a07515e587a939d8f54363654041fd1f4e465", size = 421024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/4d/724975cf0087f6550bd01fd62203418afc0ea33fd099aed318c5bcc52df8/orjson-3.11.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0ab8d13aa2a3e98b4a43487c9205b2c92c38c054b4237777484d503357c8437", size = 155774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a3/f4c4e3f46b55db29e0a5f20493b924fc791092d9a03ff2068c9fe6c1002f/orjson-3.11.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f884c7fb1020d44612bd7ac0db0babba0e2f78b68d9a650c7959bf99c783773f", size = 147393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/86/6f5529dd27230966171ee126cecb237ed08e9f05f6102bfaf63e5b32277d/orjson-3.11.6-cp314-cp314-win32.whl", hash = "sha256:8d1035d1b25732ec9f971e833a3e299d2b1a330236f75e6fd945ad982c76aaf3", size = 139760 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/b5/91ae7037b2894a6b5002fb33f4fbccec98424a928469835c3837fbb22a9b/orjson-3.11.6-cp314-cp314-win_amd64.whl", hash = "sha256:931607a8865d21682bb72de54231655c86df1870502d2962dbfd12c82890d077", size = 136633 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3764,7 +3760,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-fasthtml"
|
||||
version = "0.12.40"
|
||||
version = "0.12.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
@@ -3778,9 +3774,9 @@ dependencies = [
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/5e/e01017b6214b18627df267e4bf0a6927b3e1d18bed025957012618d8fb69/python_fasthtml-0.12.40.tar.gz", hash = "sha256:5cfeb7d37a72cf59cbb1e328fa1f29dbdc428e8f8d5a7c41d9bb8d5443047e7b", size = 71572 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/60/6b3b7ec0ab8054928b74cfa7ee49d90c3cde576268e054ba9f17989b7c1e/python_fasthtml-0.12.39.tar.gz", hash = "sha256:ae324b34a1586698b052cad8e9ff9255bd6cb486b01b946e95b9b63f220a5532", size = 70842 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/f2/fd91c0ed5a75b2f2b55348c3b9aa7382534744c223d8e6dd104164a087c7/python_fasthtml-0.12.40-py3-none-any.whl", hash = "sha256:6d87e67ed40c69d2e98af8df1965ef47b411b1fd9ef55e257269b83c22428611", size = 75204 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/1b/354a0ab669703f87e9ab0464670be8791a9de59c2693ffb0d9a584927b5e/python_fasthtml-0.12.39-py3-none-any.whl", hash = "sha256:d9d2a173714852f906d1821f5eeb40640db2ef2b2f997edee63471feb58127be", size = 73179 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user