* Added hindsight_liteLLM implementation * Add instructions for entity vs bank id * Add another line about entity * Address PR review comments and enhance litellm integration - Remove deprecated limit parameter from recall() and arecall() functions since Hindsight uses budget/max_tokens for result control - Remove dead MODEL_MAX_OUTPUT_TOKENS dict and max_output_tokens property from LLMProvider (superseded by hardcoded max_completion_tokens) - Add test-litellm-integration job to CI workflow - Add reflect API support with use_reflect config option - Add verbose mode debug info via get_last_injection_debug() - Add entity_id support for multi-user memory isolation - Add retain() and reflect() wrapper functions - Update docstrings and examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Make max_memories optional to allow unlimited memory injection - Change max_memories default from 10 to None (no limit) - When max_memories is None, all results from the API are used - Fix recall result handling to properly detect list vs object return - Update wrappers (OpenAI, Anthropic) with same optional behavior This allows users to control memory limits via max_memory_tokens and recall_budget without an artificial count limit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove entity_id from hindsight_litellm; add gpt-4o token cap Multi-user support now uses separate bank_ids per user instead of entity_id scoping (e.g., bank_id=f"user-{user_id}"). This simplifies the API and aligns with the Hindsight architecture. Also fixes max_completion_tokens error for gpt-4o models by capping the value at 16384 (gpt-4o's limit) instead of sending the default 65000 which exceeds the model's supported maximum. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix dark mode styling across Control Plane UI components Improvements to ensure proper text visibility and contrast in both light and dark modes: - Add global CSS rules for datetime-local calendar picker icon visibility using filter: invert() for both light (0.5) and dark (1) modes - Fix text colors in dialog components to use theme-aware foreground colors - Update memory detail panel, document/chunk modals, and data views to use proper dark mode text classes (text-foreground, text-card-foreground) - Fix form labels, headings, and content text in bank selector dialogs - Update entities view and documents view table styling for dark mode - Bump package versions to 0.1.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove session_id feature and add How It Works section to README - Remove session_id and session management (new_session, set_session, get_session) from config.py, callbacks.py, and __init__.py - Session management was a client-only abstraction not backed by core API - Add "How It Works" section to README with visual flow diagram - Update README to remove session management documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix readme example * Add dark mode again --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
import asyncio
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from pg0 import Pg0
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_PORT = 5555
|
|
DEFAULT_USERNAME = "hindsight"
|
|
DEFAULT_PASSWORD = "hindsight"
|
|
DEFAULT_DATABASE = "hindsight"
|
|
|
|
|
|
class EmbeddedPostgres:
|
|
"""Manages an embedded PostgreSQL server instance using pg0-embedded."""
|
|
|
|
def __init__(
|
|
self,
|
|
port: int = DEFAULT_PORT,
|
|
username: str = DEFAULT_USERNAME,
|
|
password: str = DEFAULT_PASSWORD,
|
|
database: str = DEFAULT_DATABASE,
|
|
name: str = "hindsight",
|
|
**kwargs,
|
|
):
|
|
self.port = port
|
|
self.username = username
|
|
self.password = password
|
|
self.database = database
|
|
self.name = name
|
|
self._pg0: Optional[Pg0] = None
|
|
|
|
def _get_pg0(self) -> Pg0:
|
|
if self._pg0 is None:
|
|
self._pg0 = Pg0(
|
|
name=self.name,
|
|
port=self.port,
|
|
username=self.username,
|
|
password=self.password,
|
|
database=self.database,
|
|
)
|
|
return self._pg0
|
|
|
|
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
|
|
"""Start the PostgreSQL server with retry logic."""
|
|
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
|
|
|
|
pg0 = self._get_pg0()
|
|
last_error = None
|
|
|
|
for attempt in range(1, max_retries + 1):
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
info = await loop.run_in_executor(None, pg0.start)
|
|
logger.info(f"PostgreSQL started on port {self.port}")
|
|
# Construct URI manually since pg0-embedded may return None
|
|
uri = info.uri if info and info.uri else f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
|
return uri
|
|
except Exception as e:
|
|
last_error = str(e)
|
|
if attempt < max_retries:
|
|
delay = retry_delay * (2 ** (attempt - 1))
|
|
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error}")
|
|
logger.debug(f"Retrying in {delay:.1f}s...")
|
|
await asyncio.sleep(delay)
|
|
else:
|
|
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error}")
|
|
|
|
raise RuntimeError(
|
|
f"Failed to start embedded PostgreSQL after {max_retries} attempts. "
|
|
f"Last error: {last_error}"
|
|
)
|
|
|
|
async def stop(self) -> None:
|
|
"""Stop the PostgreSQL server."""
|
|
pg0 = self._get_pg0()
|
|
logger.info(f"Stopping embedded PostgreSQL (name: {self.name})...")
|
|
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
await loop.run_in_executor(None, pg0.stop)
|
|
logger.info("Embedded PostgreSQL stopped")
|
|
except Exception as e:
|
|
if "not running" in str(e).lower():
|
|
return
|
|
raise RuntimeError(f"Failed to stop PostgreSQL: {e}")
|
|
|
|
async def get_uri(self) -> str:
|
|
"""Get the connection URI for the PostgreSQL server."""
|
|
pg0 = self._get_pg0()
|
|
loop = asyncio.get_event_loop()
|
|
info = await loop.run_in_executor(None, pg0.info)
|
|
# Construct URI manually since pg0-embedded may return None
|
|
uri = info.uri if info and info.uri else f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
|
return uri
|
|
|
|
async def is_running(self) -> bool:
|
|
"""Check if the PostgreSQL server is currently running."""
|
|
try:
|
|
pg0 = self._get_pg0()
|
|
loop = asyncio.get_event_loop()
|
|
info = await loop.run_in_executor(None, pg0.info)
|
|
return info is not None and info.running
|
|
except Exception:
|
|
return False
|
|
|
|
async def ensure_running(self) -> str:
|
|
"""Ensure the PostgreSQL server is running, starting it if needed."""
|
|
if await self.is_running():
|
|
return await self.get_uri()
|
|
return await self.start()
|
|
|
|
|
|
_default_instance: Optional[EmbeddedPostgres] = None
|
|
|
|
|
|
def get_embedded_postgres() -> EmbeddedPostgres:
|
|
"""Get or create the default EmbeddedPostgres instance."""
|
|
global _default_instance
|
|
if _default_instance is None:
|
|
_default_instance = EmbeddedPostgres()
|
|
return _default_instance
|
|
|
|
|
|
async def start_embedded_postgres() -> str:
|
|
"""Quick start function for embedded PostgreSQL."""
|
|
return await get_embedded_postgres().ensure_running()
|
|
|
|
|
|
async def stop_embedded_postgres() -> None:
|
|
"""Stop the default embedded PostgreSQL instance."""
|
|
global _default_instance
|
|
if _default_instance:
|
|
await _default_instance.stop()
|