feat: Windows native support — run Hindsight without Docker (#699)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
grimmjoww578 2026-03-26 06:23:03 -04:00 committed by GitHub
parent 6bb83f4600
commit c5700ff5b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 34 additions and 13 deletions

View file

@ -81,9 +81,15 @@ async def insert_facts_batch(
if fact.entities: if fact.entities:
signal_parts.extend(e.name for e in fact.entities) signal_parts.extend(e.name for e in fact.entities)
if fact.occurred_start: 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: 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) text_signals_list.append(" ".join(signal_parts) if signal_parts else None)
# Batch insert all facts # Batch insert all facts

View file

@ -211,14 +211,23 @@ def main():
# Prepare uvicorn config # Prepare uvicorn config
# When using workers or reload, we must use import string so each worker can import the app # 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 use_import_string = args.workers > 1 or args.reload
# Check for uvloop availability # 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: try:
import uvloop # noqa: F401 import uvloop # noqa: F401
loop_impl = "uvloop" loop_impl = "uvloop"
print("uvloop available, will use for event loop") print("uvloop available, will use for event loop")
except ImportError: except ImportError:
loop_impl = "asyncio"
print("uvloop not installed, using default asyncio event loop") print("uvloop not installed, using default asyncio event loop")
uvicorn_config = { uvicorn_config = {

View file

@ -13,7 +13,10 @@ This module provides metrics for:
import logging import logging
import os import os
try:
import resource import resource
except ImportError:
resource = None # Windows doesn't have resource module
import threading import threading
import time import time
from contextlib import contextmanager from contextlib import contextmanager
@ -455,6 +458,8 @@ class MetricsCollector(MetricsCollectorBase):
def _setup_process_metrics(self): def _setup_process_metrics(self):
"""Set up observable gauges for process metrics.""" """Set up observable gauges for process metrics."""
if resource is None:
return # Skip process metrics on Windows
def get_cpu_times(_options): def get_cpu_times(_options):
"""Get process CPU times.""" """Get process CPU times."""

View file

@ -43,7 +43,8 @@ dependencies = [
"litellm>=1.0.0,<=1.82.6", # 1.82.7+ contains a supply chain attack (malicious .pth credential stealer) "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 "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) "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 # Transitive dependency security fixes
"pyasn1>=0.6.3", # DoS vulnerability fix "pyasn1>=0.6.3", # DoS vulnerability fix
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass fix "urllib3>=2.6.3", # Decompression-bomb safeguards bypass fix