Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d8c5ce0c | ||
|
|
634c2adf9e | ||
|
|
eafb5bdfcd | ||
|
|
7dd68538bb | ||
|
|
0ca94e6500 | ||
|
|
f74d042224 | ||
|
|
526ed8fe76 | ||
|
|
59f55c95c0 | ||
|
|
3da353a550 |
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Pre-commit hook - runs all scripts in scripts/hooks/
|
||||
|
||||
set -e
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
HOOKS_DIR="$REPO_ROOT/scripts/hooks"
|
||||
|
||||
if [ ! -d "$HOOKS_DIR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Running pre-commit hooks ==="
|
||||
echo ""
|
||||
|
||||
# Run all executable scripts in hooks directory
|
||||
for hook in "$HOOKS_DIR"/*.sh; do
|
||||
if [ -x "$hook" ]; then
|
||||
echo "[hook] $(basename "$hook")"
|
||||
(cd "$REPO_ROOT" && "$hook")
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Pre-commit hooks completed ==="
|
||||
echo ""
|
||||
@@ -61,7 +61,7 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
|
||||
"""
|
||||
try:
|
||||
bank_id = get_current_bank_id()
|
||||
await memory.put_batch_async(
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[{"content": content, "context": context}]
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ ENV_PORT = "HINDSIGHT_API_PORT"
|
||||
ENV_LOG_LEVEL = "HINDSIGHT_API_LOG_LEVEL"
|
||||
ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
|
||||
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
|
||||
ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
|
||||
|
||||
# Default values
|
||||
DEFAULT_DATABASE_URL = "pg0"
|
||||
@@ -47,6 +48,7 @@ DEFAULT_PORT = 8888
|
||||
DEFAULT_LOG_LEVEL = "info"
|
||||
DEFAULT_MCP_ENABLED = True
|
||||
DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp"
|
||||
DEFAULT_MCP_LOCAL_BANK_ID = "mcp"
|
||||
|
||||
# Required embedding dimension for database schema
|
||||
EMBEDDING_DIMENSION = 384
|
||||
|
||||
@@ -282,3 +282,81 @@ If no genuine opinions are expressed (e.g., the response just says "I don't know
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract opinions: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
async def reflect(
|
||||
llm_config,
|
||||
query: str,
|
||||
experience_facts: List[str] = None,
|
||||
world_facts: List[str] = None,
|
||||
opinion_facts: List[str] = None,
|
||||
name: str = "Assistant",
|
||||
disposition: DispositionTraits = None,
|
||||
background: str = "",
|
||||
context: str = None,
|
||||
) -> str:
|
||||
"""
|
||||
Standalone reflect function for generating answers based on facts.
|
||||
|
||||
This is a static version of the reflect operation that can be called
|
||||
without a MemoryEngine instance, useful for testing.
|
||||
|
||||
Args:
|
||||
llm_config: LLM provider instance
|
||||
query: Question to answer
|
||||
experience_facts: List of experience/agent fact strings
|
||||
world_facts: List of world fact strings
|
||||
opinion_facts: List of opinion fact strings
|
||||
name: Name of the agent/persona
|
||||
disposition: Disposition traits (defaults to neutral)
|
||||
background: Background information
|
||||
context: Additional context for the prompt
|
||||
|
||||
Returns:
|
||||
Generated answer text
|
||||
"""
|
||||
# Default disposition if not provided
|
||||
if disposition is None:
|
||||
disposition = DispositionTraits(skepticism=3, literalism=3, empathy=3)
|
||||
|
||||
# Convert string lists to MemoryFact format for formatting
|
||||
def to_memory_facts(facts: List[str], fact_type: str) -> List[MemoryFact]:
|
||||
if not facts:
|
||||
return []
|
||||
return [MemoryFact(id=f"test-{i}", text=f, fact_type=fact_type) for i, f in enumerate(facts)]
|
||||
|
||||
agent_results = to_memory_facts(experience_facts or [], "experience")
|
||||
world_results = to_memory_facts(world_facts or [], "world")
|
||||
opinion_results = to_memory_facts(opinion_facts or [], "opinion")
|
||||
|
||||
# Format facts for prompt
|
||||
agent_facts_text = format_facts_for_prompt(agent_results)
|
||||
world_facts_text = format_facts_for_prompt(world_results)
|
||||
opinion_facts_text = format_facts_for_prompt(opinion_results)
|
||||
|
||||
# Build prompt
|
||||
prompt = build_think_prompt(
|
||||
agent_facts_text=agent_facts_text,
|
||||
world_facts_text=world_facts_text,
|
||||
opinion_facts_text=opinion_facts_text,
|
||||
query=query,
|
||||
name=name,
|
||||
disposition=disposition,
|
||||
background=background,
|
||||
context=context,
|
||||
)
|
||||
|
||||
system_message = get_system_message(disposition)
|
||||
|
||||
# Call LLM
|
||||
answer_text = await llm_config.call(
|
||||
messages=[
|
||||
{"role": "system", "content": system_message},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
scope="memory_think",
|
||||
temperature=0.9,
|
||||
max_completion_tokens=1000
|
||||
)
|
||||
|
||||
return answer_text.strip()
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Local MCP server for use with Claude Code (stdio transport).
|
||||
|
||||
This runs a fully local Hindsight instance with embedded PostgreSQL (pg0).
|
||||
No external database or server required.
|
||||
|
||||
Run with:
|
||||
hindsight-local-mcp
|
||||
|
||||
Or with uvx:
|
||||
uvx hindsight-api@latest hindsight-local-mcp
|
||||
|
||||
Configure in Claude Code's MCP settings:
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["hindsight-api@latest", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Environment variables:
|
||||
HINDSIGHT_API_LLM_API_KEY: Required. API key for LLM provider.
|
||||
HINDSIGHT_API_LLM_PROVIDER: Optional. LLM provider (default: "openai").
|
||||
HINDSIGHT_API_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
|
||||
HINDSIGHT_API_MCP_LOCAL_BANK_ID: Optional. Memory bank ID (default: "mcp").
|
||||
HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "info").
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from hindsight_api.config import (
|
||||
ENV_MCP_LOCAL_BANK_ID,
|
||||
DEFAULT_MCP_LOCAL_BANK_ID,
|
||||
)
|
||||
|
||||
# Configure logging - default to info
|
||||
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
|
||||
_log_level_map = {
|
||||
"critical": logging.CRITICAL,
|
||||
"error": logging.ERROR,
|
||||
"warning": logging.WARNING,
|
||||
"info": logging.INFO,
|
||||
"debug": logging.DEBUG,
|
||||
}
|
||||
logging.basicConfig(
|
||||
level=_log_level_map.get(_log_level_str, logging.WARNING),
|
||||
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||
stream=sys.stderr, # MCP uses stdout for protocol, logs go to stderr
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
|
||||
"""
|
||||
Create a stdio MCP server with retain/recall tools.
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID to use for all operations.
|
||||
memory: Optional MemoryEngine instance. If not provided, creates one with pg0.
|
||||
|
||||
Returns:
|
||||
Configured FastMCP server instance.
|
||||
"""
|
||||
# Import here to avoid slow startup if just checking --help
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
||||
|
||||
# Create memory engine with pg0 embedded database if not provided
|
||||
if memory is None:
|
||||
memory = MemoryEngine(db_url="pg0://hindsight-mcp")
|
||||
|
||||
mcp = FastMCP("hindsight")
|
||||
|
||||
@mcp.tool()
|
||||
async def retain(content: str, context: str = "general") -> dict:
|
||||
"""
|
||||
Store important information to long-term memory.
|
||||
|
||||
Use this tool PROACTIVELY whenever the user shares:
|
||||
- Personal facts, preferences, or interests
|
||||
- Important events or milestones
|
||||
- User history, experiences, or background
|
||||
- Decisions, opinions, or stated preferences
|
||||
- Goals, plans, or future intentions
|
||||
- Relationships or people mentioned
|
||||
- Work context, projects, or responsibilities
|
||||
|
||||
Args:
|
||||
content: The fact/memory to store (be specific and include relevant details)
|
||||
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
async def _retain():
|
||||
try:
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[{"content": content, "context": context}]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error storing memory: {e}", exc_info=True)
|
||||
|
||||
# Fire and forget - don't block on memory storage
|
||||
asyncio.create_task(_retain())
|
||||
return {"status": "accepted", "message": "Memory storage initiated"}
|
||||
|
||||
@mcp.tool()
|
||||
async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict:
|
||||
"""
|
||||
Search memories to provide personalized, context-aware responses.
|
||||
|
||||
Use this tool PROACTIVELY to:
|
||||
- Check user's preferences before making suggestions
|
||||
- Recall user's history to provide continuity
|
||||
- Remember user's goals and context
|
||||
- Personalize responses based on past interactions
|
||||
|
||||
Args:
|
||||
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
|
||||
max_tokens: Maximum tokens to return in results (default: 4096)
|
||||
budget: Search budget level - "low", "mid", or "high" (default: "low")
|
||||
"""
|
||||
try:
|
||||
# Map string budget to enum
|
||||
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
|
||||
budget_enum = budget_map.get(budget.lower(), Budget.LOW)
|
||||
|
||||
search_result = await memory.recall_async(
|
||||
bank_id=bank_id,
|
||||
query=query,
|
||||
fact_type=list(VALID_RECALL_FACT_TYPES),
|
||||
budget=budget_enum,
|
||||
max_tokens=max_tokens
|
||||
)
|
||||
|
||||
return search_result.model_dump()
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching: {e}", exc_info=True)
|
||||
return {"error": str(e), "results": []}
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
async def _initialize_and_run(bank_id: str):
|
||||
"""Initialize memory and run the MCP server."""
|
||||
from hindsight_api import MemoryEngine
|
||||
|
||||
# Create and initialize memory engine with pg0 embedded database
|
||||
print("Initializing memory engine...", file=sys.stderr)
|
||||
memory = MemoryEngine(db_url="pg0://hindsight-mcp")
|
||||
await memory.initialize()
|
||||
print("Memory engine initialized.", file=sys.stderr)
|
||||
|
||||
# Create and run the server
|
||||
mcp = create_local_mcp_server(bank_id, memory=memory)
|
||||
await mcp.run_stdio_async()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the stdio MCP server."""
|
||||
import asyncio
|
||||
from hindsight_api.config import get_config, ENV_LLM_API_KEY
|
||||
|
||||
# Check for required environment variables
|
||||
config = get_config()
|
||||
if not config.llm_api_key:
|
||||
print(f"Error: {ENV_LLM_API_KEY} environment variable is required", file=sys.stderr)
|
||||
print("Set it in your MCP configuration or shell environment", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get bank ID from environment, default to "mcp"
|
||||
bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID)
|
||||
|
||||
# Print startup message to stderr (stdout is reserved for MCP protocol)
|
||||
print(f"Hindsight MCP server starting (bank_id={bank_id})...", file=sys.stderr)
|
||||
|
||||
# Run the async initialization and server
|
||||
asyncio.run(_initialize_and_run(bank_id))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,7 +6,6 @@ from pg0 import Pg0
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PORT = 5555
|
||||
DEFAULT_USERNAME = "hindsight"
|
||||
DEFAULT_PASSWORD = "hindsight"
|
||||
DEFAULT_DATABASE = "hindsight"
|
||||
@@ -17,14 +16,14 @@ class EmbeddedPostgres:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: int = DEFAULT_PORT,
|
||||
port: Optional[int] = None,
|
||||
username: str = DEFAULT_USERNAME,
|
||||
password: str = DEFAULT_PASSWORD,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
name: str = "hindsight",
|
||||
**kwargs,
|
||||
):
|
||||
self.port = port
|
||||
self.port = port # None means pg0 will auto-assign
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.database = database
|
||||
@@ -33,18 +32,22 @@ class EmbeddedPostgres:
|
||||
|
||||
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,
|
||||
)
|
||||
kwargs = {
|
||||
"name": self.name,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"database": self.database,
|
||||
}
|
||||
# Only set port if explicitly specified
|
||||
if self.port is not None:
|
||||
kwargs["port"] = self.port
|
||||
self._pg0 = Pg0(**kwargs)
|
||||
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})...")
|
||||
port_info = f"port={self.port}" if self.port else "port=auto"
|
||||
logger.info(f"Starting embedded PostgreSQL (name={self.name}, {port_info})...")
|
||||
|
||||
pg0 = self._get_pg0()
|
||||
last_error = None
|
||||
@@ -53,9 +56,9 @@ class EmbeddedPostgres:
|
||||
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}"
|
||||
# Get URI from pg0 (includes auto-assigned port)
|
||||
uri = info.uri
|
||||
logger.info(f"PostgreSQL started: {uri}")
|
||||
return uri
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
@@ -91,9 +94,7 @@ class EmbeddedPostgres:
|
||||
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
|
||||
return info.uri
|
||||
|
||||
async def is_running(self) -> bool:
|
||||
"""Check if the PostgreSQL server is currently running."""
|
||||
|
||||
@@ -50,6 +50,7 @@ test = [
|
||||
|
||||
[project.scripts]
|
||||
hindsight-api = "hindsight_api.main:main"
|
||||
hindsight-local-mcp = "hindsight_api.mcp_local:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_api"]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""
|
||||
Test LLM provider with different models and providers.
|
||||
Test LLM provider with different models using actual memory operations.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
from hindsight_api.engine.llm_wrapper import LLMProvider
|
||||
from hindsight_api.engine.utils import extract_facts
|
||||
from hindsight_api.engine.search.think_utils import reflect
|
||||
|
||||
|
||||
# Model matrix: (provider, model)
|
||||
@@ -15,13 +18,14 @@ MODEL_MATRIX = [
|
||||
("openai", "gpt-5-mini"),
|
||||
("openai", "gpt-5-nano"),
|
||||
("openai", "gpt-5"),
|
||||
("openai", "gpt-5.2"),
|
||||
# Groq models
|
||||
("groq", "llama-3.3-70b-versatile"),
|
||||
("groq", "openai/gpt-oss-120b"),
|
||||
("groq", "openai/gpt-oss-20b"),
|
||||
# Gemini models
|
||||
("gemini", "gemini-2.5-flash"),
|
||||
("gemini", "gemini-2.5-flash-lite"),
|
||||
("gemini", "gemini-3-pro-preview"),
|
||||
]
|
||||
|
||||
|
||||
@@ -38,10 +42,10 @@ def get_api_key_for_provider(provider: str) -> str | None:
|
||||
|
||||
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_provider_call(provider: str, model: str):
|
||||
async def test_llm_provider_memory_operations(provider: str, model: str):
|
||||
"""
|
||||
Test LLM provider can make a basic call with different models.
|
||||
Skips if the required API key is not available.
|
||||
Test LLM provider with actual memory operations: fact extraction and reflect.
|
||||
All models must pass this test.
|
||||
"""
|
||||
api_key = get_api_key_for_provider(provider)
|
||||
if not api_key:
|
||||
@@ -54,74 +58,53 @@ async def test_llm_provider_call(provider: str, model: str):
|
||||
model=model,
|
||||
)
|
||||
|
||||
# Test basic call
|
||||
response = await llm.call(
|
||||
messages=[{"role": "user", "content": "Say 'hello' and nothing else."}],
|
||||
max_completion_tokens=50,
|
||||
temperature=0.1,
|
||||
# Test 1: Fact extraction (structured output)
|
||||
test_text = """
|
||||
User: I just got back from my trip to Paris last week. The Eiffel Tower was amazing!
|
||||
Assistant: That sounds wonderful! How long were you there?
|
||||
User: About 5 days. I also visited the Louvre and saw the Mona Lisa.
|
||||
"""
|
||||
event_date = datetime(2024, 12, 10)
|
||||
|
||||
facts, chunks = await extract_facts(
|
||||
text=test_text,
|
||||
event_date=event_date,
|
||||
context="Travel conversation",
|
||||
llm_config=llm,
|
||||
)
|
||||
|
||||
print(f"\n{provider}/{model} response: {response}")
|
||||
assert response is not None, f"{provider}/{model} returned None"
|
||||
print(f"\n{provider}/{model} - Fact extraction:")
|
||||
print(f" Extracted {len(facts)} facts from {len(chunks)} chunks")
|
||||
for fact in facts:
|
||||
print(f" - {fact.fact}")
|
||||
|
||||
assert facts is not None, f"{provider}/{model} fact extraction returned None"
|
||||
assert len(facts) > 0, f"{provider}/{model} should extract at least one fact"
|
||||
|
||||
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_provider_verify_connection(provider: str, model: str):
|
||||
"""
|
||||
Test LLM provider verify_connection method with different models.
|
||||
Skips if the required API key is not available.
|
||||
"""
|
||||
api_key = get_api_key_for_provider(provider)
|
||||
if not api_key:
|
||||
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||
# Verify facts have required fields
|
||||
for fact in facts:
|
||||
assert fact.fact, f"{provider}/{model} fact missing text"
|
||||
assert fact.fact_type in ["world", "experience", "opinion"], f"{provider}/{model} invalid fact_type: {fact.fact_type}"
|
||||
|
||||
llm = LLMProvider(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
base_url="",
|
||||
model=model,
|
||||
# Test 2: Reflect (actual reflect function)
|
||||
response = await reflect(
|
||||
llm_config=llm,
|
||||
query="What was the highlight of my Paris trip?",
|
||||
experience_facts=[
|
||||
"I visited Paris in December 2024",
|
||||
"I saw the Eiffel Tower and it was amazing",
|
||||
"I visited the Louvre and saw the Mona Lisa",
|
||||
"The trip lasted 5 days",
|
||||
],
|
||||
world_facts=[
|
||||
"The Eiffel Tower is a famous landmark in Paris",
|
||||
"The Mona Lisa is displayed at the Louvre museum",
|
||||
],
|
||||
name="Traveler",
|
||||
)
|
||||
|
||||
# Test verify_connection
|
||||
await llm.verify_connection()
|
||||
print(f"\n{provider}/{model} connection verified")
|
||||
print(f"\n{provider}/{model} - Reflect response:")
|
||||
print(f" {response[:200]}...")
|
||||
|
||||
|
||||
# Models that support large output (65000+ tokens)
|
||||
LARGE_OUTPUT_MODELS = [
|
||||
("openai", "gpt-5-mini"),
|
||||
("openai", "gpt-5-nano"),
|
||||
("openai", "gpt-5"),
|
||||
("gemini", "gemini-2.5-flash"),
|
||||
("gemini", "gemini-2.5-flash-lite"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider,model", LARGE_OUTPUT_MODELS)
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_provider_large_output(provider: str, model: str):
|
||||
"""
|
||||
Test LLM provider with large max_completion_tokens (65000).
|
||||
Only tests models that support large outputs.
|
||||
Skips if the required API key is not available.
|
||||
"""
|
||||
api_key = get_api_key_for_provider(provider)
|
||||
if not api_key:
|
||||
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||
|
||||
llm = LLMProvider(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
base_url="",
|
||||
model=model,
|
||||
)
|
||||
|
||||
# Test call with large max_completion_tokens
|
||||
response = await llm.call(
|
||||
messages=[{"role": "user", "content": "Say 'ok'"}],
|
||||
max_completion_tokens=65000,
|
||||
)
|
||||
|
||||
print(f"\n{provider}/{model} large output response: {response}")
|
||||
assert response is not None, f"{provider}/{model} returned None"
|
||||
assert response is not None, f"{provider}/{model} reflect returned None"
|
||||
assert len(response) > 10, f"{provider}/{model} reflect response too short"
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Test local MCP server."""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_memory():
|
||||
"""Create a mock MemoryEngine."""
|
||||
memory = MagicMock()
|
||||
memory._initialized = True
|
||||
memory.retain_batch_async = AsyncMock()
|
||||
memory.recall_async = AsyncMock(return_value=MagicMock(results=[]))
|
||||
return memory
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mcp_server_retain(mock_memory):
|
||||
"""Test that retain tool fires async and returns immediately."""
|
||||
from hindsight_api.mcp_local import create_local_mcp_server
|
||||
|
||||
bank_id = "test-bank"
|
||||
mcp_server = create_local_mcp_server(bank_id, memory=mock_memory)
|
||||
|
||||
# Get the tools
|
||||
tools = mcp_server._tool_manager._tools
|
||||
assert "retain" in tools
|
||||
|
||||
# Call retain
|
||||
retain_tool = tools["retain"]
|
||||
result = await retain_tool.fn(content="test content", context="test_context")
|
||||
|
||||
# Returns immediately with accepted status
|
||||
assert result["status"] == "accepted"
|
||||
|
||||
# Wait for background task to complete
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Verify the memory was called correctly
|
||||
mock_memory.retain_batch_async.assert_called_once()
|
||||
call_kwargs = mock_memory.retain_batch_async.call_args.kwargs
|
||||
assert call_kwargs["bank_id"] == "test-bank"
|
||||
assert call_kwargs["contents"] == [{"content": "test content", "context": "test_context"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mcp_server_recall(mock_memory):
|
||||
"""Test that recall tool calls memory.recall_async with correct params."""
|
||||
from hindsight_api.mcp_local import create_local_mcp_server
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
# Mock recall_async to return a proper pydantic model
|
||||
mock_result = MagicMock()
|
||||
mock_result.model_dump.return_value = {"results": []}
|
||||
mock_memory.recall_async = AsyncMock(return_value=mock_result)
|
||||
|
||||
bank_id = "test-bank"
|
||||
mcp_server = create_local_mcp_server(bank_id, memory=mock_memory)
|
||||
|
||||
# Get the tools
|
||||
tools = mcp_server._tool_manager._tools
|
||||
assert "recall" in tools
|
||||
|
||||
# Call recall with new params
|
||||
recall_tool = tools["recall"]
|
||||
result = await recall_tool.fn(query="test query", max_tokens=2048, budget="mid")
|
||||
|
||||
# Result is a dict
|
||||
assert isinstance(result, dict)
|
||||
|
||||
# Verify the memory was called correctly
|
||||
mock_memory.recall_async.assert_called_once()
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["bank_id"] == "test-bank"
|
||||
assert call_kwargs["query"] == "test query"
|
||||
assert call_kwargs["max_tokens"] == 2048
|
||||
assert call_kwargs["budget"] == Budget.MID
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mcp_server_retain_with_default_context(mock_memory):
|
||||
"""Test that retain uses default context when not provided."""
|
||||
from hindsight_api.mcp_local import create_local_mcp_server
|
||||
|
||||
bank_id = "test-bank"
|
||||
mcp_server = create_local_mcp_server(bank_id, memory=mock_memory)
|
||||
|
||||
tools = mcp_server._tool_manager._tools
|
||||
retain_tool = tools["retain"]
|
||||
|
||||
# Call retain without context
|
||||
await retain_tool.fn(content="test content")
|
||||
|
||||
# Wait for background task
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
call_kwargs = mock_memory.retain_batch_async.call_args.kwargs
|
||||
assert call_kwargs["contents"] == [{"content": "test content", "context": "general"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mcp_server_retain_error_handling(mock_memory):
|
||||
"""Test that retain errors are logged but don't affect response."""
|
||||
from hindsight_api.mcp_local import create_local_mcp_server
|
||||
|
||||
mock_memory.retain_batch_async = AsyncMock(side_effect=Exception("Test error"))
|
||||
|
||||
mcp_server = create_local_mcp_server("test-bank", memory=mock_memory)
|
||||
|
||||
tools = mcp_server._tool_manager._tools
|
||||
retain_tool = tools["retain"]
|
||||
|
||||
# Retain returns immediately with accepted status (fire and forget)
|
||||
result = await retain_tool.fn(content="test content")
|
||||
assert result["status"] == "accepted"
|
||||
|
||||
# Wait for background task to complete (and log error)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mcp_server_recall_error_handling(mock_memory):
|
||||
"""Test that recall handles errors gracefully."""
|
||||
from hindsight_api.mcp_local import create_local_mcp_server
|
||||
|
||||
mock_memory.recall_async = AsyncMock(side_effect=Exception("Test error"))
|
||||
|
||||
mcp_server = create_local_mcp_server("test-bank", memory=mock_memory)
|
||||
|
||||
tools = mcp_server._tool_manager._tools
|
||||
recall_tool = tools["recall"]
|
||||
|
||||
result = await recall_tool.fn(query="test query")
|
||||
|
||||
# Result is a dict with error
|
||||
assert isinstance(result, dict)
|
||||
assert "error" in result
|
||||
assert result["results"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_mcp_server_recall_with_defaults(mock_memory):
|
||||
"""Test that recall uses default max_tokens and budget."""
|
||||
from hindsight_api.mcp_local import create_local_mcp_server
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.model_dump.return_value = {"results": []}
|
||||
mock_memory.recall_async = AsyncMock(return_value=mock_result)
|
||||
|
||||
mcp_server = create_local_mcp_server("test-bank", memory=mock_memory)
|
||||
|
||||
tools = mcp_server._tool_manager._tools
|
||||
recall_tool = tools["recall"]
|
||||
|
||||
# Call with defaults
|
||||
await recall_tool.fn(query="test query")
|
||||
|
||||
call_kwargs = mock_memory.recall_async.call_args.kwargs
|
||||
assert call_kwargs["max_tokens"] == 4096
|
||||
assert call_kwargs["budget"] == Budget.LOW
|
||||
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
def mock_memory():
|
||||
"""Create a mock MemoryEngine."""
|
||||
memory = MagicMock()
|
||||
memory.put_batch_async = AsyncMock()
|
||||
memory.retain_batch_async = AsyncMock()
|
||||
memory.recall_async = AsyncMock(return_value=MagicMock(results=[]))
|
||||
return memory
|
||||
|
||||
@@ -52,8 +52,8 @@ async def test_mcp_tools_use_context_bank_id(mock_memory):
|
||||
assert "successfully" in result.lower()
|
||||
|
||||
# Verify the memory was called with the context bank_id
|
||||
mock_memory.put_batch_async.assert_called_once()
|
||||
call_kwargs = mock_memory.put_batch_async.call_args.kwargs
|
||||
mock_memory.retain_batch_async.assert_called_once()
|
||||
call_kwargs = mock_memory.retain_batch_async.call_args.kwargs
|
||||
assert call_kwargs["bank_id"] == "context-bank-id"
|
||||
finally:
|
||||
_current_bank_id.reset(token)
|
||||
|
||||
@@ -26,17 +26,18 @@ The following models have been tested and verified to work correctly with Hindsi
|
||||
|
||||
| Provider | Model |
|
||||
|----------|-------|
|
||||
| **OpenAI** | `gpt-5.2` |
|
||||
| **OpenAI** | `gpt-5` |
|
||||
| **OpenAI** | `gpt-5-mini` |
|
||||
| **OpenAI** | `gpt-5-nano` |
|
||||
| **OpenAI** | `gpt-4.1-mini` |
|
||||
| **OpenAI** | `gpt-4.1-nano` |
|
||||
| **OpenAI** | `gpt-4o-mini` |
|
||||
| **Gemini** | `gemini-3-pro-preview` |
|
||||
| **Gemini** | `gemini-2.5-flash` |
|
||||
| **Gemini** | `gemini-2.5-flash-lite` |
|
||||
| **Groq** | `openai/gpt-oss-120b` |
|
||||
| **Groq** | `openai/gpt-oss-20b` |
|
||||
| **Groq** | `llama-3.3-70b-versatile` |
|
||||
|
||||
### Using Other Models
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Local MCP Server
|
||||
|
||||
Hindsight provides a fully local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required.
|
||||
|
||||
This is ideal for:
|
||||
- **Personal use with Claude Code** — Give Claude long-term memory across conversations
|
||||
- **Development and testing** — Quick setup without infrastructure
|
||||
- **Privacy-focused setups** — All data stays on your machine
|
||||
|
||||
## Quick Start
|
||||
|
||||
### With uvx (recommended)
|
||||
|
||||
```bash
|
||||
uvx hindsight-api@latest hindsight-local-mcp
|
||||
```
|
||||
|
||||
### With pip
|
||||
|
||||
```bash
|
||||
pip install hindsight-api
|
||||
hindsight-local-mcp
|
||||
```
|
||||
|
||||
## Claude Code Configuration
|
||||
|
||||
Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["hindsight-api@latest", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Bank ID
|
||||
|
||||
By default, memories are stored in a bank called `mcp`. To use a different bank:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["hindsight-api@latest", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key",
|
||||
"HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All standard [Hindsight configuration variables](/developer/configuration) are supported.
|
||||
|
||||
### Local MCP Specific
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | No | `mcp` | Memory bank ID to use |
|
||||
|
||||
## Available Tools
|
||||
|
||||
### retain
|
||||
|
||||
Store information to long-term memory. This is a **fire-and-forget** operation — it returns immediately while processing happens in the background.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"content": "User's favorite color is blue",
|
||||
"context": "preferences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "accepted",
|
||||
"message": "Memory storage initiated"
|
||||
}
|
||||
```
|
||||
|
||||
### recall
|
||||
|
||||
Search memories to provide personalized responses.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_tokens` | integer | No | Maximum tokens to return (default: 4096) |
|
||||
| `budget` | string | No | Search depth: `low`, `mid`, or `high` (default: `low`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"query": "What are the user's color preferences?",
|
||||
"max_tokens": 2048,
|
||||
"budget": "mid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The local MCP server:
|
||||
|
||||
1. **Starts an embedded PostgreSQL** (pg0) on an automatically assigned port
|
||||
2. **Initializes the Hindsight memory engine** with local embeddings
|
||||
3. **Connects via stdio** to Claude Code using the MCP protocol
|
||||
|
||||
Data is persisted in the pg0 data directory (`~/.pg0/hindsight-mcp/`), so your memories survive restarts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "HINDSIGHT_API_LLM_API_KEY required"
|
||||
|
||||
Make sure you've set the API key in your MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "sk-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Slow startup
|
||||
|
||||
The first startup may take longer as it:
|
||||
- Downloads the embedding model (~100MB)
|
||||
- Initializes the PostgreSQL database
|
||||
|
||||
Subsequent starts are faster.
|
||||
|
||||
### Checking logs
|
||||
|
||||
Set `HINDSIGHT_API_LOG_LEVEL=debug` for verbose output:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"HINDSIGHT_API_LOG_LEVEL": "debug"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Logs are written to stderr and visible in Claude Code's MCP server output.
|
||||
@@ -152,6 +152,11 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Integrations',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/integrations/local-mcp',
|
||||
label: 'Local MCP Server',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/integrations/litellm',
|
||||
|
||||
@@ -509,32 +509,22 @@ article a:not(.button):not([class*="hash-link"]) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Links containing code - the code inherits the transparent text-fill from the link */
|
||||
article a code {
|
||||
-webkit-text-fill-color: #3396e8 !important;
|
||||
color: #3396e8 !important;
|
||||
}
|
||||
|
||||
article a:hover code {
|
||||
-webkit-text-fill-color: #0074d9 !important;
|
||||
color: #0074d9 !important;
|
||||
}
|
||||
|
||||
article a:not(.button):not([class*="hash-link"]):hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--hindsight-gradient-start);
|
||||
}
|
||||
|
||||
/* Links inside code blocks - use solid color instead of gradient */
|
||||
code a,
|
||||
pre a,
|
||||
article code a,
|
||||
article pre a {
|
||||
background: none !important;
|
||||
-webkit-background-clip: unset !important;
|
||||
-webkit-text-fill-color: var(--ifm-color-primary) !important;
|
||||
background-clip: unset !important;
|
||||
color: var(--ifm-color-primary) !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
code a:hover,
|
||||
pre a:hover,
|
||||
article code a:hover,
|
||||
article pre a:hover {
|
||||
color: var(--ifm-color-primary-dark) !important;
|
||||
-webkit-text-fill-color: var(--ifm-color-primary-dark) !important;
|
||||
}
|
||||
|
||||
/* Admonitions - gradient themed */
|
||||
.theme-admonition,
|
||||
[class*="admonition_"] {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> Agent Memory that Works Like Human Memory
|
||||
|
||||
This file contains the complete Hindsight documentation for LLM consumption.
|
||||
Generated: 2025-12-15T13:52:15.559Z
|
||||
Generated: 2025-12-16T09:56:12.435Z
|
||||
|
||||
---
|
||||
|
||||
@@ -2770,17 +2770,18 @@ The following models have been tested and verified to work correctly with Hindsi
|
||||
|
||||
| Provider | Model |
|
||||
|----------|-------|
|
||||
| **OpenAI** | `gpt-5.2` |
|
||||
| **OpenAI** | `gpt-5` |
|
||||
| **OpenAI** | `gpt-5-mini` |
|
||||
| **OpenAI** | `gpt-5-nano` |
|
||||
| **OpenAI** | `gpt-4.1-mini` |
|
||||
| **OpenAI** | `gpt-4.1-nano` |
|
||||
| **OpenAI** | `gpt-4o-mini` |
|
||||
| **Gemini** | `gemini-3-pro-preview` |
|
||||
| **Gemini** | `gemini-2.5-flash` |
|
||||
| **Gemini** | `gemini-2.5-flash-lite` |
|
||||
| **Groq** | `openai/gpt-oss-120b` |
|
||||
| **Groq** | `openai/gpt-oss-20b` |
|
||||
| **Groq** | `llama-3.3-70b-versatile` |
|
||||
|
||||
### Using Other Models
|
||||
|
||||
@@ -5209,4 +5210,179 @@ cleanup()
|
||||
- A running Hindsight API server
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## File: sdks/integrations/local-mcp.md
|
||||
|
||||
# Local MCP Server
|
||||
|
||||
Hindsight provides a fully local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required.
|
||||
|
||||
This is ideal for:
|
||||
- **Personal use with Claude Code** — Give Claude long-term memory across conversations
|
||||
- **Development and testing** — Quick setup without infrastructure
|
||||
- **Privacy-focused setups** — All data stays on your machine
|
||||
|
||||
## Quick Start
|
||||
|
||||
### With uvx (recommended)
|
||||
|
||||
```bash
|
||||
uvx hindsight-api@latest hindsight-local-mcp
|
||||
```
|
||||
|
||||
### With pip
|
||||
|
||||
```bash
|
||||
pip install hindsight-api
|
||||
hindsight-local-mcp
|
||||
```
|
||||
|
||||
## Claude Code Configuration
|
||||
|
||||
Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["hindsight-api@latest", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Bank ID
|
||||
|
||||
By default, memories are stored in a bank called `mcp`. To use a different bank:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["hindsight-api@latest", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "your-openai-key",
|
||||
"HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All standard [Hindsight configuration variables](/developer/configuration) are supported.
|
||||
|
||||
### Local MCP Specific
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | No | `mcp` | Memory bank ID to use |
|
||||
|
||||
## Available Tools
|
||||
|
||||
### retain
|
||||
|
||||
Store information to long-term memory. This is a **fire-and-forget** operation — it returns immediately while processing happens in the background.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"content": "User's favorite color is blue",
|
||||
"context": "preferences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "accepted",
|
||||
"message": "Memory storage initiated"
|
||||
}
|
||||
```
|
||||
|
||||
### recall
|
||||
|
||||
Search memories to provide personalized responses.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_tokens` | integer | No | Maximum tokens to return (default: 4096) |
|
||||
| `budget` | string | No | Search depth: `low`, `mid`, or `high` (default: `low`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"query": "What are the user's color preferences?",
|
||||
"max_tokens": 2048,
|
||||
"budget": "mid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The local MCP server:
|
||||
|
||||
1. **Starts an embedded PostgreSQL** (pg0) on an automatically assigned port
|
||||
2. **Initializes the Hindsight memory engine** with local embeddings
|
||||
3. **Connects via stdio** to Claude Code using the MCP protocol
|
||||
|
||||
Data is persisted in the pg0 data directory (`~/.pg0/hindsight-mcp/`), so your memories survive restarts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "HINDSIGHT_API_LLM_API_KEY required"
|
||||
|
||||
Make sure you've set the API key in your MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "sk-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Slow startup
|
||||
|
||||
The first startup may take longer as it:
|
||||
- Downloads the embedding model (~100MB)
|
||||
- Initializes the PostgreSQL database
|
||||
|
||||
Subsequent starts are faster.
|
||||
|
||||
### Checking logs
|
||||
|
||||
Set `HINDSIGHT_API_LOG_LEVEL=debug` for verbose output:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"HINDSIGHT_API_LOG_LEVEL": "debug"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Logs are written to stderr and visible in Claude Code's MCP server output.
|
||||
|
||||
|
||||
---
|
||||
|
||||
+4
-1
@@ -5,5 +5,8 @@
|
||||
"hindsight-clients/typescript",
|
||||
"hindsight-control-plane",
|
||||
"hindsight-docs"
|
||||
]
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "./scripts/setup-hooks.sh"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Regenerate llms-full.txt when docs change
|
||||
|
||||
LOG_PREFIX=" "
|
||||
|
||||
# Check if any docs files are staged
|
||||
DOCS_CHANGED=$(git diff --cached --name-only -- 'hindsight-docs/docs/**/*.md' 'hindsight-docs/docs/**/*.mdx' 2>/dev/null || true)
|
||||
|
||||
if [ -z "$DOCS_CHANGED" ]; then
|
||||
echo "${LOG_PREFIX}No docs changes, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${LOG_PREFIX}Docs changed, regenerating llms-full.txt..."
|
||||
|
||||
# Check if npm is available and hindsight-docs exists
|
||||
if [ ! -d "hindsight-docs" ] || ! command -v npm &> /dev/null; then
|
||||
echo "${LOG_PREFIX}Warning: Cannot regenerate llms-full.txt (missing hindsight-docs or npm)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd hindsight-docs
|
||||
|
||||
# Run the generate script
|
||||
if npm run generate-llms --silent 2>/dev/null; then
|
||||
# Check if llms-full.txt changed
|
||||
if [ -n "$(git diff --name-only -- static/llms-full.txt 2>/dev/null)" ]; then
|
||||
echo "${LOG_PREFIX}llms-full.txt updated, staging..."
|
||||
git add static/llms-full.txt
|
||||
else
|
||||
echo "${LOG_PREFIX}llms-full.txt unchanged"
|
||||
fi
|
||||
else
|
||||
echo "${LOG_PREFIX}Warning: Failed to regenerate llms-full.txt"
|
||||
fi
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Setup git hooks for the repository
|
||||
|
||||
set -e
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
echo "Setting up git hooks..."
|
||||
git config core.hooksPath "$REPO_ROOT/.githooks"
|
||||
echo "Git hooks configured to use .githooks directory"
|
||||
echo "Done!"
|
||||
Reference in New Issue
Block a user