feat(litellm): async retain, reflect support, and API cleanup (#167)
* feat(litellm): async retain with sync option, fix client session cleanup - Add sync parameter to retain() for blocking vs background operation - Default to async retain (sync=False) for better performance - Add get_pending_retain_errors() to check async failures - Fix "Unclosed client session" warnings by properly closing clients - Fix "Timeout context manager" asyncio errors by creating fresh clients - Each API call now creates and closes its own client (aiohttp limitation) - Add _get_client() and _close_client() helpers for consistent handling - Update recall(), reflect(), _retain_sync() and _inject_memories() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(litellm): add reflect support and require explicit hindsight_query - Make hindsight_query required when inject_memories=True to enforce intentional memory queries (no automatic last-user-message fallback) - Add reflect_context parameter for shaping LLM reasoning in reflect - Add reflect_response_schema for structured JSON output from reflect - Add _reflect_sync() and _reflect_async() methods in callbacks - Update wrappers.py to support response_schema in reflect/areflect This improves the developer experience by making memory injection explicit and adds full reflect API support through the integration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(litellm): rename recall_budget to budget, add per-call reflect context - Rename `recall_budget` parameter to `budget` for consistency with API - Add `hindsight_reflect_context` kwarg for per-call reflect context override - Fix reflect() to not pass None values for optional parameters Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(litellm): update README for new API structure and features - Document configure() vs set_defaults() separation - Add hindsight_query requirement when inject_memories=True - Document async retain (sync=False default) and get_pending_retain_errors() - Add hindsight_reflect_context per-call override documentation - Document budget parameter (renamed from recall_budget) - Add reflect_context and reflect_response_schema options - Update all code examples to use new API structure - Add new functions to API Reference table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(litellm): update tests for new configure/set_defaults API - Update tests to use separate configure() and set_defaults() calls - Fix test assertions to check config vs defaults appropriately - Add tests for legacy parameter backwards compatibility - Add new TestSetDefaults test class - Fix _format_memories test call signature (settings, config order) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add set_bank_mission(), deprecate set_bank_background() - Add mission parameter to hindsight_client.create_bank() - Add set_bank_mission() function to hindsight_litellm - Deprecate set_bank_background() with DeprecationWarning - Update _create_or_update_bank() to support mission parameter - Update README and docstrings to document the new API The 'background' field has been deprecated in the Hindsight API in favor of 'mission' which is used for mental model generation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove deprecated background parameter and legacy configure() parameters - Remove set_bank_background() in favor of set_bank_mission() - Remove background parameter from _create_or_update_bank() - Remove background parameter from hindsight_client.create_bank() - Remove legacy parameters from configure() (bank_id, document_id, budget, etc.) - These have been replaced by the set_defaults() API - Remove legacy test cases for deprecated parameters Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: update tests and docs to use mission instead of background The create_bank() parameter was renamed from background to mission. Update all tests and doc examples to use the new parameter name. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
This commit is contained in:
parent
8e39cb7bc8
commit
1d4879a206
10 changed files with 1928 additions and 694 deletions
|
|
@ -327,7 +327,14 @@ class Hindsight:
|
|||
background: str | None = None,
|
||||
disposition: dict[str, float] | None = None,
|
||||
) -> BankProfileResponse:
|
||||
"""Create or update a memory bank."""
|
||||
"""Create or update a memory bank.
|
||||
|
||||
Args:
|
||||
bank_id: Unique identifier for the bank
|
||||
name: Human-readable display name
|
||||
mission: Instructions guiding what Hindsight should learn and remember (for mental models)
|
||||
disposition: Optional disposition traits (skepticism, literalism, empathy)
|
||||
"""
|
||||
from hindsight_client_api.models import create_bank_request, disposition_traits
|
||||
|
||||
disposition_obj = None
|
||||
|
|
@ -336,7 +343,7 @@ class Hindsight:
|
|||
|
||||
request_obj = create_bank_request.CreateBankRequest(
|
||||
name=name,
|
||||
background=background,
|
||||
mission=mission,
|
||||
disposition=disposition_obj,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ class TestReflect:
|
|||
"""Setup: Store some test memories and bank background."""
|
||||
client.create_bank(
|
||||
bank_id=bank_id,
|
||||
background="I am a helpful AI assistant interested in technology and science.",
|
||||
mission="I am a helpful AI assistant interested in technology and science.",
|
||||
)
|
||||
|
||||
client.retain_batch(
|
||||
|
|
@ -261,7 +261,7 @@ class TestEndToEndWorkflow:
|
|||
# 1. Create bank
|
||||
client.create_bank(
|
||||
bank_id=workflow_bank_id,
|
||||
background="I am a software engineer who loves Python programming.",
|
||||
mission="I am a software engineer who loves Python programming.",
|
||||
)
|
||||
|
||||
# 2. Store memories
|
||||
|
|
@ -666,7 +666,7 @@ class TestDeleteBank:
|
|||
# Create bank with some data
|
||||
client.create_bank(
|
||||
bank_id=bank_id,
|
||||
background="This bank will be deleted",
|
||||
mission="This bank will be deleted",
|
||||
)
|
||||
client.retain(
|
||||
bank_id=bank_id,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ client = Hindsight(base_url=HINDSIGHT_URL)
|
|||
client.create_bank(
|
||||
bank_id="my-bank",
|
||||
name="Research Assistant",
|
||||
background="I am a research assistant specializing in machine learning",
|
||||
mission="I am a research assistant specializing in machine learning",
|
||||
disposition={
|
||||
"skepticism": 4,
|
||||
"literalism": 3,
|
||||
|
|
@ -33,15 +33,15 @@ client.create_bank(
|
|||
# [/docs:create-bank]
|
||||
|
||||
|
||||
# [docs:bank-background]
|
||||
# [docs:bank-mission]
|
||||
client.create_bank(
|
||||
bank_id="financial-advisor",
|
||||
name="Financial Advisor",
|
||||
background="""I am a conservative financial advisor with 20 years of experience.
|
||||
mission="""I am a conservative financial advisor with 20 years of experience.
|
||||
I prioritize capital preservation over aggressive growth.
|
||||
I have seen multiple market crashes and believe in diversification."""
|
||||
)
|
||||
# [/docs:bank-background]
|
||||
# [/docs:bank-mission]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ response = client.reflect(
|
|||
client.create_bank(
|
||||
bank_id="cautious-advisor",
|
||||
name="Cautious Advisor",
|
||||
background="I am a risk-aware financial advisor",
|
||||
mission="I am a risk-aware financial advisor",
|
||||
disposition={
|
||||
"skepticism": 5, # Very skeptical of claims
|
||||
"literalism": 4, # Focuses on exact requirements
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ Universal LLM memory integration via LiteLLM. Add persistent memory to any LLM a
|
|||
## Features
|
||||
|
||||
- **Universal LLM Support** - Works with 100+ LLM providers via LiteLLM (OpenAI, Anthropic, Groq, Azure, AWS Bedrock, Google Vertex AI, and more)
|
||||
- **Simple Integration** - Just configure, enable, and use `hindsight_litellm.completion()`
|
||||
- **Simple Integration** - Just configure, set defaults, enable, and use `hindsight_litellm.completion()`
|
||||
- **Automatic Memory Injection** - Relevant memories are injected into prompts before LLM calls
|
||||
- **Automatic Conversation Storage** - Conversations are stored to Hindsight for future recall
|
||||
- **Automatic Conversation Storage** - Conversations are stored to Hindsight for future recall (async by default for performance)
|
||||
- **Two Memory Modes** - Choose between `reflect` (synthesized context) or `recall` (raw memory retrieval)
|
||||
- **Direct Memory APIs** - Query, synthesize, and store memories manually
|
||||
- **Native Client Wrappers** - Alternative wrappers for OpenAI and Anthropic SDKs
|
||||
- **Debug Mode** - Inspect exactly what memories are being injected
|
||||
- **Async Error Tracking** - Check for background operation failures with `get_pending_retain_errors()`
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -24,20 +25,31 @@ pip install hindsight-litellm
|
|||
```python
|
||||
import hindsight_litellm
|
||||
|
||||
# Configure and enable memory integration
|
||||
# Step 1: Configure static settings
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="my-agent",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Step 2: Set defaults (bank_id is required)
|
||||
hindsight_litellm.set_defaults(
|
||||
bank_id="my-agent",
|
||||
use_reflect=True, # Use reflect for synthesized context
|
||||
)
|
||||
|
||||
# Step 3: Enable memory integration
|
||||
hindsight_litellm.enable()
|
||||
|
||||
# Use the convenience wrapper - memory is automatically injected and stored
|
||||
# Step 4: Use with explicit hindsight_query (required when inject_memories=True)
|
||||
response = hindsight_litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}],
|
||||
hindsight_query="What do I know about AI discussions?", # Required!
|
||||
)
|
||||
```
|
||||
|
||||
**Important:** When `inject_memories=True` (default), you must provide `hindsight_query` to specify what to search for in memory. This ensures intentional, focused memory queries.
|
||||
|
||||
## How It Works
|
||||
|
||||
Here's what happens under the hood when you call `completion()`:
|
||||
|
|
@ -121,56 +133,89 @@ The memory injection and storage happen automatically - you just use `completion
|
|||
|
||||
## Configuration Options
|
||||
|
||||
The API is split into two functions for clarity:
|
||||
|
||||
### 1. `configure()` - Static Settings
|
||||
|
||||
Settings that typically don't change during a session:
|
||||
|
||||
```python
|
||||
hindsight_litellm.configure(
|
||||
# Required
|
||||
hindsight_api_url="http://localhost:8888", # Hindsight API server URL
|
||||
bank_id="my-agent", # Memory bank ID
|
||||
|
||||
api_key="your-api-key", # Optional API key for authentication
|
||||
# Optional - Authentication
|
||||
api_key="your-api-key", # API key for Hindsight authentication
|
||||
|
||||
# Optional - Memory behavior
|
||||
store_conversations=True, # Store conversations after LLM calls
|
||||
inject_memories=True, # Inject relevant memories into prompts
|
||||
use_reflect=False, # Use reflect API (synthesized) vs recall (raw memories)
|
||||
reflect_include_facts=False, # Include source facts with reflect responses
|
||||
max_memories=None, # Maximum memories to inject (None = unlimited)
|
||||
max_memory_tokens=4096, # Maximum tokens for memory context
|
||||
recall_budget="mid", # Recall budget: "low", "mid", "high"
|
||||
fact_types=["world", "agent"], # Filter fact types to inject
|
||||
|
||||
# Optional - Bank Configuration
|
||||
bank_name="My Agent", # Human-readable display name for the memory bank
|
||||
background="This agent...", # Instructions guiding what Hindsight should remember (see below)
|
||||
sync_storage=False, # False = async storage (default, better performance)
|
||||
# True = sync storage (blocks, raises errors immediately)
|
||||
|
||||
# Optional - Advanced
|
||||
injection_mode="system_message", # or "prepend_user"
|
||||
excluded_models=["gpt-3.5*"], # Exclude certain models
|
||||
injection_mode="system_message", # How to inject: "system_message" or "prepend_user"
|
||||
excluded_models=["gpt-3.5*"], # Exclude certain models from interception
|
||||
verbose=True, # Enable verbose logging and debug info
|
||||
)
|
||||
```
|
||||
|
||||
### Bank Configuration: background and bank_name
|
||||
### 2. `set_defaults()` - Per-Call Defaults
|
||||
|
||||
The `background` and `bank_name` parameters configure the memory bank itself. When provided, `configure()` will automatically create or update the bank with these settings.
|
||||
|
||||
- **bank_name**: A human-readable display name for the memory bank. Useful for identifying banks in the Hindsight UI or when managing multiple banks.
|
||||
|
||||
- **background**: Instructions that guide Hindsight on what information is important to extract and remember from conversations. This influences memory extraction during the `retain` operation and can affect how the bank's "disposition" (skepticism, literalism, empathy) is calibrated.
|
||||
Default values for per-call settings. These can be overridden on individual calls using `hindsight_*` kwargs:
|
||||
|
||||
```python
|
||||
# Example: Customer support routing agent
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="support-router",
|
||||
bank_name="Customer Support Router",
|
||||
background="""This agent routes customer support requests to the appropriate team.
|
||||
Remember which types of issues should go to which teams (billing, technical, sales).
|
||||
Track customer preferences for communication channels and past issue resolutions.
|
||||
Note any escalation patterns or VIP customers who need special handling.""",
|
||||
hindsight_litellm.set_defaults(
|
||||
# Required
|
||||
bank_id="my-agent", # Memory bank ID
|
||||
|
||||
# Optional - Memory retrieval
|
||||
budget="mid", # Budget level: "low", "mid", "high"
|
||||
fact_types=["world", "opinion"], # Filter fact types to retrieve
|
||||
max_memories=10, # Maximum memories to inject (None = unlimited)
|
||||
max_memory_tokens=4096, # Maximum tokens for memory context
|
||||
include_entities=True, # Include entity observations in recall
|
||||
|
||||
# Optional - Reflect mode
|
||||
use_reflect=True, # Use reflect API (synthesized) vs recall (raw memories)
|
||||
reflect_include_facts=False, # Include source facts in debug info
|
||||
reflect_context="I am a delivery agent finding recipients.", # Context for reflect reasoning
|
||||
reflect_response_schema={...}, # JSON Schema for structured reflect output
|
||||
|
||||
# Optional - Debugging
|
||||
trace=False, # Enable trace info for debugging
|
||||
document_id="conversation-1", # Document ID for grouping conversations
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Per-Call Overrides
|
||||
|
||||
Override any default on individual calls using `hindsight_*` kwargs:
|
||||
|
||||
```python
|
||||
response = hindsight_litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[...],
|
||||
hindsight_query="Where is Alice located?", # REQUIRED when inject_memories=True
|
||||
hindsight_reflect_context="Currently on floor 3", # Per-call reflect context override
|
||||
# hindsight_bank_id="other-bank", # Override bank_id for this call
|
||||
)
|
||||
```
|
||||
|
||||
### Bank Configuration: mission
|
||||
|
||||
Use `set_bank_mission()` to configure what the memory bank should learn and remember (used for mental models):
|
||||
|
||||
```python
|
||||
hindsight_litellm.set_bank_mission(
|
||||
mission="""This agent routes customer support requests to the appropriate team.
|
||||
Remember which types of issues should go to which teams (billing, technical, sales).
|
||||
Track customer preferences for communication channels and past issue resolutions.""",
|
||||
name="Customer Support Router", # Optional display name
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
### Memory Modes: Reflect vs Recall
|
||||
|
||||
- **Recall mode** (`use_reflect=False`, default): Retrieves raw memory facts and injects them as a numbered list. Best when you need precise, individual memories.
|
||||
|
|
@ -178,18 +223,19 @@ hindsight_litellm.configure(
|
|||
|
||||
```python
|
||||
# Recall mode - raw memories
|
||||
hindsight_litellm.configure(
|
||||
bank_id="my-agent",
|
||||
use_reflect=False, # Default
|
||||
)
|
||||
hindsight_litellm.set_defaults(bank_id="my-agent", use_reflect=False)
|
||||
# Injects: "1. [WORLD] User prefers Python\n2. [OPINION] User dislikes Java..."
|
||||
|
||||
# Reflect mode - synthesized context
|
||||
hindsight_litellm.configure(
|
||||
hindsight_litellm.set_defaults(bank_id="my-agent", use_reflect=True)
|
||||
# Injects: "Based on previous conversations, the user is a Python developer who..."
|
||||
|
||||
# Reflect with context - shapes LLM reasoning (not retrieval)
|
||||
hindsight_litellm.set_defaults(
|
||||
bank_id="my-agent",
|
||||
use_reflect=True,
|
||||
reflect_context="I am a delivery agent looking for package recipients.",
|
||||
)
|
||||
# Injects: "Based on previous conversations, the user is a Python developer who..."
|
||||
```
|
||||
|
||||
## Multi-Provider Support
|
||||
|
|
@ -199,29 +245,29 @@ Works with any LiteLLM-supported provider:
|
|||
```python
|
||||
import hindsight_litellm
|
||||
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="my-agent",
|
||||
)
|
||||
hindsight_litellm.configure(hindsight_api_url="http://localhost:8888")
|
||||
hindsight_litellm.set_defaults(bank_id="my-agent")
|
||||
hindsight_litellm.enable()
|
||||
|
||||
messages = [{"role": "user", "content": "Hello!"}]
|
||||
|
||||
# OpenAI
|
||||
hindsight_litellm.completion(model="gpt-4o", messages=[...])
|
||||
hindsight_litellm.completion(model="gpt-4o", messages=messages, hindsight_query="greeting")
|
||||
|
||||
# Anthropic
|
||||
hindsight_litellm.completion(model="claude-3-5-sonnet-20241022", messages=[...])
|
||||
hindsight_litellm.completion(model="claude-3-5-sonnet-20241022", messages=messages, hindsight_query="greeting")
|
||||
|
||||
# Groq
|
||||
hindsight_litellm.completion(model="groq/llama-3.1-70b-versatile", messages=[...])
|
||||
hindsight_litellm.completion(model="groq/llama-3.1-70b-versatile", messages=messages, hindsight_query="greeting")
|
||||
|
||||
# Azure OpenAI
|
||||
hindsight_litellm.completion(model="azure/gpt-4", messages=[...])
|
||||
hindsight_litellm.completion(model="azure/gpt-4", messages=messages, hindsight_query="greeting")
|
||||
|
||||
# AWS Bedrock
|
||||
hindsight_litellm.completion(model="bedrock/anthropic.claude-3", messages=[...])
|
||||
hindsight_litellm.completion(model="bedrock/anthropic.claude-3", messages=messages, hindsight_query="greeting")
|
||||
|
||||
# Google Vertex AI
|
||||
hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=[...])
|
||||
hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=messages, hindsight_query="greeting")
|
||||
```
|
||||
|
||||
## Direct Memory APIs
|
||||
|
|
@ -229,9 +275,10 @@ hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=[...])
|
|||
### Recall - Query raw memories
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, recall
|
||||
from hindsight_litellm import configure, set_defaults, recall
|
||||
|
||||
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="my-agent")
|
||||
|
||||
# Query memories
|
||||
memories = recall("what projects am I working on?", budget="mid")
|
||||
|
|
@ -246,9 +293,10 @@ for m in memories:
|
|||
### Reflect - Get synthesized context
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, reflect
|
||||
from hindsight_litellm import configure, set_defaults, reflect
|
||||
|
||||
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="my-agent")
|
||||
|
||||
# Get synthesized memory context
|
||||
result = reflect("what do you know about the user's preferences?")
|
||||
|
|
@ -256,21 +304,42 @@ print(result.text)
|
|||
|
||||
# Output:
|
||||
# "Based on our conversations, the user prefers Python for backend development..."
|
||||
|
||||
# With context to shape the response (doesn't affect retrieval)
|
||||
result = reflect(
|
||||
query="what do I know about Alice?",
|
||||
context="I am a delivery agent looking for package recipients.",
|
||||
)
|
||||
```
|
||||
|
||||
### Retain - Store memories
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, retain
|
||||
from hindsight_litellm import configure, set_defaults, retain, get_pending_retain_errors
|
||||
|
||||
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="my-agent")
|
||||
|
||||
# Store a memory
|
||||
# Async retain (default) - fast, non-blocking
|
||||
# Returns immediately; actual storage happens in background
|
||||
result = retain(
|
||||
content="User mentioned they're working on a machine learning project",
|
||||
context="Discussion about current projects",
|
||||
)
|
||||
print(f"Retained successfully: {result.success}, items: {result.items_count}")
|
||||
# result.success is True immediately (actual errors collected separately)
|
||||
|
||||
# Sync retain - blocks until complete, raises errors immediately
|
||||
result = retain(
|
||||
content="Critical information that must be stored",
|
||||
context="Important data",
|
||||
sync=True, # Block until storage completes
|
||||
)
|
||||
|
||||
# Check for async retain errors (call periodically)
|
||||
errors = get_pending_retain_errors()
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"Background retain failed: {e}")
|
||||
```
|
||||
|
||||
### Async APIs
|
||||
|
|
@ -332,19 +401,16 @@ response = wrapped.messages.create(
|
|||
When `verbose=True`, you can inspect exactly what memories are being injected:
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, enable, completion, get_last_injection_debug
|
||||
from hindsight_litellm import configure, set_defaults, enable, completion, get_last_injection_debug
|
||||
|
||||
configure(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
verbose=True,
|
||||
use_reflect=True,
|
||||
)
|
||||
configure(hindsight_api_url="http://localhost:8888", verbose=True)
|
||||
set_defaults(bank_id="my-agent", use_reflect=True)
|
||||
enable()
|
||||
|
||||
response = completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "What's my favorite color?"}]
|
||||
messages=[{"role": "user", "content": "What's my favorite color?"}],
|
||||
hindsight_query="What is the user's favorite color?",
|
||||
)
|
||||
|
||||
# Inspect what was injected
|
||||
|
|
@ -365,7 +431,11 @@ from hindsight_litellm import hindsight_memory
|
|||
import litellm
|
||||
|
||||
with hindsight_memory(bank_id="user-123"):
|
||||
response = litellm.completion(model="gpt-4", messages=[...])
|
||||
response = litellm.completion(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Hello!"}],
|
||||
hindsight_query="greeting context",
|
||||
)
|
||||
# Memory integration automatically disabled after context
|
||||
```
|
||||
|
||||
|
|
@ -387,7 +457,8 @@ cleanup()
|
|||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `configure(...)` | Configure global Hindsight settings |
|
||||
| `configure(...)` | Configure static Hindsight settings (API URL, auth, storage options) |
|
||||
| `set_defaults(...)` | Set defaults for per-call settings (bank_id, budget, reflect options) |
|
||||
| `enable()` | Enable memory integration with LiteLLM |
|
||||
| `disable()` | Disable memory integration |
|
||||
| `is_enabled()` | Check if memory integration is enabled |
|
||||
|
|
@ -397,20 +468,30 @@ cleanup()
|
|||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `get_config()` | Get current configuration |
|
||||
| `is_configured()` | Check if Hindsight is configured |
|
||||
| `reset_config()` | Reset configuration to defaults |
|
||||
| `get_config()` | Get current static configuration |
|
||||
| `get_defaults()` | Get current per-call defaults |
|
||||
| `is_configured()` | Check if Hindsight is configured with a bank_id |
|
||||
| `reset_config()` | Reset all configuration to defaults |
|
||||
| `set_document_id(id)` | Convenience function to update document_id |
|
||||
| `set_bank_mission(...)` | Set mission/instructions for a memory bank (for mental models) |
|
||||
|
||||
### Memory Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `recall(query, ...)` | Synchronously query raw memories |
|
||||
| `arecall(query, ...)` | Asynchronously query raw memories |
|
||||
| `reflect(query, ...)` | Synchronously get synthesized memory context |
|
||||
| `areflect(query, ...)` | Asynchronously get synthesized memory context |
|
||||
| `retain(content, ...)` | Synchronously store a memory |
|
||||
| `aretain(content, ...)` | Asynchronously store a memory |
|
||||
| `recall(query, ...)` | Query raw memories (sync) |
|
||||
| `arecall(query, ...)` | Query raw memories (async) |
|
||||
| `reflect(query, ...)` | Get synthesized memory context (sync) |
|
||||
| `areflect(query, ...)` | Get synthesized memory context (async) |
|
||||
| `retain(content, sync=False, ...)` | Store a memory (async by default, use `sync=True` to block) |
|
||||
| `aretain(content, ...)` | Store a memory (async) |
|
||||
|
||||
### Error Tracking Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `get_pending_retain_errors()` | Get and clear errors from background retain operations |
|
||||
| `get_pending_storage_errors()` | Get and clear errors from background conversation storage |
|
||||
|
||||
### Debug Functions
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -19,7 +19,14 @@ import concurrent.futures
|
|||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
from .config import get_config, is_configured, HindsightConfig, MemoryInjectionMode
|
||||
from .config import (
|
||||
get_config,
|
||||
get_defaults,
|
||||
is_configured,
|
||||
HindsightConfig,
|
||||
HindsightDefaults,
|
||||
MemoryInjectionMode,
|
||||
)
|
||||
|
||||
# Use requests for sync HTTP calls to avoid async event loop issues
|
||||
try:
|
||||
|
|
@ -37,6 +44,16 @@ except ImportError:
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HindsightError(Exception):
|
||||
"""Exception raised when a Hindsight operation fails.
|
||||
|
||||
This is raised when inject_memories=True and recall fails,
|
||||
or when store_conversations=True and store fails.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Thread pool for running async operations in background
|
||||
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=4, thread_name_prefix="hindsight-")
|
||||
|
||||
|
|
@ -76,6 +93,43 @@ class HindsightCallback(CustomLogger):
|
|||
self._recent_hashes: Set[str] = set()
|
||||
self._max_hash_cache = 1000
|
||||
|
||||
def _get_effective_settings(self, kwargs: Dict[str, Any]) -> HindsightDefaults:
|
||||
"""Get effective per-call settings from kwargs with fallback to defaults.
|
||||
|
||||
Per-call kwargs (hindsight_*) override defaults. Supported kwargs:
|
||||
- hindsight_bank_id: Override bank_id
|
||||
- hindsight_document_id: Override document_id
|
||||
- hindsight_budget: Override budget
|
||||
- hindsight_fact_types: Override fact_types
|
||||
- hindsight_max_memories: Override max_memories
|
||||
- hindsight_max_memory_tokens: Override max_memory_tokens
|
||||
- hindsight_use_reflect: Override use_reflect
|
||||
- hindsight_reflect_include_facts: Override reflect_include_facts
|
||||
- hindsight_context: Override reflect_context
|
||||
- hindsight_response_schema: Override reflect_response_schema
|
||||
- hindsight_include_entities: Override include_entities
|
||||
- hindsight_trace: Override trace
|
||||
|
||||
Note: hindsight_query is handled separately in log_pre_api_call since it's
|
||||
always per-call (no sensible default for dynamic queries).
|
||||
"""
|
||||
defaults = get_defaults() or HindsightDefaults()
|
||||
|
||||
return HindsightDefaults(
|
||||
bank_id=kwargs.get("hindsight_bank_id", defaults.bank_id),
|
||||
document_id=kwargs.get("hindsight_document_id", defaults.document_id),
|
||||
budget=kwargs.get("hindsight_budget", defaults.budget),
|
||||
fact_types=kwargs.get("hindsight_fact_types", defaults.fact_types),
|
||||
max_memories=kwargs.get("hindsight_max_memories", defaults.max_memories),
|
||||
max_memory_tokens=kwargs.get("hindsight_max_memory_tokens", defaults.max_memory_tokens),
|
||||
use_reflect=kwargs.get("hindsight_use_reflect", defaults.use_reflect),
|
||||
reflect_include_facts=kwargs.get("hindsight_reflect_include_facts", defaults.reflect_include_facts),
|
||||
reflect_context=kwargs.get("hindsight_context", defaults.reflect_context),
|
||||
reflect_response_schema=kwargs.get("hindsight_response_schema", defaults.reflect_response_schema),
|
||||
include_entities=kwargs.get("hindsight_include_entities", defaults.include_entities),
|
||||
trace=kwargs.get("hindsight_trace", defaults.trace),
|
||||
)
|
||||
|
||||
def _get_http_session(self):
|
||||
"""Get or create a requests Session (thread-safe)."""
|
||||
if self._http_session is None:
|
||||
|
|
@ -92,14 +146,18 @@ class HindsightCallback(CustomLogger):
|
|||
)
|
||||
return self._http_session
|
||||
|
||||
def _http_post(self, url: str, json_data: dict, config: HindsightConfig) -> Optional[dict]:
|
||||
"""Make a synchronous HTTP POST request."""
|
||||
try:
|
||||
session = self._get_http_session()
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if config.api_key:
|
||||
headers["Authorization"] = f"Bearer {config.api_key}"
|
||||
def _http_post(self, url: str, json_data: dict, config: HindsightConfig) -> dict:
|
||||
"""Make a synchronous HTTP POST request.
|
||||
|
||||
Raises:
|
||||
HindsightError: If the request fails for any reason.
|
||||
"""
|
||||
session = self._get_http_session()
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if config.api_key:
|
||||
headers["Authorization"] = f"Bearer {config.api_key}"
|
||||
|
||||
try:
|
||||
if HAS_REQUESTS:
|
||||
response = session.post(url, json=json_data, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
|
@ -108,10 +166,14 @@ class HindsightCallback(CustomLogger):
|
|||
response = session.post(url, json=json_data, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
else:
|
||||
raise HindsightError("No HTTP client available (install requests or httpx)")
|
||||
except HindsightError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if config.verbose:
|
||||
logger.warning(f"HTTP POST failed: {e}")
|
||||
return None
|
||||
logger.error(f"HTTP POST failed: {e}")
|
||||
raise HindsightError(f"Hindsight API request failed: {e}") from e
|
||||
|
||||
def _should_skip_model(self, model: str, config: HindsightConfig) -> bool:
|
||||
"""Check if this model should be excluded from interception."""
|
||||
|
|
@ -138,6 +200,20 @@ class HindsightCallback(CustomLogger):
|
|||
return " ".join(text_parts)
|
||||
return None
|
||||
|
||||
def _messages_to_query(self, messages: List[Dict[str, Any]]) -> str:
|
||||
"""Concatenate all message contents into a single query string."""
|
||||
message_parts = []
|
||||
for msg in messages:
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, str) and content:
|
||||
message_parts.append(content)
|
||||
elif isinstance(content, list):
|
||||
# Handle structured content (e.g., vision messages)
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
message_parts.append(item.get("text", ""))
|
||||
return "\n".join(message_parts)
|
||||
|
||||
def _compute_conversation_hash(
|
||||
self,
|
||||
user_input: str,
|
||||
|
|
@ -163,6 +239,7 @@ class HindsightCallback(CustomLogger):
|
|||
def _format_memories(
|
||||
self,
|
||||
results: List[Any],
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
) -> str:
|
||||
"""Format memory recall results into a context string.
|
||||
|
|
@ -174,7 +251,7 @@ class HindsightCallback(CustomLogger):
|
|||
return ""
|
||||
|
||||
# Apply limit if set, otherwise use all results
|
||||
results_to_use = results[:config.max_memories] if config.max_memories else results
|
||||
results_to_use = results[:settings.max_memories] if settings.max_memories else results
|
||||
memory_lines = []
|
||||
for i, result in enumerate(results_to_use, 1):
|
||||
# Handle both RecallResult objects and dicts
|
||||
|
|
@ -248,194 +325,343 @@ class HindsightCallback(CustomLogger):
|
|||
|
||||
return updated_messages
|
||||
|
||||
def _get_bank_id(self, config: HindsightConfig) -> str:
|
||||
"""Get the bank_id for API calls."""
|
||||
return config.bank_id
|
||||
|
||||
def _recall_memories_sync(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Recall relevant memories from Hindsight (sync) using direct HTTP."""
|
||||
"""Recall relevant memories from Hindsight (sync) using direct HTTP.
|
||||
|
||||
Raises:
|
||||
HindsightError: If inject_memories=True and recall fails.
|
||||
"""
|
||||
bank_id = settings.bank_id
|
||||
if not bank_id:
|
||||
raise HindsightError(
|
||||
"No bank_id configured. Call set_defaults(bank_id=...) "
|
||||
"or pass hindsight_bank_id=... to the completion call."
|
||||
)
|
||||
|
||||
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories/recall"
|
||||
|
||||
request_data = {
|
||||
"query": query,
|
||||
"budget": settings.budget or "mid",
|
||||
"max_tokens": settings.max_memory_tokens or 4096,
|
||||
}
|
||||
if settings.fact_types:
|
||||
request_data["types"] = settings.fact_types
|
||||
|
||||
# Add trace parameter for debugging
|
||||
if settings.trace:
|
||||
request_data["trace"] = True
|
||||
|
||||
# Add include options for entity observations
|
||||
# include_entities=True -> include: {entities: {}}
|
||||
# include_entities=False -> include: {entities: null}
|
||||
if settings.include_entities:
|
||||
request_data["include"] = {"entities": {}}
|
||||
else:
|
||||
request_data["include"] = {"entities": None}
|
||||
|
||||
try:
|
||||
bank_id = self._get_bank_id(config)
|
||||
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories/recall"
|
||||
|
||||
request_data = {
|
||||
"query": query,
|
||||
"budget": config.recall_budget or "mid",
|
||||
"max_tokens": config.max_memory_tokens or 4096,
|
||||
}
|
||||
if config.fact_types:
|
||||
request_data["types"] = config.fact_types
|
||||
|
||||
response = self._http_post(url, request_data, config)
|
||||
if response and "results" in response:
|
||||
return response["results"]
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
except HindsightError as e:
|
||||
if config.verbose:
|
||||
logger.warning(f"Failed to recall memories: {e}")
|
||||
return []
|
||||
logger.error(f"Failed to recall memories: {e}")
|
||||
raise HindsightError(f"Memory recall failed: {e}") from e
|
||||
|
||||
async def _recall_memories_async(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
) -> List[Any]:
|
||||
"""Recall relevant memories from Hindsight (async).
|
||||
|
||||
Uses thread pool executor with sync HTTP to avoid event loop conflicts.
|
||||
|
||||
Raises:
|
||||
HindsightError: If inject_memories=True and recall fails.
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
results = await loop.run_in_executor(
|
||||
_executor,
|
||||
self._recall_memories_sync,
|
||||
query,
|
||||
config
|
||||
loop = asyncio.get_running_loop()
|
||||
results = await loop.run_in_executor(
|
||||
_executor,
|
||||
lambda: self._recall_memories_sync(query, settings, config)
|
||||
)
|
||||
|
||||
return results if isinstance(results, list) else []
|
||||
|
||||
def _reflect_sync(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
) -> Optional[str]:
|
||||
"""Generate a reflection response from Hindsight (sync) using direct HTTP.
|
||||
|
||||
Returns:
|
||||
The reflect response text, or None if no response.
|
||||
|
||||
Raises:
|
||||
HindsightError: If inject_memories=True and reflect fails.
|
||||
"""
|
||||
bank_id = settings.bank_id
|
||||
if not bank_id:
|
||||
raise HindsightError(
|
||||
"No bank_id configured. Call set_defaults(bank_id=...) "
|
||||
"or pass hindsight_bank_id=... to the completion call."
|
||||
)
|
||||
|
||||
return results if isinstance(results, list) else []
|
||||
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/reflect"
|
||||
|
||||
except Exception as e:
|
||||
request_data: Dict[str, Any] = {
|
||||
"query": query,
|
||||
"budget": settings.budget or "mid",
|
||||
"max_tokens": settings.max_memory_tokens or 4096,
|
||||
}
|
||||
|
||||
# Add context if provided (shapes reasoning but not retrieval)
|
||||
if settings.reflect_context:
|
||||
request_data["context"] = settings.reflect_context
|
||||
|
||||
# Add response_schema for structured output
|
||||
if settings.reflect_response_schema:
|
||||
request_data["response_schema"] = settings.reflect_response_schema
|
||||
|
||||
# Add include options for facts if requested
|
||||
if settings.reflect_include_facts:
|
||||
request_data["include"] = {"facts": {}}
|
||||
|
||||
try:
|
||||
response = self._http_post(url, request_data, config)
|
||||
if response:
|
||||
# Handle structured output if schema was provided
|
||||
if settings.reflect_response_schema and "structured_output" in response:
|
||||
# Return structured output as JSON string for injection
|
||||
import json
|
||||
return json.dumps(response["structured_output"], indent=2)
|
||||
# Otherwise return text response
|
||||
return response.get("text", "")
|
||||
return None
|
||||
except HindsightError as e:
|
||||
if config.verbose:
|
||||
logger.warning(f"Failed to recall memories: {e}")
|
||||
return []
|
||||
logger.error(f"Failed to reflect: {e}")
|
||||
raise HindsightError(f"Reflect failed: {e}") from e
|
||||
|
||||
async def _reflect_async(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
) -> Optional[str]:
|
||||
"""Generate a reflection response from Hindsight (async).
|
||||
|
||||
Uses thread pool executor with sync HTTP to avoid event loop conflicts.
|
||||
|
||||
Returns:
|
||||
The reflect response text, or None if no response.
|
||||
|
||||
Raises:
|
||||
HindsightError: If inject_memories=True and reflect fails.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
result = await loop.run_in_executor(
|
||||
_executor,
|
||||
lambda: self._reflect_sync(query, settings, config)
|
||||
)
|
||||
return result
|
||||
|
||||
def _store_conversation_sync(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response: ModelResponse,
|
||||
model: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig,
|
||||
) -> None:
|
||||
"""Store the conversation to Hindsight (sync) using direct HTTP.
|
||||
|
||||
By default, stores the full conversation history passed to the LLM.
|
||||
Each message is stored as a separate item, all linked by document_id.
|
||||
IMPORTANT: This intentionally sends the FULL conversation history each call,
|
||||
not just the new messages. This is required because Hindsight's retain API
|
||||
with document_id performs an UPSERT (replace), not an append.
|
||||
|
||||
Hindsight will process the document as a whole for memory extraction.
|
||||
If we only sent deltas (new messages), Hindsight would only have the latest
|
||||
fragment and lose all prior context. By sending the full conversation each
|
||||
time, Hindsight always has the complete context to extract meaningful facts.
|
||||
|
||||
Example with delta-only (WRONG):
|
||||
Call 1: "USER: deliver to Alex\\nASSISTANT_TOOL_CALLS: look_at_business"
|
||||
Call 2: "TOOL_RESULT: TechStart Labs\\nASSISTANT_TOOL_CALLS: go_up" # Lost context!
|
||||
|
||||
Example with full conversation (CORRECT):
|
||||
Call 1: "USER: deliver to Alex\\nASSISTANT_TOOL_CALLS: look_at_business"
|
||||
Call 2: "USER: deliver to Alex\\nASSISTANT_TOOL_CALLS: look_at_business\\n
|
||||
TOOL_RESULT: TechStart Labs\\nASSISTANT_TOOL_CALLS: go_up" # Full context!
|
||||
|
||||
Each upsert replaces the previous, so the final stored document contains
|
||||
the complete conversation for Hindsight to process.
|
||||
|
||||
Raises:
|
||||
HindsightError: If store_conversations=True and store fails.
|
||||
"""
|
||||
try:
|
||||
# Extract assistant response from the LLM response
|
||||
assistant_output = ""
|
||||
if response.choices and len(response.choices) > 0:
|
||||
choice = response.choices[0]
|
||||
if hasattr(choice, "message") and choice.message:
|
||||
assistant_output = choice.message.content or ""
|
||||
bank_id = settings.bank_id
|
||||
if not bank_id:
|
||||
raise HindsightError(
|
||||
"No bank_id configured. Call set_defaults(bank_id=...) "
|
||||
"or pass hindsight_bank_id=... to the completion call."
|
||||
)
|
||||
|
||||
if not assistant_output:
|
||||
return
|
||||
# Extract assistant response from the LLM response
|
||||
assistant_output = ""
|
||||
assistant_tool_calls = []
|
||||
if response.choices and len(response.choices) > 0:
|
||||
choice = response.choices[0]
|
||||
if hasattr(choice, "message") and choice.message:
|
||||
assistant_output = choice.message.content or ""
|
||||
# Also capture tool calls
|
||||
if hasattr(choice.message, "tool_calls") and choice.message.tool_calls:
|
||||
for tc in choice.message.tool_calls:
|
||||
if hasattr(tc, "function"):
|
||||
assistant_tool_calls.append(f"{tc.function.name}({tc.function.arguments})")
|
||||
|
||||
# Build conversation items - each message becomes a separate item
|
||||
# All linked by document_id for Hindsight to process together
|
||||
items = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "").upper()
|
||||
content = msg.get("content", "")
|
||||
# Skip if no content AND no tool calls - nothing to store
|
||||
if not assistant_output and not assistant_tool_calls:
|
||||
return
|
||||
|
||||
# Skip system messages - they're instructions, not conversation
|
||||
if role == "SYSTEM":
|
||||
continue
|
||||
# Build conversation items - each message becomes a separate item
|
||||
# All linked by document_id for Hindsight to process together
|
||||
items = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "").upper()
|
||||
content = msg.get("content", "")
|
||||
|
||||
# Skip if this looks like our injected memory context
|
||||
if isinstance(content, str) and content.startswith("# Relevant Memories"):
|
||||
continue
|
||||
# Skip system messages - they're instructions, not conversation
|
||||
if role == "SYSTEM":
|
||||
continue
|
||||
|
||||
# Handle structured content (e.g., vision messages)
|
||||
if isinstance(content, list):
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
content = " ".join(text_parts)
|
||||
# Skip if this looks like our injected memory context
|
||||
if isinstance(content, str) and content.startswith("# Relevant Memories"):
|
||||
continue
|
||||
|
||||
# Handle tool messages (results from tool calls)
|
||||
if role == "TOOL":
|
||||
tool_call_id = msg.get("tool_call_id", "")
|
||||
items.append(f"TOOL_RESULT: {content}")
|
||||
continue
|
||||
|
||||
# Handle assistant messages with tool calls
|
||||
tool_calls = msg.get("tool_calls", [])
|
||||
if tool_calls:
|
||||
tc_strs = []
|
||||
for tc in tool_calls:
|
||||
if hasattr(tc, "function"):
|
||||
tc_strs.append(f"{tc.function.name}({tc.function.arguments})")
|
||||
elif isinstance(tc, dict) and "function" in tc:
|
||||
func = tc["function"]
|
||||
tc_strs.append(f"{func.get('name', '')}({func.get('arguments', '')})")
|
||||
if tc_strs:
|
||||
items.append(f"ASSISTANT_TOOL_CALLS: {'; '.join(tc_strs)}")
|
||||
if content:
|
||||
# Map roles to clearer labels
|
||||
label = "USER" if role == "USER" else "ASSISTANT"
|
||||
items.append(f"{label}: {content}")
|
||||
items.append(f"ASSISTANT: {content}")
|
||||
continue
|
||||
|
||||
# Add the new assistant response
|
||||
# Handle structured content (e.g., vision messages)
|
||||
if isinstance(content, list):
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
content = " ".join(text_parts)
|
||||
|
||||
if content:
|
||||
# Map roles to clearer labels
|
||||
label = "USER" if role == "USER" else "ASSISTANT"
|
||||
items.append(f"{label}: {content}")
|
||||
|
||||
# Add the new assistant response (text or tool calls)
|
||||
if assistant_output:
|
||||
items.append(f"ASSISTANT: {assistant_output}")
|
||||
if assistant_tool_calls:
|
||||
items.append(f"ASSISTANT_TOOL_CALLS: {'; '.join(assistant_tool_calls)}")
|
||||
|
||||
if not items:
|
||||
return
|
||||
if not items:
|
||||
return
|
||||
|
||||
# Use last user message for deduplication hash
|
||||
user_input = self._extract_user_query(messages) or ""
|
||||
# Use last user message for deduplication hash
|
||||
user_input = self._extract_user_query(messages) or ""
|
||||
|
||||
# Deduplication check
|
||||
conv_hash = self._compute_conversation_hash(user_input, assistant_output)
|
||||
if self._is_duplicate(conv_hash):
|
||||
if config.verbose:
|
||||
logger.debug(f"Skipping duplicate conversation: {conv_hash}")
|
||||
return
|
||||
# Deduplication check - include tool calls if no text content
|
||||
dedup_output = assistant_output or ";".join(assistant_tool_calls)
|
||||
conv_hash = self._compute_conversation_hash(user_input, dedup_output)
|
||||
if self._is_duplicate(conv_hash):
|
||||
if config.verbose:
|
||||
logger.debug(f"Skipping duplicate conversation: {conv_hash}")
|
||||
return
|
||||
|
||||
# Build the full conversation as a single item for now
|
||||
# (Future: could store each message as separate item in same document)
|
||||
conversation_text = "\n\n".join(items)
|
||||
# Build the full conversation as a single item for now
|
||||
# (Future: could store each message as separate item in same document)
|
||||
conversation_text = "\n\n".join(items)
|
||||
|
||||
# Build metadata
|
||||
metadata = {
|
||||
"source": "litellm",
|
||||
"model": model,
|
||||
}
|
||||
# Build metadata
|
||||
metadata = {
|
||||
"source": "litellm",
|
||||
"model": model,
|
||||
}
|
||||
|
||||
# Add token usage if available
|
||||
if hasattr(response, "usage") and response.usage:
|
||||
if hasattr(response.usage, "total_tokens"):
|
||||
metadata["tokens"] = str(response.usage.total_tokens)
|
||||
# Add token usage if available
|
||||
if hasattr(response, "usage") and response.usage:
|
||||
if hasattr(response.usage, "total_tokens"):
|
||||
metadata["tokens"] = str(response.usage.total_tokens)
|
||||
|
||||
bank_id = self._get_bank_id(config)
|
||||
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories"
|
||||
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories"
|
||||
|
||||
request_data = {
|
||||
"items": [
|
||||
{
|
||||
"content": conversation_text,
|
||||
"context": f"conversation:litellm:{model}",
|
||||
"metadata": metadata,
|
||||
"document_id": config.document_id, # Group by document
|
||||
}
|
||||
],
|
||||
}
|
||||
request_data = {
|
||||
"items": [
|
||||
{
|
||||
"content": conversation_text,
|
||||
"context": f"conversation:litellm:{model}",
|
||||
"metadata": metadata,
|
||||
"document_id": settings.document_id, # Group by document
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
try:
|
||||
self._http_post(url, request_data, config)
|
||||
|
||||
if config.verbose:
|
||||
logger.info(f"Stored conversation to Hindsight bank: {config.bank_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Stored conversation to Hindsight bank: {bank_id}")
|
||||
except HindsightError as e:
|
||||
if config.verbose:
|
||||
logger.warning(f"Failed to store conversation: {e}")
|
||||
logger.error(f"Failed to store conversation: {e}")
|
||||
raise HindsightError(f"Memory storage failed: {e}") from e
|
||||
|
||||
async def _store_conversation_async(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response: ModelResponse,
|
||||
model: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig,
|
||||
) -> None:
|
||||
"""Store the conversation to Hindsight (async).
|
||||
|
||||
Uses thread pool executor with sync HTTP to avoid event loop conflicts.
|
||||
|
||||
Raises:
|
||||
HindsightError: If store_conversations=True and store fails.
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(
|
||||
_executor,
|
||||
self._store_conversation_sync,
|
||||
messages,
|
||||
response,
|
||||
model,
|
||||
config
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(
|
||||
_executor,
|
||||
lambda: self._store_conversation_sync(
|
||||
messages, response, model, settings, config
|
||||
)
|
||||
except Exception as e:
|
||||
if config.verbose:
|
||||
logger.warning(f"Failed to store conversation: {e}")
|
||||
)
|
||||
|
||||
# ========== LiteLLM CustomLogger Interface ==========
|
||||
|
||||
|
|
@ -449,28 +675,53 @@ class HindsightCallback(CustomLogger):
|
|||
|
||||
This is where we inject memories into the messages.
|
||||
"""
|
||||
if not is_configured():
|
||||
config = get_config()
|
||||
if not config or not config.inject_memories:
|
||||
return
|
||||
|
||||
config = get_config()
|
||||
if not config or not config.enabled or not config.inject_memories:
|
||||
return
|
||||
# Get effective settings (kwargs override defaults)
|
||||
settings = self._get_effective_settings(kwargs)
|
||||
if not settings.bank_id:
|
||||
raise ValueError(
|
||||
"No bank_id configured. Either call set_defaults(bank_id=...) "
|
||||
"or pass hindsight_bank_id=... to the completion call."
|
||||
)
|
||||
|
||||
if self._should_skip_model(model, config):
|
||||
return
|
||||
|
||||
# Extract user query
|
||||
user_query = self._extract_user_query(messages)
|
||||
if not user_query:
|
||||
return
|
||||
# hindsight_query is required when inject_memories=True
|
||||
custom_query = kwargs.get("hindsight_query")
|
||||
if not custom_query:
|
||||
raise ValueError(
|
||||
"hindsight_query is required when inject_memories=True. "
|
||||
"Pass hindsight_query='your query' to specify what to search for in memory. "
|
||||
"Example: hindsight_query=recipient_name or hindsight_query='What do I know about Alice?'"
|
||||
)
|
||||
|
||||
# Recall relevant memories
|
||||
memories = self._recall_memories_sync(user_query, config)
|
||||
if not memories:
|
||||
return
|
||||
user_query = custom_query
|
||||
|
||||
# Use reflect or recall based on settings
|
||||
if settings.use_reflect:
|
||||
# Use reflect API for disposition-aware reasoning
|
||||
reflect_response = self._reflect_sync(user_query, settings, config)
|
||||
if not reflect_response:
|
||||
return
|
||||
|
||||
# Format reflect response as context
|
||||
memory_context = (
|
||||
"# Relevant Context from Memory\n"
|
||||
f"{reflect_response}"
|
||||
)
|
||||
else:
|
||||
# Use recall API for raw fact retrieval
|
||||
memories = self._recall_memories_sync(user_query, settings, config)
|
||||
if not memories:
|
||||
return
|
||||
|
||||
# Format and inject memories
|
||||
memory_context = self._format_memories(memories, settings, config)
|
||||
|
||||
# Format and inject memories
|
||||
memory_context = self._format_memories(memories, config)
|
||||
updated_messages = self._inject_memories_into_messages(
|
||||
messages, memory_context, config
|
||||
)
|
||||
|
|
@ -480,7 +731,8 @@ class HindsightCallback(CustomLogger):
|
|||
messages.extend(updated_messages)
|
||||
|
||||
if config.verbose:
|
||||
logger.info(f"Injected {len(memories)} memories into prompt")
|
||||
mode = "reflect" if settings.use_reflect else "recall"
|
||||
logger.info(f"Injected memory context via {mode}")
|
||||
|
||||
async def async_log_pre_api_call(
|
||||
self,
|
||||
|
|
@ -492,28 +744,53 @@ class HindsightCallback(CustomLogger):
|
|||
|
||||
This is where we inject memories into the messages.
|
||||
"""
|
||||
if not is_configured():
|
||||
config = get_config()
|
||||
if not config or not config.inject_memories:
|
||||
return
|
||||
|
||||
config = get_config()
|
||||
if not config or not config.enabled or not config.inject_memories:
|
||||
return
|
||||
# Get effective settings (kwargs override defaults)
|
||||
settings = self._get_effective_settings(kwargs)
|
||||
if not settings.bank_id:
|
||||
raise ValueError(
|
||||
"No bank_id configured. Either call set_defaults(bank_id=...) "
|
||||
"or pass hindsight_bank_id=... to the completion call."
|
||||
)
|
||||
|
||||
if self._should_skip_model(model, config):
|
||||
return
|
||||
|
||||
# Extract user query
|
||||
user_query = self._extract_user_query(messages)
|
||||
if not user_query:
|
||||
return
|
||||
# hindsight_query is required when inject_memories=True
|
||||
custom_query = kwargs.get("hindsight_query")
|
||||
if not custom_query:
|
||||
raise ValueError(
|
||||
"hindsight_query is required when inject_memories=True. "
|
||||
"Pass hindsight_query='your query' to specify what to search for in memory. "
|
||||
"Example: hindsight_query=recipient_name or hindsight_query='What do I know about Alice?'"
|
||||
)
|
||||
|
||||
# Recall relevant memories
|
||||
memories = await self._recall_memories_async(user_query, config)
|
||||
if not memories:
|
||||
return
|
||||
user_query = custom_query
|
||||
|
||||
# Use reflect or recall based on settings
|
||||
if settings.use_reflect:
|
||||
# Use reflect API for disposition-aware reasoning
|
||||
reflect_response = await self._reflect_async(user_query, settings, config)
|
||||
if not reflect_response:
|
||||
return
|
||||
|
||||
# Format reflect response as context
|
||||
memory_context = (
|
||||
"# Relevant Context from Memory\n"
|
||||
f"{reflect_response}"
|
||||
)
|
||||
else:
|
||||
# Use recall API for raw fact retrieval
|
||||
memories = await self._recall_memories_async(user_query, settings, config)
|
||||
if not memories:
|
||||
return
|
||||
|
||||
# Format and inject memories
|
||||
memory_context = self._format_memories(memories, settings, config)
|
||||
|
||||
# Format and inject memories
|
||||
memory_context = self._format_memories(memories, config)
|
||||
updated_messages = self._inject_memories_into_messages(
|
||||
messages, memory_context, config
|
||||
)
|
||||
|
|
@ -523,7 +800,8 @@ class HindsightCallback(CustomLogger):
|
|||
messages.extend(updated_messages)
|
||||
|
||||
if config.verbose:
|
||||
logger.info(f"Injected {len(memories)} memories into prompt")
|
||||
mode = "reflect" if settings.use_reflect else "recall"
|
||||
logger.info(f"Injected memory context via {mode}")
|
||||
|
||||
def log_success_event(
|
||||
self,
|
||||
|
|
@ -536,11 +814,14 @@ class HindsightCallback(CustomLogger):
|
|||
|
||||
This is where we store the conversation.
|
||||
"""
|
||||
if not is_configured():
|
||||
config = get_config()
|
||||
if not config or not config.store_conversations:
|
||||
return
|
||||
|
||||
config = get_config()
|
||||
if not config or not config.enabled or not config.store_conversations:
|
||||
# Get effective settings (kwargs override defaults)
|
||||
settings = self._get_effective_settings(kwargs)
|
||||
if not settings.bank_id:
|
||||
# bank_id validation already done in log_pre_api_call
|
||||
return
|
||||
|
||||
model = kwargs.get("model", "unknown")
|
||||
|
|
@ -552,7 +833,7 @@ class HindsightCallback(CustomLogger):
|
|||
return
|
||||
|
||||
# Store the conversation
|
||||
self._store_conversation_sync(messages, response_obj, model, config)
|
||||
self._store_conversation_sync(messages, response_obj, model, settings, config)
|
||||
|
||||
async def async_log_success_event(
|
||||
self,
|
||||
|
|
@ -565,11 +846,14 @@ class HindsightCallback(CustomLogger):
|
|||
|
||||
This is where we store the conversation.
|
||||
"""
|
||||
if not is_configured():
|
||||
config = get_config()
|
||||
if not config or not config.store_conversations:
|
||||
return
|
||||
|
||||
config = get_config()
|
||||
if not config or not config.enabled or not config.store_conversations:
|
||||
# Get effective settings (kwargs override defaults)
|
||||
settings = self._get_effective_settings(kwargs)
|
||||
if not settings.bank_id:
|
||||
# bank_id validation already done in async_log_pre_api_call
|
||||
return
|
||||
|
||||
model = kwargs.get("model", "unknown")
|
||||
|
|
@ -581,7 +865,7 @@ class HindsightCallback(CustomLogger):
|
|||
return
|
||||
|
||||
# Store the conversation
|
||||
await self._store_conversation_async(messages, response_obj, model, config)
|
||||
await self._store_conversation_async(messages, response_obj, model, settings, config)
|
||||
|
||||
def log_failure_event(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1,179 +1,258 @@
|
|||
"""Global configuration for Hindsight-LiteLLM integration."""
|
||||
"""Global configuration for Hindsight-LiteLLM integration.
|
||||
|
||||
from typing import Optional, List
|
||||
This module provides a clean API for configuring Hindsight integration:
|
||||
|
||||
1. configure() - Static settings that rarely change during a session
|
||||
- API URL, authentication, logging, injection mode, etc.
|
||||
|
||||
2. set_defaults() - Default values for per-call settings
|
||||
- bank_id, document_id, budget, fact_types, etc.
|
||||
- These are used when per-call kwargs are not provided
|
||||
|
||||
3. Per-call kwargs (hindsight_* prefix) - Override any default per-call
|
||||
- hindsight_bank_id, hindsight_document_id, etc.
|
||||
|
||||
4. set_bank_mission() - Set the mission for a memory bank (for mental models)
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MemoryInjectionMode(str, Enum):
|
||||
"""How memories should be injected into the prompt."""
|
||||
SYSTEM_MESSAGE = "system_message" # Add as system message
|
||||
PREPEND_USER = "prepend_user" # Prepend to user message
|
||||
DISABLED = "disabled" # Don't inject memories
|
||||
"""How memories should be injected into the prompt.
|
||||
|
||||
Use inject_memories=False if you don't want memory injection.
|
||||
"""
|
||||
SYSTEM_MESSAGE = "system_message" # Add to/create system message
|
||||
PREPEND_USER = "prepend_user" # Prepend to last user message
|
||||
|
||||
|
||||
@dataclass
|
||||
class HindsightConfig:
|
||||
"""Configuration for Hindsight integration with LiteLLM.
|
||||
"""Static configuration for Hindsight integration with LiteLLM.
|
||||
|
||||
These settings typically don't change during a session.
|
||||
|
||||
Attributes:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
bank_id: Memory bank ID for memory operations (required). For multi-user
|
||||
support, use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
api_key: Optional API key for Hindsight authentication
|
||||
store_conversations: Whether to store conversations to Hindsight
|
||||
inject_memories: Whether to inject relevant memories into prompts
|
||||
injection_mode: How to inject memories (system_message or prepend_user)
|
||||
max_memories: Maximum number of memories to inject
|
||||
max_memory_tokens: Maximum tokens for injected memory context
|
||||
recall_budget: Budget level for memory recall (low, mid, high)
|
||||
fact_types: List of fact types to filter recall (world, agent, opinion, observation)
|
||||
document_id: Optional document ID for grouping stored conversations
|
||||
enabled: Master switch to enable/disable Hindsight integration
|
||||
excluded_models: List of model patterns to exclude from interception
|
||||
verbose: Enable verbose logging
|
||||
bank_name: Optional display name for the memory bank
|
||||
background: Optional background/instructions for memory extraction
|
||||
use_reflect: Use reflect API instead of recall for memory injection (synthesizes answer)
|
||||
sync_storage: If True, storage runs synchronously and raises errors immediately.
|
||||
If False (default), storage runs in background thread for better performance.
|
||||
"""
|
||||
|
||||
hindsight_api_url: str = "http://localhost:8888"
|
||||
bank_id: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
store_conversations: bool = True
|
||||
inject_memories: bool = True
|
||||
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE
|
||||
max_memories: Optional[int] = None # None = no limit (use all results from API)
|
||||
max_memory_tokens: int = 4096
|
||||
recall_budget: str = "mid" # low, mid, high
|
||||
fact_types: Optional[List[str]] = None # world, agent, opinion, observation
|
||||
document_id: Optional[str] = None
|
||||
enabled: bool = True
|
||||
excluded_models: List[str] = field(default_factory=list)
|
||||
verbose: bool = False
|
||||
bank_name: Optional[str] = None # Display name for the memory bank
|
||||
background: Optional[str] = None # Background/instructions for memory extraction
|
||||
use_reflect: bool = False # Use reflect instead of recall for memory injection
|
||||
reflect_include_facts: bool = False # Include facts used by reflect in debug info
|
||||
sync_storage: bool = False
|
||||
|
||||
|
||||
# Global configuration instance
|
||||
@dataclass
|
||||
class HindsightDefaults:
|
||||
"""Default values for per-call settings.
|
||||
|
||||
These can be overridden on a per-call basis using hindsight_* kwargs.
|
||||
|
||||
Attributes:
|
||||
bank_id: Memory bank ID for memory operations
|
||||
document_id: Optional document ID for grouping stored conversations
|
||||
budget: Budget level for memory recall (low, mid, high)
|
||||
fact_types: List of fact types to filter recall (world, experience, opinion, observation)
|
||||
max_memories: Maximum number of memories to inject (None = no limit)
|
||||
max_memory_tokens: Maximum tokens for injected memory context
|
||||
use_reflect: Use reflect API instead of recall for memory injection
|
||||
reflect_include_facts: Include facts used by reflect in debug info
|
||||
reflect_context: Additional context for reflect reasoning (does not affect retrieval)
|
||||
reflect_response_schema: JSON Schema for structured reflect output
|
||||
include_entities: Include entity observations in recall results
|
||||
trace: Enable trace info for recall debugging
|
||||
|
||||
Note:
|
||||
For custom queries, use the hindsight_query kwarg per-call instead of a default,
|
||||
since queries typically need to be dynamic (e.g., include recipient name).
|
||||
"""
|
||||
|
||||
bank_id: Optional[str] = None
|
||||
document_id: Optional[str] = None
|
||||
budget: str = "mid" # low, mid, high
|
||||
fact_types: Optional[List[str]] = None # world, experience, opinion, observation
|
||||
max_memories: Optional[int] = None # None = no limit
|
||||
max_memory_tokens: int = 4096
|
||||
use_reflect: bool = False
|
||||
reflect_include_facts: bool = False
|
||||
reflect_context: Optional[str] = None # Context for reflect reasoning
|
||||
reflect_response_schema: Optional[Dict[str, Any]] = None # JSON Schema for structured output
|
||||
include_entities: bool = True # Include entity observations by default
|
||||
trace: bool = False # Enable trace info for debugging
|
||||
|
||||
|
||||
# Global instances
|
||||
_global_config: Optional[HindsightConfig] = None
|
||||
_global_defaults: Optional[HindsightDefaults] = None
|
||||
|
||||
|
||||
def configure(
|
||||
hindsight_api_url: str = "http://localhost:8888",
|
||||
bank_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE,
|
||||
max_memories: Optional[int] = None,
|
||||
max_memory_tokens: int = 4096,
|
||||
recall_budget: str = "mid",
|
||||
fact_types: Optional[List[str]] = None,
|
||||
document_id: Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
excluded_models: Optional[List[str]] = None,
|
||||
verbose: bool = False,
|
||||
bank_name: Optional[str] = None,
|
||||
background: Optional[str] = None,
|
||||
use_reflect: bool = False,
|
||||
reflect_include_facts: bool = False,
|
||||
sync_storage: bool = False,
|
||||
) -> HindsightConfig:
|
||||
"""Configure global Hindsight integration settings for LiteLLM.
|
||||
"""Configure static Hindsight integration settings for LiteLLM.
|
||||
|
||||
This function sets up the global configuration that will be used by the
|
||||
LiteLLM callbacks to inject memories and store conversations.
|
||||
This sets up settings that typically don't change during a session.
|
||||
For per-call settings like bank_id, use set_defaults() or per-call kwargs.
|
||||
|
||||
Args:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
bank_id: Memory bank ID for memory operations (required). For multi-user
|
||||
support, use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
api_key: Optional API key for Hindsight authentication
|
||||
store_conversations: Whether to store conversations to Hindsight
|
||||
inject_memories: Whether to inject relevant memories into prompts
|
||||
injection_mode: How to inject memories into the prompt
|
||||
max_memories: Maximum number of memories to inject
|
||||
max_memory_tokens: Maximum tokens for injected memory context
|
||||
recall_budget: Budget level for memory recall (low, mid, high)
|
||||
fact_types: List of fact types to filter (world, agent, opinion, observation)
|
||||
document_id: Optional document ID for grouping stored conversations
|
||||
enabled: Master switch to enable/disable Hindsight integration
|
||||
excluded_models: List of model patterns to exclude from interception
|
||||
verbose: Enable verbose logging
|
||||
bank_name: Optional display name for the memory bank
|
||||
background: Optional background/instructions that help Hindsight understand
|
||||
what information is important to extract and remember from conversations.
|
||||
This is passed to create_bank() to configure the memory bank.
|
||||
use_reflect: Use reflect API instead of recall for memory injection.
|
||||
When True, Hindsight will synthesize a contextual answer based on
|
||||
memories rather than returning raw memory facts.
|
||||
reflect_include_facts: When use_reflect=True, include the facts that
|
||||
were used to generate the reflect response in the debug info.
|
||||
This is useful for debugging what memories the reflect API used.
|
||||
sync_storage: If True, storage runs synchronously and raises errors immediately.
|
||||
If False (default), storage runs in background for better performance.
|
||||
Use get_pending_storage_errors() to check for async storage failures.
|
||||
|
||||
Returns:
|
||||
The configured HindsightConfig instance
|
||||
|
||||
Example:
|
||||
>>> from hindsight_litellm import configure, enable
|
||||
>>> from hindsight_litellm import configure, set_defaults, enable
|
||||
>>> configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... bank_id="user-123", # Per-user bank for multi-user support
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... background="This agent routes customer requests to support channels. "
|
||||
... "Remember which types of issues should go to which channels.",
|
||||
... api_key="your-api-key",
|
||||
... verbose=True,
|
||||
... )
|
||||
>>> enable() # Register callbacks with LiteLLM
|
||||
>>> set_defaults(bank_id="user-123")
|
||||
>>> enable() # Start memory integration
|
||||
"""
|
||||
global _global_config
|
||||
|
||||
_global_config = HindsightConfig(
|
||||
hindsight_api_url=hindsight_api_url,
|
||||
bank_id=bank_id,
|
||||
api_key=api_key,
|
||||
store_conversations=store_conversations,
|
||||
inject_memories=inject_memories,
|
||||
injection_mode=injection_mode,
|
||||
max_memories=max_memories,
|
||||
max_memory_tokens=max_memory_tokens,
|
||||
recall_budget=recall_budget,
|
||||
fact_types=fact_types,
|
||||
document_id=document_id,
|
||||
enabled=enabled,
|
||||
excluded_models=excluded_models or [],
|
||||
verbose=verbose,
|
||||
bank_name=bank_name,
|
||||
background=background,
|
||||
use_reflect=use_reflect,
|
||||
reflect_include_facts=reflect_include_facts,
|
||||
sync_storage=sync_storage,
|
||||
)
|
||||
|
||||
# If background or bank_name is provided, create/update the bank
|
||||
if bank_id and (background or bank_name):
|
||||
_create_or_update_bank(
|
||||
hindsight_api_url=hindsight_api_url,
|
||||
bank_id=bank_id,
|
||||
name=bank_name,
|
||||
background=background,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
return _global_config
|
||||
|
||||
|
||||
def set_defaults(
|
||||
bank_id: Optional[str] = None,
|
||||
document_id: Optional[str] = None,
|
||||
budget: Optional[str] = None,
|
||||
fact_types: Optional[List[str]] = None,
|
||||
max_memories: Optional[int] = None,
|
||||
max_memory_tokens: Optional[int] = None,
|
||||
use_reflect: Optional[bool] = None,
|
||||
reflect_include_facts: Optional[bool] = None,
|
||||
reflect_context: Optional[str] = None,
|
||||
reflect_response_schema: Optional[Dict[str, Any]] = None,
|
||||
include_entities: Optional[bool] = None,
|
||||
trace: Optional[bool] = None,
|
||||
) -> HindsightDefaults:
|
||||
"""Set default values for per-call settings.
|
||||
|
||||
These defaults are used when per-call kwargs are not provided.
|
||||
Any of these can be overridden on individual LLM calls using
|
||||
hindsight_* kwargs (e.g., hindsight_bank_id="other-bank").
|
||||
|
||||
Args:
|
||||
bank_id: Default memory bank ID for memory operations
|
||||
document_id: Default document ID for grouping stored conversations
|
||||
budget: Default budget level for memory recall (low, mid, high)
|
||||
fact_types: Default fact types to filter (world, experience, opinion, observation)
|
||||
max_memories: Default max number of memories to inject
|
||||
max_memory_tokens: Default max tokens for memory context
|
||||
use_reflect: Default whether to use reflect API instead of recall
|
||||
reflect_include_facts: Default whether to include facts in reflect debug info
|
||||
reflect_context: Default context for reflect reasoning (shapes LLM response, not retrieval)
|
||||
reflect_response_schema: Default JSON Schema for structured reflect output
|
||||
include_entities: Default whether to include entity observations in recall (default True)
|
||||
trace: Default whether to enable trace info for debugging (default False)
|
||||
|
||||
Returns:
|
||||
The configured HindsightDefaults instance
|
||||
|
||||
Note:
|
||||
For custom memory queries, use hindsight_query per-call instead of a default,
|
||||
since queries typically need to be dynamic (e.g., include recipient name).
|
||||
|
||||
Example:
|
||||
>>> from hindsight_litellm import set_defaults
|
||||
>>> set_defaults(
|
||||
... bank_id="my-agent",
|
||||
... budget="high",
|
||||
... fact_types=["world", "opinion"],
|
||||
... reflect_context="I am a delivery agent finding package recipients.",
|
||||
... )
|
||||
>>>
|
||||
>>> # Override per-call with dynamic query:
|
||||
>>> response = litellm.completion(
|
||||
... model="gpt-4",
|
||||
... messages=[...],
|
||||
... hindsight_query=f"Where is {recipient_name} located?", # Dynamic query
|
||||
... )
|
||||
"""
|
||||
global _global_defaults
|
||||
|
||||
# Get current defaults or create new
|
||||
current = _global_defaults or HindsightDefaults()
|
||||
|
||||
# Update only provided values
|
||||
_global_defaults = HindsightDefaults(
|
||||
bank_id=bank_id if bank_id is not None else current.bank_id,
|
||||
document_id=document_id if document_id is not None else current.document_id,
|
||||
budget=budget if budget is not None else current.budget,
|
||||
fact_types=fact_types if fact_types is not None else current.fact_types,
|
||||
max_memories=max_memories if max_memories is not None else current.max_memories,
|
||||
max_memory_tokens=max_memory_tokens if max_memory_tokens is not None else current.max_memory_tokens,
|
||||
use_reflect=use_reflect if use_reflect is not None else current.use_reflect,
|
||||
reflect_include_facts=reflect_include_facts if reflect_include_facts is not None else current.reflect_include_facts,
|
||||
reflect_context=reflect_context if reflect_context is not None else current.reflect_context,
|
||||
reflect_response_schema=reflect_response_schema if reflect_response_schema is not None else current.reflect_response_schema,
|
||||
include_entities=include_entities if include_entities is not None else current.include_entities,
|
||||
trace=trace if trace is not None else current.trace,
|
||||
)
|
||||
|
||||
return _global_defaults
|
||||
|
||||
|
||||
def _create_or_update_bank(
|
||||
hindsight_api_url: str,
|
||||
bank_id: str,
|
||||
name: Optional[str] = None,
|
||||
background: Optional[str] = None,
|
||||
mission: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
) -> None:
|
||||
"""Create or update a memory bank with the given configuration.
|
||||
|
||||
This is called automatically by configure() when background or bank_name is provided.
|
||||
Args:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
bank_id: The bank ID to create/update
|
||||
name: Optional display name for the bank
|
||||
mission: Instructions guiding what Hindsight should learn and remember
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
try:
|
||||
from hindsight_client import Hindsight
|
||||
|
|
@ -182,18 +261,18 @@ def _create_or_update_bank(
|
|||
client.create_bank(
|
||||
bank_id=bank_id,
|
||||
name=name,
|
||||
background=background,
|
||||
mission=mission,
|
||||
)
|
||||
if verbose:
|
||||
import logging
|
||||
logging.getLogger("hindsight_litellm").info(
|
||||
f"Created/updated bank '{bank_id}' with background"
|
||||
f"Created/updated bank '{bank_id}' with mission"
|
||||
)
|
||||
except ImportError:
|
||||
if verbose:
|
||||
import logging
|
||||
logging.getLogger("hindsight_litellm").warning(
|
||||
"hindsight_client not installed. Cannot create bank with background. "
|
||||
"hindsight_client not installed. Cannot create bank. "
|
||||
"Install with: pip install hindsight-client"
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
@ -205,7 +284,7 @@ def _create_or_update_bank(
|
|||
|
||||
|
||||
def get_config() -> Optional[HindsightConfig]:
|
||||
"""Get the current global configuration.
|
||||
"""Get the current global static configuration.
|
||||
|
||||
Returns:
|
||||
The current HindsightConfig instance, or None if not configured
|
||||
|
|
@ -213,20 +292,136 @@ def get_config() -> Optional[HindsightConfig]:
|
|||
return _global_config
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
"""Check if Hindsight has been configured.
|
||||
def get_defaults() -> Optional[HindsightDefaults]:
|
||||
"""Get the current global defaults for per-call settings.
|
||||
|
||||
Returns:
|
||||
True if configure() has been called with a valid bank_id
|
||||
The current HindsightDefaults instance, or None if not set
|
||||
"""
|
||||
return _global_defaults
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
"""Check if Hindsight has been configured with a valid bank_id.
|
||||
|
||||
Returns:
|
||||
True if configure() has been called and a bank_id is set in defaults
|
||||
"""
|
||||
return (
|
||||
_global_config is not None
|
||||
and _global_config.enabled
|
||||
and _global_config.bank_id is not None
|
||||
and _global_defaults is not None
|
||||
and _global_defaults.bank_id is not None
|
||||
)
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset the global configuration to None."""
|
||||
global _global_config
|
||||
"""Reset all global configuration to None."""
|
||||
global _global_config, _global_defaults
|
||||
_global_config = None
|
||||
_global_defaults = None
|
||||
|
||||
|
||||
def set_document_id(document_id: str | None) -> None:
|
||||
"""Set the document_id for grouping stored conversations.
|
||||
|
||||
This is a convenience function that updates just the document_id
|
||||
in the defaults without requiring a full set_defaults() call.
|
||||
|
||||
When document_id is set, Hindsight uses upsert behavior:
|
||||
- Same document_id = replace previous version
|
||||
- Hindsight deduplicates facts automatically
|
||||
|
||||
Args:
|
||||
document_id: Document ID for grouping conversations, or None to clear
|
||||
|
||||
Example:
|
||||
>>> from hindsight_litellm import configure, set_defaults, enable, set_document_id
|
||||
>>> configure(hindsight_api_url="http://localhost:8888")
|
||||
>>> set_defaults(bank_id="my-agent")
|
||||
>>> enable()
|
||||
>>>
|
||||
>>> # Start a new conversation
|
||||
>>> set_document_id("conversation-123")
|
||||
>>> response = litellm.completion(model="gpt-4", messages=[...])
|
||||
>>>
|
||||
>>> # Switch to another conversation
|
||||
>>> set_document_id("conversation-456")
|
||||
>>> response = litellm.completion(model="gpt-4", messages=[...])
|
||||
"""
|
||||
global _global_defaults
|
||||
if _global_defaults is not None:
|
||||
_global_defaults = HindsightDefaults(
|
||||
bank_id=_global_defaults.bank_id,
|
||||
document_id=document_id,
|
||||
budget=_global_defaults.budget,
|
||||
fact_types=_global_defaults.fact_types,
|
||||
max_memories=_global_defaults.max_memories,
|
||||
max_memory_tokens=_global_defaults.max_memory_tokens,
|
||||
use_reflect=_global_defaults.use_reflect,
|
||||
reflect_include_facts=_global_defaults.reflect_include_facts,
|
||||
reflect_context=_global_defaults.reflect_context,
|
||||
reflect_response_schema=_global_defaults.reflect_response_schema,
|
||||
include_entities=_global_defaults.include_entities,
|
||||
trace=_global_defaults.trace,
|
||||
)
|
||||
else:
|
||||
# Create defaults with just document_id if none exist
|
||||
_global_defaults = HindsightDefaults(document_id=document_id)
|
||||
|
||||
|
||||
def set_bank_mission(
|
||||
bank_id: Optional[str] = None,
|
||||
mission: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Set or update the mission for a memory bank.
|
||||
|
||||
The mission guides Hindsight on what information to learn and remember,
|
||||
and is used for mental model generation. If the bank doesn't exist,
|
||||
it will be auto-created.
|
||||
|
||||
Args:
|
||||
bank_id: The bank ID to update. If not provided, uses the default bank_id.
|
||||
mission: Instructions guiding what Hindsight should learn and remember.
|
||||
name: Optional display name for the bank.
|
||||
|
||||
Raises:
|
||||
ValueError: If no bank_id is provided and no default is set.
|
||||
RuntimeError: If configure() hasn't been called.
|
||||
|
||||
Example:
|
||||
>>> from hindsight_litellm import configure, set_defaults, set_bank_mission
|
||||
>>> configure(hindsight_api_url="http://localhost:8888")
|
||||
>>> set_defaults(bank_id="delivery-agent")
|
||||
>>> set_bank_mission(
|
||||
... mission="You are a delivery agent navigating a building. "
|
||||
... "Remember employee locations, building layout, and optimal paths."
|
||||
... )
|
||||
"""
|
||||
config = get_config()
|
||||
if not config:
|
||||
raise RuntimeError("Hindsight not configured. Call configure() first.")
|
||||
|
||||
# Determine which bank_id to use
|
||||
effective_bank_id = bank_id
|
||||
if effective_bank_id is None:
|
||||
defaults = get_defaults()
|
||||
if defaults:
|
||||
effective_bank_id = defaults.bank_id
|
||||
|
||||
if not effective_bank_id:
|
||||
raise ValueError(
|
||||
"No bank_id provided and no default bank_id set. "
|
||||
"Either pass bank_id or call set_defaults(bank_id=...) first."
|
||||
)
|
||||
|
||||
# Use the Hindsight API to create/update the bank
|
||||
_create_or_update_bank(
|
||||
hindsight_api_url=config.hindsight_api_url,
|
||||
bank_id=effective_bank_id,
|
||||
name=name,
|
||||
mission=mission,
|
||||
verbose=config.verbose,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,36 @@ integration with native client libraries.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .config import get_config, is_configured, HindsightConfig
|
||||
from .config import get_config, get_defaults, is_configured, HindsightConfig
|
||||
|
||||
|
||||
# Background thread support for async retain
|
||||
_retain_errors: List[Exception] = []
|
||||
_retain_errors_lock = threading.Lock()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_client(api_url: str):
|
||||
"""Create a fresh Hindsight client for the given URL.
|
||||
|
||||
Note: We create a fresh client each time because the hindsight_client
|
||||
uses aiohttp internally, and reusing clients across different sync
|
||||
calls causes asyncio context issues.
|
||||
"""
|
||||
from hindsight_client import Hindsight
|
||||
return Hindsight(base_url=api_url, timeout=30.0)
|
||||
|
||||
|
||||
def _close_client():
|
||||
"""No-op for compatibility. Clients are now closed after each use."""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecallResult:
|
||||
"""A single memory recall result."""
|
||||
|
|
@ -106,24 +127,25 @@ def recall(
|
|||
>>> if memories.debug:
|
||||
... print(f"Queried bank: {memories.debug.bank_id}")
|
||||
"""
|
||||
# Get config or use overrides
|
||||
# Get config and defaults, or use overrides
|
||||
config = get_config()
|
||||
defaults = get_defaults()
|
||||
|
||||
api_url = hindsight_api_url or (config.hindsight_api_url if config else None)
|
||||
target_bank_id = bank_id or (config.bank_id if config else None)
|
||||
target_fact_types = fact_types or (config.fact_types if config else None)
|
||||
target_budget = budget or (config.recall_budget if config else "mid")
|
||||
target_max_tokens = max_tokens or (config.max_memory_tokens if config else 4096)
|
||||
target_bank_id = bank_id or (defaults.bank_id if defaults else None)
|
||||
target_fact_types = fact_types or (defaults.fact_types if defaults else None)
|
||||
target_budget = budget or (defaults.budget if defaults else "mid")
|
||||
target_max_tokens = max_tokens or (defaults.max_memory_tokens if defaults else 4096)
|
||||
|
||||
if not api_url or not target_bank_id:
|
||||
raise RuntimeError(
|
||||
"Hindsight not configured. Call configure() or provide bank_id and hindsight_api_url."
|
||||
)
|
||||
|
||||
client = None
|
||||
try:
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url=api_url, timeout=30.0)
|
||||
# Create fresh client for this operation
|
||||
client = _get_client(api_url)
|
||||
|
||||
# Call recall API
|
||||
results = client.recall(
|
||||
|
|
@ -178,6 +200,12 @@ def recall(
|
|||
if config and config.verbose:
|
||||
logger.warning(f"Failed to recall memories: {e}")
|
||||
raise
|
||||
finally:
|
||||
if client is not None:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def arecall(
|
||||
|
|
@ -233,6 +261,7 @@ def reflect(
|
|||
bank_id: Optional[str] = None,
|
||||
budget: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
response_schema: Optional[dict] = None,
|
||||
hindsight_api_url: Optional[str] = None,
|
||||
) -> ReflectResult:
|
||||
"""Generate a contextual answer based on memories.
|
||||
|
|
@ -246,10 +275,11 @@ def reflect(
|
|||
use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
budget: Budget level for reflection (low, mid, high)
|
||||
context: Additional context to include in the reflection
|
||||
response_schema: JSON Schema for structured output
|
||||
hindsight_api_url: Override the configured API URL
|
||||
|
||||
Returns:
|
||||
ReflectResult with synthesized answer text
|
||||
ReflectResult with synthesized answer text (or structured_output if schema provided)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Hindsight is not configured and no overrides provided
|
||||
|
|
@ -264,28 +294,33 @@ def reflect(
|
|||
Based on our conversations, you're working on a FastAPI project...
|
||||
"""
|
||||
config = get_config()
|
||||
defaults = get_defaults()
|
||||
|
||||
api_url = hindsight_api_url or (config.hindsight_api_url if config else None)
|
||||
target_bank_id = bank_id or (config.bank_id if config else None)
|
||||
target_budget = budget or (config.recall_budget if config else "mid")
|
||||
target_bank_id = bank_id or (defaults.bank_id if defaults else None)
|
||||
target_budget = budget or (defaults.budget if defaults else "mid")
|
||||
|
||||
if not api_url or not target_bank_id:
|
||||
raise RuntimeError(
|
||||
"Hindsight not configured. Call configure() or provide bank_id and hindsight_api_url."
|
||||
)
|
||||
|
||||
client = None
|
||||
try:
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url=api_url, timeout=30.0)
|
||||
# Create fresh client for this operation
|
||||
client = _get_client(api_url)
|
||||
|
||||
# Call reflect API
|
||||
result = client.reflect(
|
||||
bank_id=target_bank_id,
|
||||
query=query,
|
||||
budget=target_budget,
|
||||
context=context,
|
||||
)
|
||||
reflect_kwargs = {
|
||||
"bank_id": target_bank_id,
|
||||
"query": query,
|
||||
"budget": target_budget,
|
||||
}
|
||||
if context is not None:
|
||||
reflect_kwargs["context"] = context
|
||||
if response_schema is not None:
|
||||
reflect_kwargs["response_schema"] = response_schema
|
||||
result = client.reflect(**reflect_kwargs)
|
||||
|
||||
# Convert to ReflectResult
|
||||
text = result.text if hasattr(result, 'text') else str(result)
|
||||
|
|
@ -310,6 +345,12 @@ def reflect(
|
|||
if config and config.verbose:
|
||||
logger.warning(f"Failed to reflect: {e}")
|
||||
raise
|
||||
finally:
|
||||
if client is not None:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def areflect(
|
||||
|
|
@ -317,6 +358,7 @@ async def areflect(
|
|||
bank_id: Optional[str] = None,
|
||||
budget: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
response_schema: Optional[dict] = None,
|
||||
hindsight_api_url: Optional[str] = None,
|
||||
) -> ReflectResult:
|
||||
"""Async version of reflect().
|
||||
|
|
@ -332,6 +374,7 @@ async def areflect(
|
|||
bank_id=bank_id,
|
||||
budget=budget,
|
||||
context=context,
|
||||
response_schema=response_schema,
|
||||
hindsight_api_url=hindsight_api_url,
|
||||
)
|
||||
)
|
||||
|
|
@ -359,64 +402,20 @@ class RetainResult:
|
|||
return self.success
|
||||
|
||||
|
||||
def retain(
|
||||
def _retain_sync(
|
||||
content: str,
|
||||
bank_id: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
document_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
hindsight_api_url: Optional[str] = None,
|
||||
api_url: str,
|
||||
target_bank_id: str,
|
||||
context: Optional[str],
|
||||
target_document_id: Optional[str],
|
||||
metadata: Optional[Dict[str, str]],
|
||||
verbose: bool,
|
||||
) -> RetainResult:
|
||||
"""Store content to Hindsight memory.
|
||||
|
||||
This function allows you to manually store content to memory without
|
||||
making an LLM call. Useful for storing feedback, user preferences,
|
||||
or any other information you want the system to remember.
|
||||
|
||||
Args:
|
||||
content: The text content to store
|
||||
bank_id: Override the configured bank_id. For multi-user support,
|
||||
use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
context: Context description for the memory (e.g., "customer_feedback")
|
||||
document_id: Optional document ID for grouping related memories
|
||||
metadata: Optional key-value metadata to attach to the memory
|
||||
hindsight_api_url: Override the configured API URL
|
||||
|
||||
Returns:
|
||||
RetainResult indicating success
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Hindsight is not configured and no overrides provided
|
||||
|
||||
Example:
|
||||
>>> from hindsight_litellm import configure, retain
|
||||
>>> configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
>>>
|
||||
>>> # Store feedback
|
||||
>>> retain("User prefers dark mode", context="user_preference")
|
||||
>>>
|
||||
>>> # Store with metadata
|
||||
>>> retain(
|
||||
... "Customer reported billing issue resolved",
|
||||
... context="support_ticket",
|
||||
... metadata={"ticket_id": "12345", "status": "resolved"}
|
||||
... )
|
||||
"""
|
||||
config = get_config()
|
||||
|
||||
api_url = hindsight_api_url or (config.hindsight_api_url if config else None)
|
||||
target_bank_id = bank_id or (config.bank_id if config else None)
|
||||
target_document_id = document_id or (config.document_id if config else None)
|
||||
|
||||
if not api_url or not target_bank_id:
|
||||
raise RuntimeError(
|
||||
"Hindsight not configured. Call configure() or provide bank_id and hindsight_api_url."
|
||||
)
|
||||
|
||||
"""Internal synchronous retain implementation."""
|
||||
client = None
|
||||
try:
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url=api_url, timeout=30.0)
|
||||
# Create fresh client for this operation
|
||||
client = _get_client(api_url)
|
||||
|
||||
# Call retain API
|
||||
result = client.retain(
|
||||
|
|
@ -433,7 +432,7 @@ def retain(
|
|||
|
||||
# Include debug info if verbose
|
||||
debug_info = None
|
||||
if config and config.verbose:
|
||||
if verbose:
|
||||
logger.info(f"Stored content to Hindsight bank: {target_bank_id}")
|
||||
debug_info = RetainDebugInfo(
|
||||
content=content,
|
||||
|
|
@ -449,9 +448,158 @@ def retain(
|
|||
except ImportError as e:
|
||||
raise RuntimeError(f"hindsight-client not installed: {e}")
|
||||
except Exception as e:
|
||||
if config and config.verbose:
|
||||
if verbose:
|
||||
logger.warning(f"Failed to retain: {e}")
|
||||
raise
|
||||
finally:
|
||||
if client is not None:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _retain_background(
|
||||
content: str,
|
||||
api_url: str,
|
||||
target_bank_id: str,
|
||||
context: Optional[str],
|
||||
target_document_id: Optional[str],
|
||||
metadata: Optional[Dict[str, str]],
|
||||
verbose: bool,
|
||||
) -> None:
|
||||
"""Background thread worker for async retain."""
|
||||
global _retain_errors
|
||||
try:
|
||||
_retain_sync(
|
||||
content=content,
|
||||
api_url=api_url,
|
||||
target_bank_id=target_bank_id,
|
||||
context=context,
|
||||
target_document_id=target_document_id,
|
||||
metadata=metadata,
|
||||
verbose=verbose,
|
||||
)
|
||||
except Exception as e:
|
||||
with _retain_errors_lock:
|
||||
_retain_errors.append(e)
|
||||
logger.warning(f"Background retain failed: {e}")
|
||||
|
||||
|
||||
def get_pending_retain_errors() -> List[Exception]:
|
||||
"""Get and clear any pending errors from background retain operations.
|
||||
|
||||
When using async retain (sync=False), errors are collected in the background.
|
||||
Call this periodically to check for and handle any failures.
|
||||
|
||||
Returns:
|
||||
List of exceptions from failed background retain operations.
|
||||
The list is cleared after calling this function.
|
||||
|
||||
Example:
|
||||
>>> errors = get_pending_retain_errors()
|
||||
>>> if errors:
|
||||
... for e in errors:
|
||||
... print(f"Retain failed: {e}")
|
||||
"""
|
||||
global _retain_errors
|
||||
with _retain_errors_lock:
|
||||
errors = _retain_errors.copy()
|
||||
_retain_errors.clear()
|
||||
return errors
|
||||
|
||||
|
||||
def retain(
|
||||
content: str,
|
||||
bank_id: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
document_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
hindsight_api_url: Optional[str] = None,
|
||||
sync: bool = False,
|
||||
) -> RetainResult:
|
||||
"""Store content to Hindsight memory.
|
||||
|
||||
This function allows you to manually store content to memory without
|
||||
making an LLM call. Useful for storing feedback, user preferences,
|
||||
or any other information you want the system to remember.
|
||||
|
||||
Args:
|
||||
content: The text content to store
|
||||
bank_id: Override the configured bank_id. For multi-user support,
|
||||
use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
context: Context description for the memory (e.g., "customer_feedback")
|
||||
document_id: Optional document ID for grouping related memories
|
||||
metadata: Optional key-value metadata to attach to the memory
|
||||
hindsight_api_url: Override the configured API URL
|
||||
sync: If True, block until storage completes. If False (default),
|
||||
run in background thread for better performance. Use
|
||||
get_pending_retain_errors() to check for async failures.
|
||||
|
||||
Returns:
|
||||
RetainResult indicating success. For async mode (sync=False),
|
||||
always returns success=True immediately; actual errors are
|
||||
collected via get_pending_retain_errors().
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Hindsight is not configured and no overrides provided
|
||||
Exception: Only raised in sync mode if storage fails
|
||||
|
||||
Example:
|
||||
>>> from hindsight_litellm import configure, retain
|
||||
>>> configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
>>>
|
||||
>>> # Async retain (default) - fast, non-blocking
|
||||
>>> retain("User prefers dark mode", context="user_preference")
|
||||
>>>
|
||||
>>> # Sync retain - blocks until complete
|
||||
>>> retain("Critical data", sync=True)
|
||||
>>>
|
||||
>>> # Check for async errors
|
||||
>>> errors = get_pending_retain_errors()
|
||||
"""
|
||||
config = get_config()
|
||||
defaults = get_defaults()
|
||||
|
||||
api_url = hindsight_api_url or (config.hindsight_api_url if config else None)
|
||||
target_bank_id = bank_id or (defaults.bank_id if defaults else None)
|
||||
target_document_id = document_id or (defaults.document_id if defaults else None)
|
||||
verbose = config.verbose if config else False
|
||||
|
||||
if not api_url or not target_bank_id:
|
||||
raise RuntimeError(
|
||||
"Hindsight not configured. Call configure() or provide bank_id and hindsight_api_url."
|
||||
)
|
||||
|
||||
if sync:
|
||||
# Synchronous mode - block and return result
|
||||
return _retain_sync(
|
||||
content=content,
|
||||
api_url=api_url,
|
||||
target_bank_id=target_bank_id,
|
||||
context=context,
|
||||
target_document_id=target_document_id,
|
||||
metadata=metadata,
|
||||
verbose=verbose,
|
||||
)
|
||||
else:
|
||||
# Async mode - run in background thread
|
||||
thread = threading.Thread(
|
||||
target=_retain_background,
|
||||
args=(
|
||||
content,
|
||||
api_url,
|
||||
target_bank_id,
|
||||
context,
|
||||
target_document_id,
|
||||
metadata,
|
||||
verbose,
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
# Return immediate success - actual errors collected via get_pending_retain_errors()
|
||||
return RetainResult(success=True, items_count=0)
|
||||
|
||||
|
||||
async def aretain(
|
||||
|
|
@ -509,7 +657,7 @@ class HindsightOpenAI:
|
|||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
max_memories: Optional[int] = None,
|
||||
recall_budget: str = "mid",
|
||||
budget: str = "mid",
|
||||
verbose: bool = False,
|
||||
):
|
||||
"""Initialize the wrapped OpenAI client.
|
||||
|
|
@ -523,7 +671,7 @@ class HindsightOpenAI:
|
|||
store_conversations: Whether to store conversations
|
||||
inject_memories: Whether to inject relevant memories
|
||||
max_memories: Maximum number of memories to inject (None = no limit)
|
||||
recall_budget: Budget level for memory recall (low, mid, high)
|
||||
budget: Budget level for memory recall (low, mid, high)
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
self._client = client
|
||||
|
|
@ -533,7 +681,7 @@ class HindsightOpenAI:
|
|||
self._store_conversations = store_conversations
|
||||
self._inject_memories = inject_memories
|
||||
self._max_memories = max_memories
|
||||
self._recall_budget = recall_budget
|
||||
self._budget = budget
|
||||
self._verbose = verbose
|
||||
self._hindsight_client = None
|
||||
|
||||
|
|
@ -560,7 +708,7 @@ class HindsightOpenAI:
|
|||
results = client.recall(
|
||||
bank_id=self._bank_id,
|
||||
query=query,
|
||||
budget=self._recall_budget,
|
||||
budget=self._budget,
|
||||
max_tokens=self._max_memories * 200 if self._max_memories else 4096,
|
||||
)
|
||||
|
||||
|
|
@ -713,7 +861,7 @@ class HindsightAnthropic:
|
|||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
max_memories: Optional[int] = None,
|
||||
recall_budget: str = "mid",
|
||||
budget: str = "mid",
|
||||
verbose: bool = False,
|
||||
):
|
||||
"""Initialize the wrapped Anthropic client.
|
||||
|
|
@ -727,7 +875,7 @@ class HindsightAnthropic:
|
|||
store_conversations: Whether to store conversations
|
||||
inject_memories: Whether to inject relevant memories
|
||||
max_memories: Maximum number of memories to inject (None = no limit)
|
||||
recall_budget: Budget level for memory recall (low, mid, high)
|
||||
budget: Budget level for memory recall (low, mid, high)
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
self._client = client
|
||||
|
|
@ -737,7 +885,7 @@ class HindsightAnthropic:
|
|||
self._store_conversations = store_conversations
|
||||
self._inject_memories = inject_memories
|
||||
self._max_memories = max_memories
|
||||
self._recall_budget = recall_budget
|
||||
self._budget = budget
|
||||
self._verbose = verbose
|
||||
self._hindsight_client = None
|
||||
|
||||
|
|
@ -764,7 +912,7 @@ class HindsightAnthropic:
|
|||
results = client.recall(
|
||||
bank_id=self._bank_id,
|
||||
query=query,
|
||||
budget=self._recall_budget,
|
||||
budget=self._budget,
|
||||
max_tokens=self._max_memories * 200 if self._max_memories else 4096,
|
||||
)
|
||||
|
||||
|
|
@ -889,7 +1037,7 @@ def wrap_openai(
|
|||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
max_memories: Optional[int] = None,
|
||||
recall_budget: str = "mid",
|
||||
budget: str = "mid",
|
||||
verbose: bool = False,
|
||||
) -> HindsightOpenAI:
|
||||
"""Wrap an OpenAI client with Hindsight memory integration.
|
||||
|
|
@ -906,7 +1054,7 @@ def wrap_openai(
|
|||
store_conversations: Whether to store conversations
|
||||
inject_memories: Whether to inject relevant memories
|
||||
max_memories: Maximum number of memories to inject (None = no limit)
|
||||
recall_budget: Budget level for memory recall (low, mid, high)
|
||||
budget: Budget level for memory recall (low, mid, high)
|
||||
verbose: Enable verbose logging
|
||||
|
||||
Returns:
|
||||
|
|
@ -935,7 +1083,7 @@ def wrap_openai(
|
|||
store_conversations=store_conversations,
|
||||
inject_memories=inject_memories,
|
||||
max_memories=max_memories,
|
||||
recall_budget=recall_budget,
|
||||
budget=budget,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
|
|
@ -948,7 +1096,7 @@ def wrap_anthropic(
|
|||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
max_memories: Optional[int] = None,
|
||||
recall_budget: str = "mid",
|
||||
budget: str = "mid",
|
||||
verbose: bool = False,
|
||||
) -> HindsightAnthropic:
|
||||
"""Wrap an Anthropic client with Hindsight memory integration.
|
||||
|
|
@ -965,7 +1113,7 @@ def wrap_anthropic(
|
|||
store_conversations: Whether to store conversations
|
||||
inject_memories: Whether to inject relevant memories
|
||||
max_memories: Maximum number of memories to inject (None = no limit)
|
||||
recall_budget: Budget level for memory recall (low, mid, high)
|
||||
budget: Budget level for memory recall (low, mid, high)
|
||||
verbose: Enable verbose logging
|
||||
|
||||
Returns:
|
||||
|
|
@ -995,6 +1143,6 @@ def wrap_anthropic(
|
|||
store_conversations=store_conversations,
|
||||
inject_memories=inject_memories,
|
||||
max_memories=max_memories,
|
||||
recall_budget=recall_budget,
|
||||
budget=budget,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ from typing import List, Dict, Any
|
|||
|
||||
from hindsight_litellm import (
|
||||
configure,
|
||||
set_defaults,
|
||||
get_defaults,
|
||||
enable,
|
||||
disable,
|
||||
is_enabled,
|
||||
|
|
@ -14,9 +16,10 @@ from hindsight_litellm import (
|
|||
is_configured,
|
||||
reset_config,
|
||||
HindsightConfig,
|
||||
HindsightDefaults,
|
||||
MemoryInjectionMode,
|
||||
)
|
||||
from hindsight_litellm.callbacks import HindsightCallback, get_callback, cleanup_callback
|
||||
from hindsight_litellm.callbacks import HindsightCallback
|
||||
|
||||
|
||||
class TestConfiguration:
|
||||
|
|
@ -34,47 +37,53 @@ class TestConfiguration:
|
|||
def test_configure_creates_config(self):
|
||||
"""Test that configure creates a config object."""
|
||||
config = configure(
|
||||
bank_id="test-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
# Set defaults separately (new API)
|
||||
defaults = set_defaults(bank_id="test-agent")
|
||||
|
||||
assert config is not None
|
||||
assert config.bank_id == "test-agent"
|
||||
assert config.hindsight_api_url == "http://localhost:8888"
|
||||
assert config.enabled is True
|
||||
assert defaults.bank_id == "test-agent"
|
||||
|
||||
def test_configure_with_all_options(self):
|
||||
"""Test configure with all options."""
|
||||
config = configure(
|
||||
hindsight_api_url="http://custom:9999",
|
||||
bank_id="custom-agent",
|
||||
api_key="secret-key",
|
||||
store_conversations=False,
|
||||
inject_memories=False,
|
||||
injection_mode=MemoryInjectionMode.PREPEND_USER,
|
||||
max_memories=5,
|
||||
max_memory_tokens=1000,
|
||||
recall_budget="high",
|
||||
fact_types=["world", "opinion"],
|
||||
document_id="doc-123",
|
||||
enabled=True,
|
||||
excluded_models=["gpt-3.5*"],
|
||||
verbose=True,
|
||||
sync_storage=True,
|
||||
)
|
||||
|
||||
# Set defaults separately (new API)
|
||||
defaults = set_defaults(
|
||||
bank_id="custom-agent",
|
||||
max_memories=5,
|
||||
max_memory_tokens=1000,
|
||||
budget="high",
|
||||
fact_types=["world", "opinion"],
|
||||
document_id="doc-123",
|
||||
)
|
||||
|
||||
assert config.hindsight_api_url == "http://custom:9999"
|
||||
assert config.bank_id == "custom-agent"
|
||||
assert config.api_key == "secret-key"
|
||||
assert config.store_conversations is False
|
||||
assert config.inject_memories is False
|
||||
assert config.injection_mode == MemoryInjectionMode.PREPEND_USER
|
||||
assert config.max_memories == 5
|
||||
assert config.max_memory_tokens == 1000
|
||||
assert config.recall_budget == "high"
|
||||
assert config.fact_types == ["world", "opinion"]
|
||||
assert config.document_id == "doc-123"
|
||||
assert config.excluded_models == ["gpt-3.5*"]
|
||||
assert config.verbose is True
|
||||
assert config.sync_storage is True
|
||||
|
||||
assert defaults.bank_id == "custom-agent"
|
||||
assert defaults.max_memories == 5
|
||||
assert defaults.max_memory_tokens == 1000
|
||||
assert defaults.budget == "high"
|
||||
assert defaults.fact_types == ["world", "opinion"]
|
||||
assert defaults.document_id == "doc-123"
|
||||
|
||||
def test_is_configured_without_bank_id(self):
|
||||
"""Test is_configured returns False without bank_id."""
|
||||
|
|
@ -83,16 +92,19 @@ class TestConfiguration:
|
|||
|
||||
def test_is_configured_with_bank_id(self):
|
||||
"""Test is_configured returns True with bank_id."""
|
||||
configure(bank_id="test-agent")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="test-agent")
|
||||
assert is_configured() is True
|
||||
|
||||
def test_reset_config(self):
|
||||
"""Test reset_config clears the configuration."""
|
||||
configure(bank_id="test-agent")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="test-agent")
|
||||
assert is_configured() is True
|
||||
|
||||
reset_config()
|
||||
assert get_config() is None
|
||||
assert get_defaults() is None
|
||||
assert is_configured() is False
|
||||
|
||||
|
||||
|
|
@ -112,44 +124,42 @@ class TestEnableDisable:
|
|||
with pytest.raises(RuntimeError, match="not configured"):
|
||||
enable()
|
||||
|
||||
def test_enable_registers_callback(self):
|
||||
"""Test enable registers callback with LiteLLM."""
|
||||
import litellm
|
||||
def test_enable_without_bank_id_raises(self):
|
||||
"""Test enable raises error without bank_id."""
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
with pytest.raises(RuntimeError, match="bank_id not set"):
|
||||
enable()
|
||||
|
||||
configure(bank_id="test-agent")
|
||||
def test_enable_sets_enabled_flag(self):
|
||||
"""Test enable sets the enabled flag."""
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="test-agent")
|
||||
enable()
|
||||
|
||||
callback = get_callback()
|
||||
assert callback in litellm.callbacks
|
||||
assert is_enabled() is True
|
||||
|
||||
def test_disable_removes_callback(self):
|
||||
"""Test disable removes callback from LiteLLM."""
|
||||
import litellm
|
||||
|
||||
configure(bank_id="test-agent")
|
||||
def test_disable_clears_enabled_flag(self):
|
||||
"""Test disable clears the enabled flag."""
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="test-agent")
|
||||
enable()
|
||||
assert is_enabled() is True
|
||||
|
||||
disable()
|
||||
callback = get_callback()
|
||||
assert callback not in litellm.callbacks
|
||||
assert is_enabled() is False
|
||||
|
||||
def test_enable_idempotent(self):
|
||||
"""Test enable is idempotent (can be called multiple times)."""
|
||||
import litellm
|
||||
|
||||
configure(bank_id="test-agent")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="test-agent")
|
||||
|
||||
# Enable multiple times
|
||||
enable()
|
||||
enable()
|
||||
enable()
|
||||
|
||||
# Should only have one callback
|
||||
callback = get_callback()
|
||||
assert litellm.callbacks.count(callback) == 1
|
||||
# Should still be enabled
|
||||
assert is_enabled() is True
|
||||
|
||||
|
||||
class TestCallback:
|
||||
|
|
@ -221,14 +231,21 @@ class TestCallback:
|
|||
def test_format_memories(self):
|
||||
"""Test formatting memories into context string."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(bank_id="test", max_memories=10, verbose=False)
|
||||
|
||||
# Create config and defaults with new API
|
||||
configure(hindsight_api_url="http://localhost:8888", verbose=False)
|
||||
set_defaults(bank_id="test", max_memories=10)
|
||||
|
||||
config = get_config()
|
||||
defaults = get_defaults()
|
||||
|
||||
memories = [
|
||||
{"text": "User likes Python", "fact_type": "world", "weight": 0.95},
|
||||
{"text": "User works at Google", "fact_type": "world", "weight": 0.8},
|
||||
]
|
||||
|
||||
formatted = callback._format_memories(memories, config)
|
||||
# Signature is: _format_memories(results, settings, config)
|
||||
formatted = callback._format_memories(memories, defaults, config)
|
||||
|
||||
assert "Relevant Memories" in formatted
|
||||
assert "User likes Python" in formatted
|
||||
|
|
@ -238,23 +255,34 @@ class TestCallback:
|
|||
def test_format_memories_with_verbose(self):
|
||||
"""Test formatting memories with verbose mode shows weights."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(bank_id="test", max_memories=10, verbose=True)
|
||||
|
||||
# Create config and defaults with new API
|
||||
configure(hindsight_api_url="http://localhost:8888", verbose=True)
|
||||
set_defaults(bank_id="test", max_memories=10)
|
||||
|
||||
config = get_config()
|
||||
defaults = get_defaults()
|
||||
|
||||
memories = [
|
||||
{"text": "User likes Python", "fact_type": "world", "weight": 0.95},
|
||||
]
|
||||
|
||||
formatted = callback._format_memories(memories, config)
|
||||
# Signature is: _format_memories(results, settings, config)
|
||||
formatted = callback._format_memories(memories, defaults, config)
|
||||
|
||||
assert "relevance: 0.95" in formatted
|
||||
|
||||
def test_inject_memories_as_system_message(self):
|
||||
"""Test injecting memories as system message."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(
|
||||
bank_id="test",
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
injection_mode=MemoryInjectionMode.SYSTEM_MESSAGE,
|
||||
)
|
||||
set_defaults(bank_id="test")
|
||||
|
||||
config = get_config()
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
|
|
@ -271,10 +299,14 @@ class TestCallback:
|
|||
def test_inject_memories_prepend_to_existing_system(self):
|
||||
"""Test injecting memories appends to existing system message."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(
|
||||
bank_id="test",
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
injection_mode=MemoryInjectionMode.SYSTEM_MESSAGE,
|
||||
)
|
||||
set_defaults(bank_id="test")
|
||||
|
||||
config = get_config()
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
|
|
@ -292,10 +324,14 @@ class TestCallback:
|
|||
def test_inject_memories_prepend_user_mode(self):
|
||||
"""Test injecting memories in prepend_user mode."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(
|
||||
bank_id="test",
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
injection_mode=MemoryInjectionMode.PREPEND_USER,
|
||||
)
|
||||
set_defaults(bank_id="test")
|
||||
|
||||
config = get_config()
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "What's my name?"},
|
||||
|
|
@ -312,10 +348,14 @@ class TestCallback:
|
|||
def test_should_skip_model_exact_match(self):
|
||||
"""Test model exclusion with exact match."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(
|
||||
bank_id="test",
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
excluded_models=["gpt-3.5-turbo"],
|
||||
)
|
||||
set_defaults(bank_id="test")
|
||||
|
||||
config = get_config()
|
||||
|
||||
assert callback._should_skip_model("gpt-3.5-turbo", config) is True
|
||||
assert callback._should_skip_model("gpt-4", config) is False
|
||||
|
|
@ -323,10 +363,14 @@ class TestCallback:
|
|||
def test_should_skip_model_wildcard(self):
|
||||
"""Test model exclusion with wildcard pattern."""
|
||||
callback = HindsightCallback()
|
||||
config = HindsightConfig(
|
||||
bank_id="test",
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
excluded_models=["gpt-3.5*", "claude-instant-*"],
|
||||
)
|
||||
set_defaults(bank_id="test")
|
||||
|
||||
config = get_config()
|
||||
|
||||
assert callback._should_skip_model("gpt-3.5-turbo", config) is True
|
||||
assert callback._should_skip_model("gpt-3.5-turbo-16k", config) is True
|
||||
|
|
@ -414,7 +458,8 @@ class TestContextManager:
|
|||
|
||||
with hindsight_memory(bank_id="test-agent"):
|
||||
assert is_enabled() is True
|
||||
assert get_config().bank_id == "test-agent"
|
||||
defaults = get_defaults()
|
||||
assert defaults.bank_id == "test-agent"
|
||||
|
||||
assert is_enabled() is False
|
||||
|
||||
|
|
@ -423,16 +468,17 @@ class TestContextManager:
|
|||
from hindsight_litellm import hindsight_memory
|
||||
|
||||
# Set up initial config
|
||||
configure(bank_id="original-agent")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
set_defaults(bank_id="original-agent")
|
||||
enable()
|
||||
assert get_config().bank_id == "original-agent"
|
||||
assert get_defaults().bank_id == "original-agent"
|
||||
|
||||
# Use context manager with different config
|
||||
with hindsight_memory(bank_id="temporary-agent"):
|
||||
assert get_config().bank_id == "temporary-agent"
|
||||
assert get_defaults().bank_id == "temporary-agent"
|
||||
|
||||
# Should restore original config
|
||||
assert get_config().bank_id == "original-agent"
|
||||
assert get_defaults().bank_id == "original-agent"
|
||||
assert is_enabled() is True
|
||||
|
||||
def test_context_manager_with_fact_types(self):
|
||||
|
|
@ -440,8 +486,8 @@ class TestContextManager:
|
|||
from hindsight_litellm import hindsight_memory
|
||||
|
||||
with hindsight_memory(bank_id="test-agent", fact_types=["world", "opinion"]):
|
||||
config = get_config()
|
||||
assert config.fact_types == ["world", "opinion"]
|
||||
defaults = get_defaults()
|
||||
assert defaults.fact_types == ["world", "opinion"]
|
||||
|
||||
|
||||
class TestFactTypes:
|
||||
|
|
@ -457,15 +503,75 @@ class TestFactTypes:
|
|||
|
||||
def test_configure_with_fact_types(self):
|
||||
"""Test configuring with fact_types."""
|
||||
config = configure(
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
defaults = set_defaults(
|
||||
bank_id="test-agent",
|
||||
fact_types=["world", "agent", "opinion"],
|
||||
)
|
||||
|
||||
assert config.fact_types == ["world", "agent", "opinion"]
|
||||
assert defaults.fact_types == ["world", "agent", "opinion"]
|
||||
|
||||
def test_configure_without_fact_types(self):
|
||||
"""Test configuring without fact_types defaults to None."""
|
||||
config = configure(bank_id="test-agent")
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
defaults = set_defaults(bank_id="test-agent")
|
||||
|
||||
assert config.fact_types is None
|
||||
assert defaults.fact_types is None
|
||||
|
||||
class TestSetDefaults:
|
||||
"""Tests for set_defaults functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Reset config before each test."""
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Clean up after each test."""
|
||||
cleanup()
|
||||
|
||||
def test_set_defaults_creates_defaults(self):
|
||||
"""Test set_defaults creates a defaults object."""
|
||||
defaults = set_defaults(bank_id="test-agent")
|
||||
|
||||
assert defaults is not None
|
||||
assert defaults.bank_id == "test-agent"
|
||||
|
||||
def test_set_defaults_with_all_options(self):
|
||||
"""Test set_defaults with all options."""
|
||||
defaults = set_defaults(
|
||||
bank_id="test-agent",
|
||||
document_id="doc-123",
|
||||
budget="high",
|
||||
fact_types=["world", "opinion"],
|
||||
max_memories=10,
|
||||
max_memory_tokens=2048,
|
||||
use_reflect=True,
|
||||
reflect_include_facts=True,
|
||||
reflect_context="I am a helpful assistant.",
|
||||
include_entities=False,
|
||||
trace=True,
|
||||
)
|
||||
|
||||
assert defaults.bank_id == "test-agent"
|
||||
assert defaults.document_id == "doc-123"
|
||||
assert defaults.budget == "high"
|
||||
assert defaults.fact_types == ["world", "opinion"]
|
||||
assert defaults.max_memories == 10
|
||||
assert defaults.max_memory_tokens == 2048
|
||||
assert defaults.use_reflect is True
|
||||
assert defaults.reflect_include_facts is True
|
||||
assert defaults.reflect_context == "I am a helpful assistant."
|
||||
assert defaults.include_entities is False
|
||||
assert defaults.trace is True
|
||||
|
||||
def test_set_defaults_updates_existing(self):
|
||||
"""Test set_defaults updates existing defaults."""
|
||||
set_defaults(bank_id="first-agent", budget="low")
|
||||
defaults = set_defaults(budget="high") # Only update budget
|
||||
|
||||
assert defaults.bank_id == "first-agent" # Preserved
|
||||
assert defaults.budget == "high" # Updated
|
||||
|
||||
def test_get_defaults_returns_none_initially(self):
|
||||
"""Test get_defaults returns None when not set."""
|
||||
assert get_defaults() is None
|
||||
|
|
|
|||
Loading…
Reference in a new issue