From c5700ff5b4990269ff1269bbef3ca039d8869e71 Mon Sep 17 00:00:00 2001 From: grimmjoww578 Date: Thu, 26 Mar 2026 06:23:03 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Windows=20native=20support=20=E2=80=94?= =?UTF-8?q?=20run=20Hindsight=20without=20Docker=20(#699)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Windows native support — run Hindsight without Docker on Windows Four compatibility fixes that allow Hindsight to run natively on Windows with an external PostgreSQL + pgvector installation: 1. **pyproject.toml**: Conditional event loop dependency - `winloop` on Windows (sys_platform == 'win32') - `uvloop` on Linux/macOS (sys_platform != 'win32') 2. **main.py**: winloop integration via `winloop.install()` - Patches asyncio event loop policy globally before uvicorn starts - uvicorn sees "asyncio" but runs winloop underneath (same perf as uvloop) - Falls back to default asyncio if winloop unavailable 3. **metrics.py**: Guard `resource` module import - `resource` is Unix-only (getrusage, getrlimit) - Conditional import with None fallback - Skip process metrics collection on Windows 4. **fact_storage.py**: Cross-platform strftime - `%-d` (no-padding day) is glibc-only, fails on Windows - Replaced with `%d` + `.replace(" 0", " ")` for same output ## Windows Setup Guide ### Prerequisites - Python 3.11+ - PostgreSQL 17 with pgvector extension - Ollama (for local embeddings) or external embedding provider ### Install PostgreSQL + pgvector on Windows ```bash winget install PostgreSQL.PostgreSQL.17 # Build pgvector from source (requires Visual Studio Build Tools) git clone https://github.com/pgvector/pgvector.git # In x64 Native Tools Command Prompt: set PGROOT=C:\Program Files\PostgreSQL\17 nmake /F Makefile.win nmake /F Makefile.win install # Enable extension psql -U postgres -d hindsight -c "CREATE EXTENSION IF NOT EXISTS vector;" ``` ### Install and Run Hindsight ```bash pip install -e ".[embedded-db]" # Set environment variables set HINDSIGHT_API_LLM_PROVIDER=openai set HINDSIGHT_API_LLM_API_KEY=your-api-key set HINDSIGHT_API_LLM_BASE_URL=https://your-llm-endpoint/v1 set HINDSIGHT_API_LLM_MODEL=your-model set HINDSIGHT_API_DATABASE_URL=postgresql://postgres@localhost:5432/hindsight set HINDSIGHT_API_EMBEDDING_PROVIDER=ollama set HINDSIGHT_API_PORT=8889 hindsight-api ``` Data persists in PostgreSQL on your local disk — survives reboots, updates, and anything that would wipe a Docker volume. Tested on Windows 11 with PostgreSQL 17.9, pgvector 0.8.2, Python 3.11, RTX 5080 (CUDA embeddings + reranking). Co-Authored-By: Claude Opus 4.6 (1M context) * fix: handle strftime ValueError on Windows in fact_storage The strftime call on occurred_start/occurred_end can raise ValueError on Windows when the datetime object has unexpected format properties. Wrap in try/except to gracefully skip date signal rather than crash the entire retain batch. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../engine/retain/fact_storage.py | 10 +++++-- hindsight-api-slim/hindsight_api/main.py | 27 ++++++++++++------- hindsight-api-slim/hindsight_api/metrics.py | 7 ++++- hindsight-api-slim/pyproject.toml | 3 ++- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/hindsight-api-slim/hindsight_api/engine/retain/fact_storage.py b/hindsight-api-slim/hindsight_api/engine/retain/fact_storage.py index c0ac9b12..412ecbfb 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/fact_storage.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/fact_storage.py @@ -81,9 +81,15 @@ async def insert_facts_batch( if fact.entities: signal_parts.extend(e.name for e in fact.entities) if fact.occurred_start: - signal_parts.append(fact.occurred_start.strftime("%B %-d %Y")) + try: + signal_parts.append(fact.occurred_start.strftime("%B %d %Y").lstrip("0").replace(" 0", " ")) + except (ValueError, AttributeError): + pass if fact.occurred_end and fact.occurred_end != fact.occurred_start: - signal_parts.append(fact.occurred_end.strftime("%B %-d %Y")) + try: + signal_parts.append(fact.occurred_end.strftime("%B %d %Y").lstrip("0").replace(" 0", " ")) + except (ValueError, AttributeError): + pass text_signals_list.append(" ".join(signal_parts) if signal_parts else None) # Batch insert all facts diff --git a/hindsight-api-slim/hindsight_api/main.py b/hindsight-api-slim/hindsight_api/main.py index 2cd23ae0..b8349fe3 100644 --- a/hindsight-api-slim/hindsight_api/main.py +++ b/hindsight-api-slim/hindsight_api/main.py @@ -211,15 +211,24 @@ def main(): # Prepare uvicorn config # When using workers or reload, we must use import string so each worker can import the app use_import_string = args.workers > 1 or args.reload - # Check for uvloop availability - try: - import uvloop # noqa: F401 - - loop_impl = "uvloop" - print("uvloop available, will use for event loop") - except ImportError: - loop_impl = "asyncio" - print("uvloop not installed, using default asyncio event loop") + # Check for uvloop/winloop availability + import sys + loop_impl = "asyncio" + if sys.platform == "win32": + try: + import winloop + winloop.install() # Patches asyncio globally — uvicorn uses "asyncio" but gets winloop + loop_impl = "asyncio" # Tell uvicorn "asyncio" — it's now winloop underneath + print("winloop installed as asyncio event loop policy (Windows uvloop port)") + except ImportError: + print("winloop not installed, using default asyncio event loop") + else: + try: + import uvloop # noqa: F401 + loop_impl = "uvloop" + print("uvloop available, will use for event loop") + except ImportError: + print("uvloop not installed, using default asyncio event loop") uvicorn_config = { "app": "hindsight_api.server:app" if use_import_string else app, diff --git a/hindsight-api-slim/hindsight_api/metrics.py b/hindsight-api-slim/hindsight_api/metrics.py index 5fd5c3d5..8fddc281 100644 --- a/hindsight-api-slim/hindsight_api/metrics.py +++ b/hindsight-api-slim/hindsight_api/metrics.py @@ -13,7 +13,10 @@ This module provides metrics for: import logging import os -import resource +try: + import resource +except ImportError: + resource = None # Windows doesn't have resource module import threading import time from contextlib import contextmanager @@ -455,6 +458,8 @@ class MetricsCollector(MetricsCollectorBase): def _setup_process_metrics(self): """Set up observable gauges for process metrics.""" + if resource is None: + return # Skip process metrics on Windows def get_cpu_times(_options): """Get process CPU times.""" diff --git a/hindsight-api-slim/pyproject.toml b/hindsight-api-slim/pyproject.toml index 767e6d24..916c496f 100644 --- a/hindsight-api-slim/pyproject.toml +++ b/hindsight-api-slim/pyproject.toml @@ -43,7 +43,8 @@ dependencies = [ "litellm>=1.0.0,<=1.82.6", # 1.82.7+ contains a supply chain attack (malicious .pth credential stealer) "markitdown[pdf,docx,pptx,xlsx,xls]>=0.1.4", # File to markdown conversion "obstore>=0.4.0", # S3/GCS/Azure object storage client (Rust-backed) - "uvloop>=0.22.1", + "winloop>=0.1.0; sys_platform == 'win32'", + "uvloop>=0.22.1; sys_platform != 'win32'", # Transitive dependency security fixes "pyasn1>=0.6.3", # DoS vulnerability fix "urllib3>=2.6.3", # Decompression-bomb safeguards bypass fix