Compare commits
1
Commits
ma
...
support-tags
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ea00bf5ee |
@@ -1,5 +0,0 @@
|
||||
"""Hindsight-LangMem: LangGraph langmem drop-in replacement using Hindsight."""
|
||||
|
||||
from hindsight_langmem.store import HindsightStore
|
||||
|
||||
__all__ = ["HindsightStore"]
|
||||
@@ -1,282 +0,0 @@
|
||||
"""Hindsight implementation of LangGraph BaseStore interface."""
|
||||
|
||||
import json
|
||||
from typing import Any, Iterable
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
GetOp,
|
||||
Item,
|
||||
ListNamespacesOp,
|
||||
Op,
|
||||
PutOp,
|
||||
Result,
|
||||
SearchItem,
|
||||
SearchOp,
|
||||
)
|
||||
|
||||
|
||||
class HindsightStore(BaseStore):
|
||||
"""
|
||||
Hindsight implementation of LangGraph BaseStore.
|
||||
|
||||
This store uses Hindsight's memory system as a backend for LangGraph's memory storage.
|
||||
Each namespace maps to a Hindsight agent, and items are stored as memory units.
|
||||
|
||||
Args:
|
||||
base_url: The base URL of the Hindsight API server
|
||||
default_agent_id: Default agent ID to use when namespace is empty (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str, default_agent_id: str | None = None):
|
||||
"""Initialize the Hindsight store.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for the Hindsight API
|
||||
default_agent_id: Default agent ID when namespace is empty
|
||||
"""
|
||||
super().__init__()
|
||||
self.client = Hindsight(base_url=base_url)
|
||||
self.default_agent_id = default_agent_id or "default"
|
||||
self._ensure_agent_exists(self.default_agent_id)
|
||||
|
||||
def _namespace_to_agent_id(self, namespace: tuple[str, ...]) -> str:
|
||||
"""Convert namespace to agent ID."""
|
||||
if not namespace:
|
||||
return self.default_agent_id
|
||||
return "__".join(namespace)
|
||||
|
||||
def _ensure_agent_exists(self, agent_id: str) -> None:
|
||||
"""Ensure an agent exists, create if it doesn't."""
|
||||
try:
|
||||
# Try to create agent (idempotent operation)
|
||||
self.client.create_agent(agent_id=agent_id)
|
||||
except Exception:
|
||||
# Agent likely already exists
|
||||
pass
|
||||
|
||||
def _serialize_value(self, value: dict[str, Any]) -> str:
|
||||
"""Serialize a value to JSON string."""
|
||||
return json.dumps(value, sort_keys=True)
|
||||
|
||||
def _deserialize_value(self, content: str) -> dict[str, Any]:
|
||||
"""Deserialize JSON string back to value."""
|
||||
try:
|
||||
return json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return {"content": content}
|
||||
|
||||
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
"""Execute a batch of operations synchronously."""
|
||||
results: list[Result] = []
|
||||
for op in ops:
|
||||
if isinstance(op, PutOp):
|
||||
results.append(self._put(op))
|
||||
elif isinstance(op, GetOp):
|
||||
results.append(self._get(op))
|
||||
elif isinstance(op, SearchOp):
|
||||
results.append(self._search(op))
|
||||
elif isinstance(op, ListNamespacesOp):
|
||||
results.append(self._list_namespaces(op))
|
||||
else:
|
||||
results.append(None)
|
||||
return results
|
||||
|
||||
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
"""Execute a batch of operations asynchronously."""
|
||||
return self.batch(ops)
|
||||
|
||||
def _put(self, op: PutOp) -> None:
|
||||
"""Store an item."""
|
||||
agent_id = self._namespace_to_agent_id(op.namespace)
|
||||
self._ensure_agent_exists(agent_id)
|
||||
|
||||
value_with_key = {"__key__": op.key, **op.value}
|
||||
content = self._serialize_value(value_with_key)
|
||||
|
||||
self.client.put(
|
||||
agent_id=agent_id,
|
||||
content=content,
|
||||
context=f"key:{op.key}",
|
||||
document_id=op.key,
|
||||
)
|
||||
return None
|
||||
|
||||
def _get(self, op: GetOp) -> Item | None:
|
||||
"""Retrieve an item by namespace and key."""
|
||||
agent_id = self._namespace_to_agent_id(op.namespace)
|
||||
|
||||
try:
|
||||
response = self.client.get_document(agent_id=agent_id, document_id=op.key)
|
||||
|
||||
if not response or not response.get("original_text"):
|
||||
return None
|
||||
|
||||
# Parse the original text to get the value
|
||||
value = self._deserialize_value(response["original_text"])
|
||||
stored_key = value.pop("__key__", op.key)
|
||||
|
||||
if stored_key != op.key:
|
||||
return None
|
||||
|
||||
return Item(
|
||||
namespace=op.namespace,
|
||||
key=op.key,
|
||||
value=value,
|
||||
created_at=response.get("created_at"),
|
||||
updated_at=response.get("updated_at"),
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _search(self, op: SearchOp) -> list[SearchItem]:
|
||||
"""Search for items within a namespace prefix."""
|
||||
agent_id = self._namespace_to_agent_id(op.namespace_prefix)
|
||||
|
||||
try:
|
||||
results = self.client.search(
|
||||
agent_id=agent_id,
|
||||
query=op.query or "",
|
||||
max_tokens=op.limit * 100,
|
||||
)
|
||||
|
||||
if not results:
|
||||
return []
|
||||
|
||||
items: list[SearchItem] = []
|
||||
seen_keys = set()
|
||||
|
||||
for result in results[op.offset : op.offset + op.limit]:
|
||||
try:
|
||||
text = result.get("text", "")
|
||||
value = self._deserialize_value(text)
|
||||
key = value.pop("__key__", result.get("id"))
|
||||
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
|
||||
items.append(
|
||||
SearchItem(
|
||||
namespace=op.namespace_prefix,
|
||||
key=key,
|
||||
value=value,
|
||||
score=1.0,
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
)
|
||||
|
||||
if len(items) >= op.limit:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return items
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
|
||||
"""List all namespaces."""
|
||||
# Not fully implemented - would need to list all agents
|
||||
return []
|
||||
|
||||
def _matches_prefix(self, namespace: tuple[str, ...], prefix: tuple[str, ...]) -> bool:
|
||||
"""Check if namespace matches prefix."""
|
||||
if len(namespace) < len(prefix):
|
||||
return False
|
||||
return namespace[: len(prefix)] == prefix
|
||||
|
||||
def _matches_suffix(self, namespace: tuple[str, ...], suffix: tuple[str, ...]) -> bool:
|
||||
"""Check if namespace matches suffix."""
|
||||
if len(namespace) < len(suffix):
|
||||
return False
|
||||
return namespace[-len(suffix) :] == suffix
|
||||
|
||||
def put(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: bool | list[str] | None = None,
|
||||
) -> None:
|
||||
"""Store a single item."""
|
||||
self._put(PutOp(namespace=namespace, key=key, value=value))
|
||||
|
||||
async def aput(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: bool | list[str] | None = None,
|
||||
) -> None:
|
||||
"""Store a single item asynchronously."""
|
||||
self.put(namespace, key, value, index)
|
||||
|
||||
def get(self, namespace: tuple[str, ...], key: str) -> Item | None:
|
||||
"""Retrieve a single item."""
|
||||
return self._get(GetOp(namespace=namespace, key=key))
|
||||
|
||||
async def aget(self, namespace: tuple[str, ...], key: str) -> Item | None:
|
||||
"""Retrieve a single item asynchronously."""
|
||||
return self.get(namespace, key)
|
||||
|
||||
def delete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item by deleting the document."""
|
||||
agent_id = self._namespace_to_agent_id(namespace)
|
||||
|
||||
try:
|
||||
self.client.delete_document(agent_id=agent_id, document_id=key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item asynchronously."""
|
||||
self.delete(namespace, key)
|
||||
|
||||
def search(
|
||||
self,
|
||||
namespace_prefix: tuple[str, ...],
|
||||
query: str | None = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
"""Search for items."""
|
||||
return self._search(SearchOp(namespace_prefix=namespace_prefix, query=query, limit=limit, offset=offset))
|
||||
|
||||
async def asearch(
|
||||
self,
|
||||
namespace_prefix: tuple[str, ...],
|
||||
query: str | None = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
"""Search for items asynchronously."""
|
||||
return self.search(namespace_prefix, query, filter, limit, offset)
|
||||
|
||||
def list_namespaces(
|
||||
self,
|
||||
prefix: tuple[str, ...] | None = None,
|
||||
suffix: tuple[str, ...] | None = None,
|
||||
max_depth: int | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
"""List all namespaces."""
|
||||
return self._list_namespaces(
|
||||
ListNamespacesOp(prefix=prefix, suffix=suffix, max_depth=max_depth, limit=limit, offset=offset)
|
||||
)
|
||||
|
||||
async def alist_namespaces(
|
||||
self,
|
||||
prefix: tuple[str, ...] | None = None,
|
||||
suffix: tuple[str, ...] | None = None,
|
||||
max_depth: int | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
"""List all namespaces asynchronously."""
|
||||
return self.list_namespaces(prefix, suffix, max_depth, limit, offset)
|
||||
@@ -1,37 +0,0 @@
|
||||
[project]
|
||||
name = "hindsight-langmem"
|
||||
version = "0.0.1"
|
||||
description = "LangGraph langmem drop-in replacement using Hindsight memory system"
|
||||
authors = [
|
||||
{name = "Hindsight Team"}
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langgraph>=0.2.0",
|
||||
"hindsight-client",
|
||||
# Transitive dependency security fixes
|
||||
"aiohttp>=3.13.3", # Multiple DoS vulnerabilities
|
||||
"langchain-core>=1.2.5", # Serialization injection vulnerability
|
||||
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-client = { path = "../../hindsight-clients/python" }
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest>=7.0.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_langmem"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["F", "I", "UP"]
|
||||
@@ -1 +0,0 @@
|
||||
"""Tests for hindsight-langmem package."""
|
||||
@@ -1,119 +0,0 @@
|
||||
"""Tests for HindsightStore implementation."""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_langmem import HindsightStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store():
|
||||
"""Create a HindsightStore instance for testing."""
|
||||
base_url = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
return HindsightStore(base_url=base_url, default_agent_id=f"test_agent_{int(time.time())}")
|
||||
|
||||
|
||||
def test_put_and_get(store):
|
||||
"""Test storing and retrieving an item."""
|
||||
namespace = ("test", "namespace")
|
||||
key = "test_key"
|
||||
value = {"data": "test_value", "number": 42}
|
||||
|
||||
store.put(namespace, key, value)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
retrieved = store.get(namespace, key)
|
||||
assert retrieved is not None
|
||||
assert retrieved.key == key
|
||||
assert retrieved.namespace == namespace
|
||||
assert retrieved.value == value
|
||||
|
||||
|
||||
def test_search(store):
|
||||
"""Test searching for items."""
|
||||
namespace = ("search", "test")
|
||||
key1 = "item1"
|
||||
value1 = {"content": "This is about machine learning", "type": "note"}
|
||||
|
||||
key2 = "item2"
|
||||
value2 = {"content": "This is about deep learning and neural networks", "type": "article"}
|
||||
|
||||
store.put(namespace, key1, value1)
|
||||
store.put(namespace, key2, value2)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
results = store.search(namespace, query="machine learning", limit=5)
|
||||
|
||||
assert len(results) > 0
|
||||
assert any(r.key in [key1, key2] for r in results)
|
||||
|
||||
|
||||
def test_delete(store):
|
||||
"""Test deleting an item."""
|
||||
namespace = ("delete", "test")
|
||||
key = "to_delete"
|
||||
value = {"data": "temporary"}
|
||||
|
||||
store.put(namespace, key, value)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
retrieved = store.get(namespace, key)
|
||||
assert retrieved is not None
|
||||
|
||||
store.delete(namespace, key)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
retrieved_after_delete = store.get(namespace, key)
|
||||
assert retrieved_after_delete is None
|
||||
|
||||
|
||||
def test_list_namespaces(store):
|
||||
"""Test listing namespaces."""
|
||||
namespace1 = ("list", "test", "one")
|
||||
namespace2 = ("list", "test", "two")
|
||||
|
||||
store.put(namespace1, "key1", {"data": "value1"})
|
||||
store.put(namespace2, "key2", {"data": "value2"})
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
namespaces = store.list_namespaces(prefix=("list",))
|
||||
|
||||
assert len(namespaces) >= 2
|
||||
assert namespace1 in namespaces
|
||||
assert namespace2 in namespaces
|
||||
|
||||
|
||||
def test_batch_operations(store):
|
||||
"""Test batch operations."""
|
||||
from langgraph.store.base import GetOp, PutOp
|
||||
|
||||
namespace = ("batch", "test")
|
||||
|
||||
ops = [
|
||||
PutOp(namespace=namespace, key="key1", value={"data": "value1"}),
|
||||
PutOp(namespace=namespace, key="key2", value={"data": "value2"}),
|
||||
]
|
||||
|
||||
results = store.batch(ops)
|
||||
assert len(results) == 2
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
get_ops = [
|
||||
GetOp(namespace=namespace, key="key1"),
|
||||
GetOp(namespace=namespace, key="key2"),
|
||||
]
|
||||
|
||||
get_results = store.batch(get_ops)
|
||||
assert len(get_results) == 2
|
||||
assert get_results[0] is not None
|
||||
assert get_results[0].value == {"data": "value1"}
|
||||
assert get_results[1] is not None
|
||||
assert get_results[1].value == {"data": "value2"}
|
||||
@@ -1,163 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "# Hindsight-LangMem: Drop-in Semantic Memory for LangGraph\n\nReplace your LangGraph memory store in one line and get advanced semantic capabilities."
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## What is Hindsight-LangMem?\n\n`hindsight-langmem` implements LangGraph's `BaseStore` interface using Hindsight as the backend.\n\n### What You Get vs Standard LangGraph Memory\n\n| Feature | Standard Memory | Hindsight-LangMem |\n|---------|-----------------|-------------------|\n| Basic Key-Value Storage | ✅ | ✅ |\n| Semantic Search | ✅ Basic | ✅ **Enhanced with spreading activation** |\n| Namespace Support | ✅ | ✅ |\n| **Personality-Driven Retrieval** | ❌ | ✅ |\n| **Automatic Fact Extraction** | ❌ | ✅ |\n| **Entity Recognition** | ❌ | ✅ |\n| **Temporal Reasoning** | ❌ | ✅ |\n| **Opinion Formation** | ❌ | ✅ |\n| **Background Knowledge** | ❌ | ✅ |\n| **Thinking/Reasoning API** | ❌ | ✅ |\n\n### When to Use\n- Conversational agents needing long-term memory\n- Personalized AI with context-aware responses \n- Multi-agent systems with distinct personalities\n- Knowledge management with semantic search"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": "## Installation\n\nRun this cell to install dependencies:",
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": "!pip install langgraph langmem",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": "Make sure Hindsight API is running at `http://localhost:8888`",
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": "## Setup API Keys\n\nSet up your OpenAI API key and Hindsight URL:",
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": "import os\nimport getpass\n\n# Set OpenAI API key\nif \"OPENAI_API_KEY\" not in os.environ:\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"Enter your OpenAI API key: \")\n\n# Set Hindsight API URL\nif \"HINDSIGHT_API_URL\" not in os.environ:\n os.environ[\"HINDSIGHT_API_URL\"] = input(\"Enter Hindsight API URL (default: http://localhost:8888): \") or \"http://localhost:8888\"\n\nprint(\"✅ API keys configured\")",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## The Drop-in Replacement\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": "### Before: Standard LangGraph Memory",
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": "from langmem import create_manage_memory_tool, create_search_memory_tool\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.store.memory import InMemoryStore\n\n# Standard store - basic key-value with optional vector search\nstore = InMemoryStore()\n\nagent = create_react_agent(\n \"openai:gpt-4o\",\n tools=[\n create_manage_memory_tool(namespace=(\"memories\",)),\n create_search_memory_tool(namespace=(\"memories\",)),\n ],\n store=store\n)",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": "### After: With Hindsight-LangMem\n\n**Just change one line!**",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": "import os\nfrom langmem import create_manage_memory_tool, create_search_memory_tool\nfrom langgraph.prebuilt import create_react_agent\nfrom hindsight_langmem import HindsightStore # ← Only import change!\n\n# Replace InMemoryStore with HindsightStore\nbase_url = os.getenv(\"HINDSIGHT_API_URL\", \"http://localhost:8888\")\nstore = HindsightStore(base_url=base_url, default_agent_id=\"my_agent\") # ← One line change!\n\n# Everything else stays exactly the same\nagent = create_react_agent(\n \"openai:gpt-4o\", # ← Use OpenAI\n tools=[\n create_manage_memory_tool(namespace=(\"memories\",)),\n create_search_memory_tool(namespace=(\"memories\",)),\n ],\n store=store # ← Now using Hindsight with enhanced capabilities!\n)\n\nprint(\"✅ Agent created with Hindsight-powered memory\")",
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "import os\nfrom langmem import create_manage_memory_tool, create_search_memory_tool\nfrom langgraph.prebuilt import create_react_agent\nfrom hindsight_langmem import HindsightStore # ← Only import change!\n\n# Replace InMemoryStore with HindsightStore\nbase_url = os.getenv(\"HINDSIGHT_API_URL\", \"http://localhost:8888\")\nstore = HindsightStore(base_url=base_url, default_agent_id=\"my_agent\") # ← One line change!\n\n# Everything else stays exactly the same\nagent = create_react_agent(\n \"anthropic:claude-3-5-sonnet-latest\",\n tools=[\n create_manage_memory_tool(namespace=(\"memories\",)),\n create_search_memory_tool(namespace=(\"memories\",)),\n ],\n store=store # ← Now using Hindsight with enhanced capabilities!\n)\n\nprint(\"✅ Agent created with Hindsight-powered memory\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example: Conversational Memory in Action"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"\n",
|
||||
"# Store information\n",
|
||||
"result1 = agent.invoke({\n",
|
||||
" \"messages\": [{\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"\"\"Remember: I'm David, a software engineer working on AI projects. \n",
|
||||
" I love Python and machine learning. Currently building a chatbot with LangGraph.\"\"\"\n",
|
||||
" }]\n",
|
||||
"})\n",
|
||||
"print(\"Agent:\", result1[\"messages\"][-1].content)\n",
|
||||
"\n",
|
||||
"time.sleep(2)\n",
|
||||
"\n",
|
||||
"# Recall information\n",
|
||||
"result2 = agent.invoke({\n",
|
||||
" \"messages\": [{\"role\": \"user\", \"content\": \"What do you remember about me?\"}]\n",
|
||||
"})\n",
|
||||
"print(\"\\nAgent:\", result2[\"messages\"][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## What Happens Behind the Scenes\n\nWhen your agent stores memories with Hindsight, automatically:\n\n1. **Fact Extraction**: Natural language → structured facts\n2. **Entity Recognition**: Identifies people, places, concepts\n3. **Semantic Indexing**: Spreading activation for better retrieval\n4. **Temporal Awareness**: Event dates tracked for time queries\n5. **Opinion Formation**: Agent develops perspectives over time\n6. **Personality Influence**: Memory retrieval shaped by personality traits\n\n**You use the standard LangGraph API - Hindsight does the rest!**"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {},
|
||||
"source": "# Different agents with different personalities\ncreative_store = HindsightStore(base_url=base_url, default_agent_id=\"creative_writer\")\nanalyst_store = HindsightStore(base_url=base_url, default_agent_id=\"data_analyst\")\n\ncreative_agent = create_react_agent(\n \"openai:gpt-4o\",\n tools=[\n create_manage_memory_tool(namespace=(\"creative\",)),\n create_search_memory_tool(namespace=(\"creative\",))\n ],\n store=creative_store\n)\n\nanalyst_agent = create_react_agent(\n \"openai:gpt-4o\",\n tools=[\n create_manage_memory_tool(namespace=(\"analysis\",)),\n create_search_memory_tool(namespace=(\"analysis\",))\n ],\n store=analyst_store\n)\n\nprint(\"✅ Two agents with isolated memories and distinct personalities\")",
|
||||
"execution_count": null,
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "# Different agents with different personalities\ncreative_store = HindsightStore(base_url=base_url, default_agent_id=\"creative_writer\")\nanalyst_store = HindsightStore(base_url=base_url, default_agent_id=\"data_analyst\")\n\ncreative_agent = create_react_agent(\n \"anthropic:claude-3-5-sonnet-latest\",\n tools=[\n create_manage_memory_tool(namespace=(\"creative\",)),\n create_search_memory_tool(namespace=(\"creative\",))\n ],\n store=creative_store\n)\n\nanalyst_agent = create_react_agent(\n \"anthropic:claude-3-5-sonnet-latest\",\n tools=[\n create_manage_memory_tool(namespace=(\"analysis\",)),\n create_search_memory_tool(namespace=(\"analysis\",))\n ],\n store=analyst_store\n)\n\nprint(\"✅ Two agents with isolated memories and distinct personalities\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Summary\n\n### The Change\n```python\n# Before\nstore = InMemoryStore()\n\n# After \nstore = HindsightStore(base_url=\"http://localhost:8888\", default_agent_id=\"my_agent\")\n```\n\n### What You Get\n- ✅ Semantic search with spreading activation\n- ✅ Automatic fact extraction from conversations\n- ✅ Entity recognition and linking\n- ✅ Temporal reasoning (time-aware queries)\n- ✅ Personality-driven memory retrieval\n- ✅ Opinion formation over time\n- ✅ Multi-agent support with isolated memories\n\n**Same LangGraph API. Smarter memory. Zero code changes (except the store line).**"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Generated
-1801
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ Features:
|
||||
- Works with any LiteLLM-supported provider
|
||||
- Multi-user support via separate bank_ids
|
||||
- Per-call overrides via hindsight_* kwargs
|
||||
- Document grouping for conversation threading
|
||||
- Session grouping for conversation threading
|
||||
- Direct recall API for manual memory queries
|
||||
- Native client wrappers for OpenAI and Anthropic
|
||||
- STRICT ERROR HANDLING: Raises HindsightError on any memory operation failure
|
||||
@@ -22,41 +22,30 @@ Error Handling:
|
||||
a HindsightError will be raised and propagate to your code.
|
||||
|
||||
API Structure:
|
||||
1. configure() - Static settings (rarely change during session)
|
||||
- hindsight_api_url, api_key, verbose
|
||||
- injection_mode, excluded_models, store_conversations, inject_memories
|
||||
1. configure() - All settings in one place
|
||||
- Connection: hindsight_api_url, api_key
|
||||
- Bank setup: mission, bank_name
|
||||
- Behavior: verbose, sync_storage, excluded_models
|
||||
- Per-call defaults: bank_id, session_id, budget, etc.
|
||||
|
||||
2. set_defaults() - Default values for per-call settings (required: bank_id)
|
||||
- bank_id (REQUIRED), document_id, budget, fact_types
|
||||
- max_memories, max_memory_tokens, use_reflect, reflect_include_facts
|
||||
- include_entities (default True), trace (default False)
|
||||
2. set_defaults() - Update per-call defaults after initial configuration
|
||||
- bank_id, session_id, budget, fact_types, etc.
|
||||
|
||||
3. Per-call kwargs (hindsight_* prefix) - Override any default per-call
|
||||
- hindsight_bank_id, hindsight_document_id, hindsight_budget, etc.
|
||||
- hindsight_include_entities, hindsight_trace
|
||||
|
||||
4. set_bank_mission() - Set mission/instructions for a bank (for mental models)
|
||||
- Can be called anytime, bank is auto-created if needed
|
||||
- set_bank_background() is deprecated, use set_bank_mission() instead
|
||||
- hindsight_bank_id, hindsight_session_id, hindsight_budget, etc.
|
||||
|
||||
Basic usage:
|
||||
>>> import hindsight_litellm
|
||||
>>> from hindsight_litellm import HindsightError
|
||||
>>>
|
||||
>>> # Configure static settings
|
||||
>>> # Configure everything in one call
|
||||
>>> hindsight_litellm.configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... bank_id="user-123",
|
||||
... mission="Remember customer preferences and past interactions.",
|
||||
... verbose=True,
|
||||
... )
|
||||
>>>
|
||||
>>> # Set defaults (bank_id is required)
|
||||
>>> hindsight_litellm.set_defaults(bank_id="user-123")
|
||||
>>>
|
||||
>>> # Optionally set bank mission (for mental models)
|
||||
>>> hindsight_litellm.set_bank_mission(
|
||||
... mission="This agent helps with customer support. Remember customer preferences."
|
||||
... )
|
||||
>>>
|
||||
>>> # Enable memory integration
|
||||
>>> hindsight_litellm.enable()
|
||||
>>>
|
||||
@@ -74,7 +63,7 @@ Basic usage:
|
||||
... model="gpt-4",
|
||||
... messages=[...],
|
||||
... hindsight_bank_id="different-bank", # Override default bank_id
|
||||
... hindsight_document_id="conv-123", # Set document_id for this call
|
||||
... hindsight_session_id="conv-123", # Set session for this call
|
||||
... )
|
||||
|
||||
Direct recall API:
|
||||
@@ -112,7 +101,7 @@ Works with any LiteLLM-supported provider:
|
||||
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Any
|
||||
from typing import Optional, List
|
||||
import threading
|
||||
import logging
|
||||
|
||||
@@ -121,12 +110,10 @@ import litellm
|
||||
from .config import (
|
||||
configure,
|
||||
set_defaults,
|
||||
set_bank_mission,
|
||||
get_config,
|
||||
get_defaults,
|
||||
is_configured,
|
||||
reset_config,
|
||||
set_document_id,
|
||||
HindsightConfig,
|
||||
HindsightDefaults,
|
||||
MemoryInjectionMode,
|
||||
@@ -190,12 +177,15 @@ class InjectionDebugInfo:
|
||||
injected: Whether memories were actually injected into the prompt
|
||||
error: Error message if injection failed (None on success)
|
||||
"""
|
||||
|
||||
mode: str # "reflect" or "recall"
|
||||
query: str
|
||||
bank_id: str
|
||||
memory_context: str # The formatted context that was injected
|
||||
reflect_text: Optional[str] = None # Raw reflect response text
|
||||
reflect_facts: Optional[List[dict]] = None # Facts used by reflect (when reflect_include_facts=True)
|
||||
reflect_facts: Optional[List[dict]] = (
|
||||
None # Facts used by reflect (when reflect_include_facts=True)
|
||||
)
|
||||
recall_results: Optional[List[dict]] = None # Raw recall results
|
||||
results_count: int = 0
|
||||
injected: bool = False
|
||||
@@ -234,7 +224,11 @@ def clear_injection_debug() -> None:
|
||||
_last_injection_debug = None
|
||||
|
||||
|
||||
def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, custom_reflect_context: Optional[str] = None) -> List[dict]:
|
||||
def _inject_memories(
|
||||
messages: List[dict],
|
||||
custom_query: Optional[str] = None,
|
||||
custom_reflect_context: Optional[str] = None,
|
||||
) -> List[dict]:
|
||||
"""Inject memories into messages list.
|
||||
|
||||
Returns the modified messages list with memories injected into the system message.
|
||||
@@ -310,28 +304,39 @@ def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, c
|
||||
# Add response_schema for structured output
|
||||
if defaults.reflect_response_schema:
|
||||
reflect_kwargs["response_schema"] = defaults.reflect_response_schema
|
||||
# Add tags filtering
|
||||
if defaults.recall_tags:
|
||||
reflect_kwargs["tags"] = defaults.recall_tags
|
||||
reflect_kwargs["tags_match"] = defaults.recall_tags_match
|
||||
|
||||
# If reflect_include_facts is enabled, use the API directly to include facts
|
||||
if defaults.reflect_include_facts:
|
||||
from hindsight_client_api.models import reflect_request, reflect_include_options
|
||||
from hindsight_client_api.models import (
|
||||
reflect_request,
|
||||
reflect_include_options,
|
||||
)
|
||||
|
||||
request_obj = reflect_request.ReflectRequest(
|
||||
include=reflect_include_options.ReflectIncludeOptions(facts={}),
|
||||
**reflect_kwargs,
|
||||
)
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
result = loop.run_until_complete(client._api.reflect(bank_id, request_obj))
|
||||
result = loop.run_until_complete(
|
||||
client._api.reflect(bank_id, request_obj)
|
||||
)
|
||||
# Extract facts from based_on
|
||||
if hasattr(result, 'based_on') and result.based_on:
|
||||
if hasattr(result, "based_on") and result.based_on:
|
||||
reflect_facts = [
|
||||
{
|
||||
"text": f.text if hasattr(f, 'text') else str(f),
|
||||
"type": getattr(f, 'type', None),
|
||||
"context": getattr(f, 'context', None),
|
||||
"text": f.text if hasattr(f, "text") else str(f),
|
||||
"type": getattr(f, "type", None),
|
||||
"context": getattr(f, "context", None),
|
||||
}
|
||||
for f in result.based_on
|
||||
]
|
||||
@@ -340,7 +345,7 @@ def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, c
|
||||
bank_id=bank_id,
|
||||
**reflect_kwargs,
|
||||
)
|
||||
reflect_text = result.text if hasattr(result, 'text') else str(result)
|
||||
reflect_text = result.text if hasattr(result, "text") else str(result)
|
||||
|
||||
if not reflect_text:
|
||||
# Store debug info for empty result
|
||||
@@ -358,31 +363,32 @@ def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, c
|
||||
return messages
|
||||
|
||||
results_count = 1 # reflect returns a single synthesized response
|
||||
memory_context = (
|
||||
"# Relevant Context from Memory\n"
|
||||
f"{reflect_text}"
|
||||
)
|
||||
memory_context = f"# Relevant Context from Memory\n{reflect_text}"
|
||||
else:
|
||||
# Use recall API (original behavior)
|
||||
result = client.recall(
|
||||
bank_id=bank_id,
|
||||
query=user_query,
|
||||
budget=defaults.budget or "mid",
|
||||
max_tokens=defaults.max_memory_tokens or 4096,
|
||||
types=defaults.fact_types,
|
||||
)
|
||||
recall_kwargs = {
|
||||
"bank_id": bank_id,
|
||||
"query": user_query,
|
||||
"budget": defaults.budget or "mid",
|
||||
"max_tokens": defaults.max_memory_tokens or 4096,
|
||||
"types": defaults.fact_types,
|
||||
}
|
||||
if defaults.recall_tags:
|
||||
recall_kwargs["tags"] = defaults.recall_tags
|
||||
recall_kwargs["tags_match"] = defaults.recall_tags_match
|
||||
result = client.recall(**recall_kwargs)
|
||||
# client.recall() returns a list directly, not an object with .results
|
||||
if isinstance(result, list):
|
||||
results = result
|
||||
elif hasattr(result, 'results'):
|
||||
elif hasattr(result, "results"):
|
||||
results = result.results
|
||||
else:
|
||||
results = []
|
||||
# Convert to dicts for debug info
|
||||
recall_results = [
|
||||
{
|
||||
"text": r.text if hasattr(r, 'text') else str(r),
|
||||
"type": getattr(r, 'type', 'world'),
|
||||
"text": r.text if hasattr(r, "text") else str(r),
|
||||
"type": getattr(r, "type", "world"),
|
||||
}
|
||||
for r in results
|
||||
]
|
||||
@@ -402,11 +408,13 @@ def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, c
|
||||
return messages
|
||||
|
||||
# Format memories (apply limit if set, otherwise use all)
|
||||
results_to_use = results[:defaults.max_memories] if defaults.max_memories else results
|
||||
results_to_use = (
|
||||
results[: defaults.max_memories] if defaults.max_memories else results
|
||||
)
|
||||
memory_lines = []
|
||||
for i, r in enumerate(results_to_use, 1):
|
||||
text = r.text if hasattr(r, 'text') else str(r)
|
||||
fact_type = getattr(r, 'type', 'world')
|
||||
text = r.text if hasattr(r, "text") else str(r)
|
||||
fact_type = getattr(r, "type", "world")
|
||||
if text:
|
||||
type_label = fact_type.upper() if fact_type else "MEMORY"
|
||||
memory_lines.append(f"{i}. [{type_label}] {text}")
|
||||
@@ -441,16 +449,13 @@ def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, c
|
||||
existing_content = msg.get("content", "")
|
||||
updated_messages[i] = {
|
||||
**msg,
|
||||
"content": f"{existing_content}\n\n{memory_context}"
|
||||
"content": f"{existing_content}\n\n{memory_context}",
|
||||
}
|
||||
found_system = True
|
||||
break
|
||||
|
||||
if not found_system:
|
||||
updated_messages.insert(0, {
|
||||
"role": "system",
|
||||
"content": memory_context
|
||||
})
|
||||
updated_messages.insert(0, {"role": "system", "content": memory_context})
|
||||
|
||||
# Store debug info when verbose
|
||||
if config.verbose:
|
||||
@@ -488,7 +493,9 @@ def _inject_memories(messages: List[dict], custom_query: Optional[str] = None, c
|
||||
except Exception as e:
|
||||
# Always set debug info on error when verbose mode is on
|
||||
if config and config.verbose:
|
||||
logging.getLogger("hindsight_litellm").warning(f"Failed to inject memories: {e}")
|
||||
logging.getLogger("hindsight_litellm").warning(
|
||||
f"Failed to inject memories: {e}"
|
||||
)
|
||||
_last_injection_debug = InjectionDebugInfo(
|
||||
mode="reflect" if (defaults and defaults.use_reflect) else "recall",
|
||||
query=user_query or "",
|
||||
@@ -534,7 +541,11 @@ def _wrapped_completion(*args, **kwargs):
|
||||
# Step 1: Inject memories (raises HindsightError on failure)
|
||||
if config and config.inject_memories and messages:
|
||||
try:
|
||||
injected_messages = _inject_memories(messages, custom_query=custom_query, custom_reflect_context=custom_reflect_context)
|
||||
injected_messages = _inject_memories(
|
||||
messages,
|
||||
custom_query=custom_query,
|
||||
custom_reflect_context=custom_reflect_context,
|
||||
)
|
||||
kwargs["messages"] = injected_messages
|
||||
except Exception as e:
|
||||
raise HindsightError(f"Failed to inject memories: {e}") from e
|
||||
@@ -577,7 +588,11 @@ async def _wrapped_acompletion(*args, **kwargs):
|
||||
# Step 1: Inject memories (raises HindsightError on failure)
|
||||
if config and config.inject_memories and messages:
|
||||
try:
|
||||
injected_messages = _inject_memories(messages, custom_query=custom_query, custom_reflect_context=custom_reflect_context)
|
||||
injected_messages = _inject_memories(
|
||||
messages,
|
||||
custom_query=custom_query,
|
||||
custom_reflect_context=custom_reflect_context,
|
||||
)
|
||||
kwargs["messages"] = injected_messages
|
||||
except Exception as e:
|
||||
raise HindsightError(f"Failed to inject memories: {e}") from e
|
||||
@@ -719,6 +734,7 @@ def cleanup() -> None:
|
||||
# Convenience wrappers - use hindsight_litellm.completion() directly
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _format_conversation_for_storage(
|
||||
messages: List[dict],
|
||||
response,
|
||||
@@ -755,7 +771,9 @@ def _format_conversation_for_storage(
|
||||
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', '')})")
|
||||
tc_strs.append(
|
||||
f"{func.get('name', '')}({func.get('arguments', '')})"
|
||||
)
|
||||
if tc_strs:
|
||||
items.append(f"ASSISTANT_TOOL_CALLS: {'; '.join(tc_strs)}")
|
||||
if content:
|
||||
@@ -783,7 +801,9 @@ def _format_conversation_for_storage(
|
||||
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})")
|
||||
assistant_tool_calls.append(
|
||||
f"{tc.function.name}({tc.function.arguments})"
|
||||
)
|
||||
|
||||
if assistant_content:
|
||||
items.append(f"ASSISTANT: {assistant_content}")
|
||||
@@ -800,21 +820,93 @@ _pending_storage_errors: List[Exception] = []
|
||||
_storage_error_lock = threading.Lock()
|
||||
|
||||
|
||||
def _get_existing_document_content(
|
||||
bank_id: str, document_id: str, verbose: bool
|
||||
) -> Optional[str]:
|
||||
"""Fetch existing document content for accumulation via low-level API.
|
||||
|
||||
Returns:
|
||||
The existing document's original_text, or None if not found.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
config = get_config()
|
||||
if not config:
|
||||
return None
|
||||
|
||||
try:
|
||||
import hindsight_client_api
|
||||
from hindsight_client_api.api import documents_api
|
||||
|
||||
api_config = hindsight_client_api.Configuration(
|
||||
host=config.hindsight_api_url, access_token=config.api_key
|
||||
)
|
||||
api_client = hindsight_client_api.ApiClient(api_config)
|
||||
if config.api_key:
|
||||
api_client.set_default_header("Authorization", f"Bearer {config.api_key}")
|
||||
docs_api = documents_api.DocumentsApi(api_client)
|
||||
|
||||
async def _fetch():
|
||||
try:
|
||||
doc = await docs_api.get_document(bank_id, document_id)
|
||||
return doc.original_text if doc else None
|
||||
except Exception as e:
|
||||
if "404" in str(e) or "Not Found" in str(e):
|
||||
return None
|
||||
raise
|
||||
finally:
|
||||
await api_client.close()
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
original_text = loop.run_until_complete(_fetch())
|
||||
if original_text and verbose:
|
||||
_storage_logger.debug(f"Fetched existing document: {document_id}")
|
||||
return original_text
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
_storage_logger.debug(f"No existing document found: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _store_conversation_sync(
|
||||
conversation_text: str,
|
||||
bank_id: str,
|
||||
document_id: Optional[str],
|
||||
tags: Optional[List[str]],
|
||||
model: str,
|
||||
verbose: bool,
|
||||
) -> None:
|
||||
"""Actually store the conversation (runs in background thread)."""
|
||||
"""Actually store the conversation (runs in background thread).
|
||||
|
||||
If document_id is set, fetches existing document content and appends
|
||||
to accumulate the full conversation in one document.
|
||||
"""
|
||||
global _pending_storage_errors
|
||||
try:
|
||||
# If document_id is set, fetch existing content and append
|
||||
content_to_store = conversation_text
|
||||
if document_id:
|
||||
existing_content = _get_existing_document_content(
|
||||
bank_id, document_id, verbose
|
||||
)
|
||||
if existing_content:
|
||||
content_to_store = f"{existing_content}\n\n{conversation_text}"
|
||||
if verbose:
|
||||
_storage_logger.debug(
|
||||
f"Appending to existing document: {document_id}"
|
||||
)
|
||||
|
||||
retain(
|
||||
content=conversation_text,
|
||||
content=content_to_store,
|
||||
bank_id=bank_id,
|
||||
context=f"conversation:litellm:{model}",
|
||||
document_id=document_id,
|
||||
tags=tags,
|
||||
metadata={"source": "litellm", "model": model},
|
||||
)
|
||||
if verbose:
|
||||
@@ -887,11 +979,26 @@ def _store_conversation(
|
||||
# Sync mode: run directly and raise errors
|
||||
if config.sync_storage:
|
||||
try:
|
||||
# If document_id is set, fetch existing content and append
|
||||
content_to_store = conversation_text
|
||||
if defaults.effective_document_id:
|
||||
existing_content = _get_existing_document_content(
|
||||
defaults.bank_id, defaults.effective_document_id, config.verbose
|
||||
)
|
||||
if existing_content:
|
||||
content_to_store = f"{existing_content}\n\n{conversation_text}"
|
||||
if config.verbose:
|
||||
_storage_logger.debug(
|
||||
f"Appending to existing document: "
|
||||
f"{defaults.effective_document_id}"
|
||||
)
|
||||
|
||||
retain(
|
||||
content=conversation_text,
|
||||
content=content_to_store,
|
||||
bank_id=defaults.bank_id,
|
||||
context=f"conversation:litellm:{model}",
|
||||
document_id=defaults.document_id,
|
||||
document_id=defaults.effective_document_id,
|
||||
tags=defaults.tags,
|
||||
metadata={"source": "litellm", "model": model},
|
||||
)
|
||||
if config.verbose:
|
||||
@@ -906,7 +1013,8 @@ def _store_conversation(
|
||||
args=(
|
||||
conversation_text,
|
||||
defaults.bank_id,
|
||||
defaults.document_id,
|
||||
defaults.effective_document_id,
|
||||
defaults.tags,
|
||||
model,
|
||||
config.verbose,
|
||||
),
|
||||
@@ -981,7 +1089,11 @@ def completion(*args, **kwargs):
|
||||
# Step 1: Inject memories (raises HindsightError on failure)
|
||||
if config and config.inject_memories and messages:
|
||||
try:
|
||||
injected_messages = _inject_memories(messages, custom_query=custom_query, custom_reflect_context=custom_reflect_context)
|
||||
injected_messages = _inject_memories(
|
||||
messages,
|
||||
custom_query=custom_query,
|
||||
custom_reflect_context=custom_reflect_context,
|
||||
)
|
||||
kwargs["messages"] = injected_messages
|
||||
except Exception as e:
|
||||
raise HindsightError(f"Failed to inject memories: {e}") from e
|
||||
@@ -1052,7 +1164,11 @@ async def acompletion(*args, **kwargs):
|
||||
# Step 1: Inject memories (raises HindsightError on failure)
|
||||
if config and config.inject_memories and messages:
|
||||
try:
|
||||
injected_messages = _inject_memories(messages, custom_query=custom_query, custom_reflect_context=custom_reflect_context)
|
||||
injected_messages = _inject_memories(
|
||||
messages,
|
||||
custom_query=custom_query,
|
||||
custom_reflect_context=custom_reflect_context,
|
||||
)
|
||||
kwargs["messages"] = injected_messages
|
||||
except Exception as e:
|
||||
raise HindsightError(f"Failed to inject memories: {e}") from e
|
||||
@@ -1161,6 +1277,7 @@ def hindsight_memory(
|
||||
if previous_defaults:
|
||||
set_defaults(
|
||||
bank_id=previous_defaults.bank_id,
|
||||
session_id=previous_defaults.session_id,
|
||||
document_id=previous_defaults.document_id,
|
||||
budget=previous_defaults.budget,
|
||||
fact_types=previous_defaults.fact_types,
|
||||
@@ -1193,12 +1310,16 @@ __all__ = [
|
||||
"recall",
|
||||
"arecall",
|
||||
"RecallResult",
|
||||
"RecallResponse",
|
||||
"RecallDebugInfo",
|
||||
"reflect",
|
||||
"areflect",
|
||||
"ReflectResult",
|
||||
"ReflectDebugInfo",
|
||||
"retain",
|
||||
"aretain",
|
||||
"RetainResult",
|
||||
"RetainDebugInfo",
|
||||
# Native client wrappers
|
||||
"wrap_openai",
|
||||
"wrap_anthropic",
|
||||
@@ -1209,8 +1330,6 @@ __all__ = [
|
||||
"get_defaults",
|
||||
"is_configured",
|
||||
"reset_config",
|
||||
"set_document_id",
|
||||
"set_bank_mission",
|
||||
"HindsightConfig",
|
||||
"HindsightDefaults",
|
||||
"MemoryInjectionMode",
|
||||
|
||||
@@ -10,7 +10,6 @@ when the hindsight_client's async methods are called from LiteLLM callbacks.
|
||||
import logging
|
||||
import fnmatch
|
||||
import hashlib
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
import asyncio
|
||||
import threading
|
||||
@@ -22,21 +21,24 @@ from litellm.types.utils import ModelResponse
|
||||
from .config import (
|
||||
get_config,
|
||||
get_defaults,
|
||||
is_configured,
|
||||
HindsightConfig,
|
||||
HindsightDefaults,
|
||||
HindsightCallSettings,
|
||||
HindsightDefaults, # Backward compatibility alias
|
||||
MemoryInjectionMode,
|
||||
_merge_call_settings,
|
||||
)
|
||||
|
||||
# Use requests for sync HTTP calls to avoid async event loop issues
|
||||
try:
|
||||
import requests
|
||||
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
try:
|
||||
import httpx
|
||||
|
||||
HAS_HTTPX = True
|
||||
except ImportError:
|
||||
HAS_HTTPX = False
|
||||
@@ -51,11 +53,14 @@ class HindsightError(Exception):
|
||||
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-")
|
||||
_executor = concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=4, thread_name_prefix="hindsight-"
|
||||
)
|
||||
|
||||
|
||||
class HindsightCallback(CustomLogger):
|
||||
@@ -93,42 +98,20 @@ class HindsightCallback(CustomLogger):
|
||||
self._recent_hashes: Set[str] = set()
|
||||
self._max_hash_cache = 1000
|
||||
|
||||
def _get_effective_settings(self, kwargs: Dict[str, Any]) -> HindsightDefaults:
|
||||
def _get_effective_settings(self, kwargs: Dict[str, Any]) -> HindsightCallSettings:
|
||||
"""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
|
||||
Uses the unified _merge_call_settings function which automatically handles
|
||||
all HindsightCallSettings fields. When a new field is added to the dataclass,
|
||||
it automatically works here.
|
||||
|
||||
Per-call kwargs use hindsight_* prefix (e.g., hindsight_bank_id, hindsight_budget).
|
||||
|
||||
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),
|
||||
)
|
||||
defaults = get_defaults() or HindsightCallSettings()
|
||||
return _merge_call_settings(defaults, kwargs)
|
||||
|
||||
def _get_http_session(self):
|
||||
"""Get or create a requests Session (thread-safe)."""
|
||||
@@ -159,7 +142,9 @@ class HindsightCallback(CustomLogger):
|
||||
|
||||
try:
|
||||
if HAS_REQUESTS:
|
||||
response = session.post(url, json=json_data, headers=headers, timeout=30)
|
||||
response = session.post(
|
||||
url, json=json_data, headers=headers, timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
elif HAS_HTTPX:
|
||||
@@ -167,7 +152,9 @@ class HindsightCallback(CustomLogger):
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
else:
|
||||
raise HindsightError("No HTTP client available (install requests or httpx)")
|
||||
raise HindsightError(
|
||||
"No HTTP client available (install requests or httpx)"
|
||||
)
|
||||
except HindsightError:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -175,6 +162,46 @@ class HindsightCallback(CustomLogger):
|
||||
logger.error(f"HTTP POST failed: {e}")
|
||||
raise HindsightError(f"Hindsight API request failed: {e}") from e
|
||||
|
||||
def _http_get(
|
||||
self, url: str, config: HindsightConfig
|
||||
) -> Optional[dict]:
|
||||
"""Make a synchronous HTTP GET request.
|
||||
|
||||
Returns:
|
||||
Response JSON dict, or None if 404 (not found).
|
||||
|
||||
Raises:
|
||||
HindsightError: If the request fails for reasons other than 404.
|
||||
"""
|
||||
session = self._get_http_session()
|
||||
headers = {}
|
||||
if config.api_key:
|
||||
headers["Authorization"] = f"Bearer {config.api_key}"
|
||||
|
||||
try:
|
||||
if HAS_REQUESTS:
|
||||
response = session.get(url, headers=headers, timeout=30)
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
elif HAS_HTTPX:
|
||||
response = session.get(url, headers=headers)
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
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.error(f"HTTP GET 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."""
|
||||
for pattern in config.excluded_models:
|
||||
@@ -237,10 +264,7 @@ class HindsightCallback(CustomLogger):
|
||||
return False
|
||||
|
||||
def _format_memories(
|
||||
self,
|
||||
results: List[Any],
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
self, results: List[Any], settings: HindsightDefaults, config: HindsightConfig
|
||||
) -> str:
|
||||
"""Format memory recall results into a context string.
|
||||
|
||||
@@ -251,14 +275,16 @@ class HindsightCallback(CustomLogger):
|
||||
return ""
|
||||
|
||||
# Apply limit if set, otherwise use all results
|
||||
results_to_use = results[:settings.max_memories] if settings.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
|
||||
if hasattr(result, 'text'):
|
||||
if hasattr(result, "text"):
|
||||
text = result.text or ""
|
||||
fact_type = getattr(result, 'type', 'world') or "world"
|
||||
weight = getattr(result, 'weight', 0.0) or 0.0
|
||||
fact_type = getattr(result, "type", "world") or "world"
|
||||
weight = getattr(result, "weight", 0.0) or 0.0
|
||||
else:
|
||||
text = result.get("text", "")
|
||||
fact_type = result.get("type", result.get("fact_type", "world"))
|
||||
@@ -301,15 +327,12 @@ class HindsightCallback(CustomLogger):
|
||||
existing_content = msg.get("content", "")
|
||||
updated_messages[i] = {
|
||||
**msg,
|
||||
"content": f"{existing_content}\n\n{memory_context}"
|
||||
"content": f"{existing_content}\n\n{memory_context}",
|
||||
}
|
||||
return updated_messages
|
||||
|
||||
# No system message found, prepend one
|
||||
updated_messages.insert(0, {
|
||||
"role": "system",
|
||||
"content": memory_context
|
||||
})
|
||||
updated_messages.insert(0, {"role": "system", "content": memory_context})
|
||||
|
||||
elif config.injection_mode == MemoryInjectionMode.PREPEND_USER:
|
||||
# Find the last user message and prepend context
|
||||
@@ -319,17 +342,14 @@ class HindsightCallback(CustomLogger):
|
||||
if isinstance(original_content, str):
|
||||
updated_messages[i] = {
|
||||
**updated_messages[i],
|
||||
"content": f"{memory_context}\n\n---\n\n{original_content}"
|
||||
"content": f"{memory_context}\n\n---\n\n{original_content}",
|
||||
}
|
||||
break
|
||||
|
||||
return updated_messages
|
||||
|
||||
def _recall_memories_sync(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
self, query: str, settings: HindsightDefaults, config: HindsightConfig
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Recall relevant memories from Hindsight (sync) using direct HTTP.
|
||||
|
||||
@@ -352,6 +372,9 @@ class HindsightCallback(CustomLogger):
|
||||
}
|
||||
if settings.fact_types:
|
||||
request_data["types"] = settings.fact_types
|
||||
if settings.recall_tags:
|
||||
request_data["tags"] = settings.recall_tags
|
||||
request_data["tags_match"] = settings.recall_tags_match
|
||||
|
||||
# Add trace parameter for debugging
|
||||
if settings.trace:
|
||||
@@ -376,10 +399,7 @@ class HindsightCallback(CustomLogger):
|
||||
raise HindsightError(f"Memory recall failed: {e}") from e
|
||||
|
||||
async def _recall_memories_async(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
self, query: str, settings: HindsightDefaults, config: HindsightConfig
|
||||
) -> List[Any]:
|
||||
"""Recall relevant memories from Hindsight (async).
|
||||
|
||||
@@ -390,17 +410,13 @@ class HindsightCallback(CustomLogger):
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
results = await loop.run_in_executor(
|
||||
_executor,
|
||||
lambda: self._recall_memories_sync(query, settings, config)
|
||||
_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
|
||||
self, query: str, settings: HindsightDefaults, config: HindsightConfig
|
||||
) -> Optional[str]:
|
||||
"""Generate a reflection response from Hindsight (sync) using direct HTTP.
|
||||
|
||||
@@ -433,6 +449,11 @@ class HindsightCallback(CustomLogger):
|
||||
if settings.reflect_response_schema:
|
||||
request_data["response_schema"] = settings.reflect_response_schema
|
||||
|
||||
# Add tags filtering
|
||||
if settings.recall_tags:
|
||||
request_data["tags"] = settings.recall_tags
|
||||
request_data["tags_match"] = settings.recall_tags_match
|
||||
|
||||
# Add include options for facts if requested
|
||||
if settings.reflect_include_facts:
|
||||
request_data["include"] = {"facts": {}}
|
||||
@@ -444,6 +465,7 @@ class HindsightCallback(CustomLogger):
|
||||
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", "")
|
||||
@@ -454,10 +476,7 @@ class HindsightCallback(CustomLogger):
|
||||
raise HindsightError(f"Reflect failed: {e}") from e
|
||||
|
||||
async def _reflect_async(
|
||||
self,
|
||||
query: str,
|
||||
settings: HindsightDefaults,
|
||||
config: HindsightConfig
|
||||
self, query: str, settings: HindsightDefaults, config: HindsightConfig
|
||||
) -> Optional[str]:
|
||||
"""Generate a reflection response from Hindsight (async).
|
||||
|
||||
@@ -471,8 +490,7 @@ class HindsightCallback(CustomLogger):
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
result = await loop.run_in_executor(
|
||||
_executor,
|
||||
lambda: self._reflect_sync(query, settings, config)
|
||||
_executor, lambda: self._reflect_sync(query, settings, config)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -527,7 +545,9 @@ class HindsightCallback(CustomLogger):
|
||||
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})")
|
||||
assistant_tool_calls.append(
|
||||
f"{tc.function.name}({tc.function.arguments})"
|
||||
)
|
||||
|
||||
# Skip if no content AND no tool calls - nothing to store
|
||||
if not assistant_output and not assistant_tool_calls:
|
||||
@@ -550,7 +570,6 @@ class HindsightCallback(CustomLogger):
|
||||
|
||||
# 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
|
||||
|
||||
@@ -563,7 +582,9 @@ class HindsightCallback(CustomLogger):
|
||||
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', '')})")
|
||||
tc_strs.append(
|
||||
f"{func.get('name', '')}({func.get('arguments', '')})"
|
||||
)
|
||||
if tc_strs:
|
||||
items.append(f"ASSISTANT_TOOL_CALLS: {'; '.join(tc_strs)}")
|
||||
if content:
|
||||
@@ -605,7 +626,30 @@ class HindsightCallback(CustomLogger):
|
||||
|
||||
# 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)
|
||||
new_conversation_text = "\n\n".join(items)
|
||||
|
||||
# If document_id is set, fetch existing content and append
|
||||
# This ensures the full conversation accumulates in one document
|
||||
conversation_text = new_conversation_text
|
||||
if settings.effective_document_id:
|
||||
try:
|
||||
doc_url = (
|
||||
f"{config.hindsight_api_url}/v1/default/banks/{bank_id}"
|
||||
f"/documents/{settings.effective_document_id}"
|
||||
)
|
||||
existing_doc = self._http_get(doc_url, config)
|
||||
if existing_doc and existing_doc.get("original_text"):
|
||||
conversation_text = (
|
||||
f"{existing_doc['original_text']}\n\n{new_conversation_text}"
|
||||
)
|
||||
if config.verbose:
|
||||
logger.debug(
|
||||
f"Appending to existing document: "
|
||||
f"{settings.effective_document_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
if config.verbose:
|
||||
logger.debug(f"No existing document found, creating new: {e}")
|
||||
|
||||
# Build metadata
|
||||
metadata = {
|
||||
@@ -620,15 +664,17 @@ class HindsightCallback(CustomLogger):
|
||||
|
||||
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories"
|
||||
|
||||
item_data = {
|
||||
"content": conversation_text,
|
||||
"context": f"conversation:litellm:{model}",
|
||||
"metadata": metadata,
|
||||
"document_id": settings.effective_document_id, # Group by session/document
|
||||
}
|
||||
if settings.tags:
|
||||
item_data["tags"] = settings.tags
|
||||
|
||||
request_data = {
|
||||
"items": [
|
||||
{
|
||||
"content": conversation_text,
|
||||
"context": f"conversation:litellm:{model}",
|
||||
"metadata": metadata,
|
||||
"document_id": settings.document_id, # Group by document
|
||||
}
|
||||
],
|
||||
"items": [item_data],
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -660,7 +706,7 @@ class HindsightCallback(CustomLogger):
|
||||
_executor,
|
||||
lambda: self._store_conversation_sync(
|
||||
messages, response, model, settings, config
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# ========== LiteLLM CustomLogger Interface ==========
|
||||
@@ -709,10 +755,7 @@ class HindsightCallback(CustomLogger):
|
||||
return
|
||||
|
||||
# Format reflect response as context
|
||||
memory_context = (
|
||||
"# Relevant Context from Memory\n"
|
||||
f"{reflect_response}"
|
||||
)
|
||||
memory_context = f"# Relevant Context from Memory\n{reflect_response}"
|
||||
else:
|
||||
# Use recall API for raw fact retrieval
|
||||
memories = self._recall_memories_sync(user_query, settings, config)
|
||||
@@ -778,10 +821,7 @@ class HindsightCallback(CustomLogger):
|
||||
return
|
||||
|
||||
# Format reflect response as context
|
||||
memory_context = (
|
||||
"# Relevant Context from Memory\n"
|
||||
f"{reflect_response}"
|
||||
)
|
||||
memory_context = f"# Relevant Context from Memory\n{reflect_response}"
|
||||
else:
|
||||
# Use recall API for raw fact retrieval
|
||||
memories = await self._recall_memories_async(user_query, settings, config)
|
||||
@@ -865,7 +905,9 @@ class HindsightCallback(CustomLogger):
|
||||
return
|
||||
|
||||
# Store the conversation
|
||||
await self._store_conversation_async(messages, response_obj, model, settings, config)
|
||||
await self._store_conversation_async(
|
||||
messages, response_obj, model, settings, config
|
||||
)
|
||||
|
||||
def log_failure_event(
|
||||
self,
|
||||
|
||||
@@ -2,23 +2,22 @@
|
||||
|
||||
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.
|
||||
1. configure() - Connection settings + default per-call settings
|
||||
- API URL, authentication, and default values for all per-call settings
|
||||
|
||||
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
|
||||
2. set_defaults() - Update default values for per-call settings
|
||||
- Convenience function to update defaults without reconfiguring connection
|
||||
|
||||
3. Per-call kwargs (hindsight_* prefix) - Override any default per-call
|
||||
- hindsight_bank_id, hindsight_document_id, etc.
|
||||
3. Per-call kwargs (hindsight_* prefix) - Override any setting per-call
|
||||
- hindsight_bank_id, hindsight_budget, hindsight_inject_memories, etc.
|
||||
|
||||
4. set_bank_mission() - Set the mission for a memory bank (for mental models)
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional, List, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# Default Hindsight API URL (production)
|
||||
DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
|
||||
@@ -31,127 +30,262 @@ class MemoryInjectionMode(str, Enum):
|
||||
|
||||
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:
|
||||
"""Static configuration for Hindsight integration with LiteLLM.
|
||||
class HindsightCallSettings:
|
||||
"""Unified settings for Hindsight memory operations.
|
||||
|
||||
These settings typically don't change during a session.
|
||||
All fields here can be:
|
||||
- Set as defaults via configure() or set_defaults()
|
||||
- Overridden per-call via hindsight_* kwargs (e.g., hindsight_bank_id="other")
|
||||
|
||||
To add a new setting, just add a field here - it automatically works everywhere.
|
||||
|
||||
Attributes:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
(default: https://api.hindsight.vectorize.io)
|
||||
bank_id: Memory bank ID for memory operations (default: "default").
|
||||
For multi-user support, use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
api_key: API key for Hindsight authentication. If not provided,
|
||||
reads from HINDSIGHT_API_KEY environment variable.
|
||||
bank_id: Memory bank ID for operations. Use different bank_ids per user
|
||||
for multi-user support (e.g., f"user-{user_id}")
|
||||
session_id: Session ID for grouping conversations (maps to Hindsight's
|
||||
document_id). Use this to group related messages in a conversation.
|
||||
When set, Hindsight uses upsert behavior (same session = replace).
|
||||
document_id: DEPRECATED - Use session_id instead. Kept for backward
|
||||
compatibility. If both are set, session_id takes precedence.
|
||||
|
||||
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)
|
||||
excluded_models: List of model patterns to exclude from interception
|
||||
verbose: Enable verbose logging
|
||||
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 = DEFAULT_HINDSIGHT_API_URL
|
||||
bank_id: str = DEFAULT_BANK_ID
|
||||
api_key: Optional[str] = None
|
||||
store_conversations: bool = True
|
||||
inject_memories: bool = True
|
||||
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE
|
||||
excluded_models: List[str] = field(default_factory=list)
|
||||
verbose: bool = False
|
||||
sync_storage: bool = False
|
||||
|
||||
|
||||
@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
|
||||
fact_types: Filter by fact types (world, experience, opinion, observation)
|
||||
max_memories: Maximum memories to inject (None = no limit)
|
||||
max_memory_tokens: Maximum tokens for memory context
|
||||
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).
|
||||
tags: Tags to apply when storing conversations. Use for visibility scoping
|
||||
(e.g., ["user:alice", "session:123"]). Stored memories will have these tags.
|
||||
recall_tags: Tags to filter by when recalling/reflecting memories. Only memories
|
||||
matching these tags (based on recall_tags_match mode) will be retrieved.
|
||||
recall_tags_match: How to match recall_tags. Options:
|
||||
- "any": OR matching, includes untagged memories (default)
|
||||
- "all": AND matching, includes untagged memories
|
||||
- "any_strict": OR matching, excludes untagged memories
|
||||
- "all_strict": AND matching, excludes untagged memories
|
||||
|
||||
use_reflect: Use reflect API instead of recall for memory injection
|
||||
reflect_context: Context for reflect reasoning (shapes response, not retrieval)
|
||||
reflect_response_schema: JSON Schema for structured reflect output
|
||||
reflect_include_facts: Include facts used by reflect in debug info
|
||||
|
||||
query: Custom query for memory recall (if not set, extracts from user message)
|
||||
verbose: Enable verbose logging
|
||||
"""
|
||||
|
||||
# Memory bank settings
|
||||
bank_id: Optional[str] = None
|
||||
document_id: Optional[str] = None
|
||||
session_id: Optional[str] = None # Primary - maps to Hindsight's document_id
|
||||
document_id: Optional[str] = None # Deprecated - use session_id instead
|
||||
|
||||
# Feature toggles
|
||||
store_conversations: bool = True
|
||||
inject_memories: bool = True
|
||||
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE
|
||||
|
||||
# Recall settings
|
||||
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
|
||||
include_entities: bool = True
|
||||
trace: bool = False
|
||||
|
||||
# Tags for visibility scoping
|
||||
tags: Optional[List[str]] = None # Tags applied when storing conversations
|
||||
recall_tags: Optional[List[str]] = None # Tags to filter recall/reflect
|
||||
recall_tags_match: str = "any" # any, all, any_strict, all_strict
|
||||
|
||||
# Reflect settings (alternative to recall)
|
||||
use_reflect: bool = False
|
||||
reflect_context: Optional[str] = None
|
||||
reflect_response_schema: Optional[Dict[str, Any]] = None
|
||||
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
|
||||
|
||||
# Query override (if not set, extracts from last user message)
|
||||
query: Optional[str] = None
|
||||
|
||||
# Logging
|
||||
verbose: bool = False
|
||||
|
||||
@property
|
||||
def effective_document_id(self) -> Optional[str]:
|
||||
"""Get the effective document_id for Hindsight API calls.
|
||||
|
||||
Returns session_id if set, otherwise falls back to document_id.
|
||||
This maps to Hindsight's document_id parameter for retain operations.
|
||||
"""
|
||||
return self.session_id if self.session_id is not None else self.document_id
|
||||
|
||||
|
||||
def _merge_call_settings(
|
||||
defaults: HindsightCallSettings, kwargs: Dict[str, Any]
|
||||
) -> HindsightCallSettings:
|
||||
"""Merge per-call kwargs (hindsight_*) with defaults.
|
||||
|
||||
This automatically handles all fields in HindsightCallSettings.
|
||||
When a new field is added to the dataclass, it works here automatically.
|
||||
|
||||
Args:
|
||||
defaults: The default settings
|
||||
kwargs: The kwargs passed to the call, may contain hindsight_* overrides
|
||||
|
||||
Returns:
|
||||
Merged settings with per-call values overriding defaults
|
||||
"""
|
||||
# Start with defaults as dict
|
||||
merged = asdict(defaults)
|
||||
|
||||
# Get valid field names from the dataclass
|
||||
valid_fields = {f.name for f in fields(HindsightCallSettings)}
|
||||
|
||||
# Override with hindsight_* kwargs
|
||||
for key, value in kwargs.items():
|
||||
if key.startswith("hindsight_"):
|
||||
setting_name = key[len("hindsight_") :]
|
||||
if setting_name in valid_fields:
|
||||
merged[setting_name] = value
|
||||
|
||||
return HindsightCallSettings(**merged)
|
||||
|
||||
|
||||
# Backward compatibility alias
|
||||
HindsightDefaults = HindsightCallSettings
|
||||
|
||||
|
||||
@dataclass
|
||||
class HindsightConfig:
|
||||
"""Connection-level configuration for Hindsight integration.
|
||||
|
||||
These are settings that require a new client connection to change:
|
||||
- API URL and authentication
|
||||
- Session-level settings (excluded_models, sync_storage)
|
||||
|
||||
Per-call settings (bank_id, budget, etc.) are in default_settings.
|
||||
|
||||
Attributes:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
api_key: API key for Hindsight authentication
|
||||
excluded_models: List of model patterns to exclude from interception
|
||||
sync_storage: If True, storage runs synchronously and raises errors immediately
|
||||
default_settings: Default values for all per-call settings
|
||||
"""
|
||||
|
||||
hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
|
||||
api_key: Optional[str] = None
|
||||
excluded_models: List[str] = field(default_factory=list)
|
||||
sync_storage: bool = False
|
||||
default_settings: HindsightCallSettings = field(
|
||||
default_factory=HindsightCallSettings
|
||||
)
|
||||
|
||||
# Backward compatibility properties - delegate to default_settings
|
||||
@property
|
||||
def bank_id(self) -> Optional[str]:
|
||||
return self.default_settings.bank_id
|
||||
|
||||
@property
|
||||
def store_conversations(self) -> bool:
|
||||
return self.default_settings.store_conversations
|
||||
|
||||
@property
|
||||
def inject_memories(self) -> bool:
|
||||
return self.default_settings.inject_memories
|
||||
|
||||
@property
|
||||
def injection_mode(self) -> MemoryInjectionMode:
|
||||
return self.default_settings.injection_mode
|
||||
|
||||
@property
|
||||
def verbose(self) -> bool:
|
||||
return self.default_settings.verbose
|
||||
|
||||
|
||||
# Global instances
|
||||
_global_config: Optional[HindsightConfig] = None
|
||||
_global_defaults: Optional[HindsightDefaults] = None
|
||||
|
||||
|
||||
def configure(
|
||||
hindsight_api_url: Optional[str] = None,
|
||||
bank_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
background: Optional[str] = None,
|
||||
excluded_models: Optional[List[str]] = None,
|
||||
sync_storage: bool = False,
|
||||
# Bank setup (one-time)
|
||||
mission: Optional[str] = None,
|
||||
bank_name: Optional[str] = None,
|
||||
# Per-call defaults (all HindsightCallSettings fields)
|
||||
bank_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
document_id: Optional[str] = None, # Deprecated - use session_id
|
||||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE,
|
||||
excluded_models: Optional[List[str]] = None,
|
||||
budget: str = "mid",
|
||||
fact_types: Optional[List[str]] = None,
|
||||
max_memories: Optional[int] = None,
|
||||
max_memory_tokens: int = 4096,
|
||||
include_entities: bool = True,
|
||||
trace: bool = False,
|
||||
tags: Optional[List[str]] = None,
|
||||
recall_tags: Optional[List[str]] = None,
|
||||
recall_tags_match: str = "any",
|
||||
use_reflect: bool = False,
|
||||
reflect_context: Optional[str] = None,
|
||||
reflect_response_schema: Optional[Dict[str, Any]] = None,
|
||||
reflect_include_facts: bool = False,
|
||||
verbose: bool = False,
|
||||
sync_storage: bool = False,
|
||||
) -> HindsightConfig:
|
||||
"""Configure static Hindsight integration settings for LiteLLM.
|
||||
"""Configure Hindsight integration settings.
|
||||
|
||||
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.
|
||||
|
||||
With sensible defaults, you can use minimal configuration:
|
||||
|
||||
configure() # Just set HINDSIGHT_API_KEY env var
|
||||
enable()
|
||||
Sets up connection settings and default values for per-call settings.
|
||||
All per-call settings can be overridden using hindsight_* kwargs.
|
||||
|
||||
Args:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
(default: https://api.hindsight.vectorize.io)
|
||||
bank_id: Memory bank ID for memory operations (default: "default").
|
||||
For multi-user support, use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
api_key: API key for Hindsight authentication. If not provided,
|
||||
reads from HINDSIGHT_API_KEY environment variable.
|
||||
background: Instructions guiding what Hindsight should learn and remember.
|
||||
bank_name: Optional display name for the bank.
|
||||
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
|
||||
excluded_models: List of model patterns to exclude from interception
|
||||
verbose: Enable verbose logging
|
||||
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.
|
||||
mission: Instructions guiding what Hindsight should learn and remember
|
||||
(used for mental model generation).
|
||||
bank_name: Optional display name for the bank.
|
||||
|
||||
# Per-call defaults (can be overridden with hindsight_* kwargs):
|
||||
bank_id: Memory bank ID (default: "default"). For multi-user support,
|
||||
use different bank_ids per user (e.g., f"user-{user_id}")
|
||||
session_id: Session ID for grouping conversations. Maps to Hindsight's
|
||||
document_id. When set, enables upsert behavior (same session = replace).
|
||||
document_id: DEPRECATED - Use session_id instead.
|
||||
store_conversations: Whether to store conversations (default: True)
|
||||
inject_memories: Whether to inject memories (default: True)
|
||||
injection_mode: How to inject memories (system_message or prepend_user)
|
||||
budget: Recall budget level - low/mid/high (default: "mid")
|
||||
fact_types: Filter by fact types (world/experience/opinion/observation)
|
||||
max_memories: Max memories to inject (None = no limit)
|
||||
max_memory_tokens: Max tokens for memory context (default: 4096)
|
||||
include_entities: Include entity observations in recall (default: True)
|
||||
trace: Enable trace info for debugging (default: False)
|
||||
tags: Tags to apply when storing conversations (e.g., ["user:alice"])
|
||||
recall_tags: Tags to filter by when recalling/reflecting memories
|
||||
recall_tags_match: Tag matching mode - any/all/any_strict/all_strict (default: "any")
|
||||
use_reflect: Use reflect API instead of recall (default: False)
|
||||
reflect_context: Context for reflect reasoning
|
||||
reflect_response_schema: JSON Schema for structured reflect output
|
||||
reflect_include_facts: Include facts in reflect debug info (default: False)
|
||||
verbose: Enable verbose logging (default: False)
|
||||
|
||||
Returns:
|
||||
The configured HindsightConfig instance
|
||||
@@ -163,39 +297,67 @@ def configure(
|
||||
>>> configure()
|
||||
>>> enable()
|
||||
>>>
|
||||
>>> # Or with custom settings
|
||||
>>> # With per-call defaults
|
||||
>>> configure(
|
||||
... bank_id="user-123", # Per-user bank for multi-user support
|
||||
... background="Remember user preferences and past interactions.",
|
||||
... bank_id="user-123",
|
||||
... budget="high",
|
||||
... background="Remember user preferences.",
|
||||
... )
|
||||
>>> enable()
|
||||
>>>
|
||||
>>> # Override per-call:
|
||||
>>> response = litellm.completion(
|
||||
... model="gpt-4",
|
||||
... messages=[...],
|
||||
... hindsight_bank_id="other-user", # Override default
|
||||
... )
|
||||
"""
|
||||
global _global_config
|
||||
|
||||
# Apply defaults
|
||||
# Apply connection-level defaults
|
||||
resolved_api_url = hindsight_api_url or DEFAULT_HINDSIGHT_API_URL
|
||||
resolved_bank_id = bank_id or DEFAULT_BANK_ID
|
||||
resolved_api_key = api_key or os.environ.get(HINDSIGHT_API_KEY_ENV)
|
||||
resolved_bank_id = bank_id or DEFAULT_BANK_ID
|
||||
|
||||
_global_config = HindsightConfig(
|
||||
hindsight_api_url=resolved_api_url,
|
||||
# Build default settings
|
||||
default_settings = HindsightCallSettings(
|
||||
bank_id=resolved_bank_id,
|
||||
api_key=resolved_api_key,
|
||||
document_id=document_id,
|
||||
session_id=session_id,
|
||||
store_conversations=store_conversations,
|
||||
inject_memories=inject_memories,
|
||||
injection_mode=injection_mode,
|
||||
excluded_models=excluded_models or [],
|
||||
budget=budget,
|
||||
fact_types=fact_types,
|
||||
max_memories=max_memories,
|
||||
max_memory_tokens=max_memory_tokens,
|
||||
include_entities=include_entities,
|
||||
trace=trace,
|
||||
tags=tags,
|
||||
recall_tags=recall_tags,
|
||||
recall_tags_match=recall_tags_match,
|
||||
use_reflect=use_reflect,
|
||||
reflect_context=reflect_context,
|
||||
reflect_response_schema=reflect_response_schema,
|
||||
reflect_include_facts=reflect_include_facts,
|
||||
verbose=verbose,
|
||||
sync_storage=sync_storage,
|
||||
)
|
||||
|
||||
# If background or bank_name is provided, create/update the bank
|
||||
if background or bank_name:
|
||||
_global_config = HindsightConfig(
|
||||
hindsight_api_url=resolved_api_url,
|
||||
api_key=resolved_api_key,
|
||||
excluded_models=excluded_models or [],
|
||||
sync_storage=sync_storage,
|
||||
default_settings=default_settings,
|
||||
)
|
||||
|
||||
# If mission or bank_name is provided, create/update the bank
|
||||
if mission or bank_name:
|
||||
_create_or_update_bank(
|
||||
hindsight_api_url=resolved_api_url,
|
||||
bank_id=resolved_bank_id,
|
||||
name=bank_name,
|
||||
mission=background,
|
||||
mission=mission,
|
||||
verbose=verbose,
|
||||
api_key=resolved_api_key,
|
||||
)
|
||||
@@ -205,83 +367,125 @@ def configure(
|
||||
|
||||
def set_defaults(
|
||||
bank_id: Optional[str] = None,
|
||||
document_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
document_id: Optional[str] = None, # Deprecated - use session_id
|
||||
store_conversations: Optional[bool] = None,
|
||||
inject_memories: Optional[bool] = None,
|
||||
injection_mode: Optional[MemoryInjectionMode] = 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.
|
||||
tags: Optional[List[str]] = None,
|
||||
recall_tags: Optional[List[str]] = None,
|
||||
recall_tags_match: Optional[str] = None,
|
||||
use_reflect: Optional[bool] = None,
|
||||
reflect_context: Optional[str] = None,
|
||||
reflect_response_schema: Optional[Dict[str, Any]] = None,
|
||||
reflect_include_facts: Optional[bool] = None,
|
||||
verbose: Optional[bool] = None,
|
||||
) -> HindsightCallSettings:
|
||||
"""Update 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
|
||||
Updates only the specified fields, preserving other defaults.
|
||||
Any of these can be overridden on individual 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)
|
||||
bank_id: Memory bank ID for memory operations
|
||||
session_id: Session ID for grouping conversations. Maps to Hindsight's
|
||||
document_id. When set, enables upsert behavior (same session = replace).
|
||||
document_id: DEPRECATED - Use session_id instead.
|
||||
store_conversations: Whether to store conversations
|
||||
inject_memories: Whether to inject memories
|
||||
injection_mode: How to inject memories (system_message or prepend_user)
|
||||
budget: Budget level for memory recall (low, mid, high)
|
||||
fact_types: Fact types to filter (world, experience, opinion, observation)
|
||||
max_memories: Max number of memories to inject
|
||||
max_memory_tokens: Max tokens for memory context
|
||||
include_entities: Include entity observations in recall
|
||||
trace: Enable trace info for debugging
|
||||
tags: Tags to apply when storing conversations
|
||||
recall_tags: Tags to filter by when recalling/reflecting memories
|
||||
recall_tags_match: Tag matching mode - any/all/any_strict/all_strict
|
||||
use_reflect: Use reflect API instead of recall
|
||||
reflect_context: Context for reflect reasoning
|
||||
reflect_response_schema: JSON Schema for structured reflect output
|
||||
reflect_include_facts: Include facts in reflect debug info
|
||||
verbose: Enable verbose logging
|
||||
|
||||
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).
|
||||
The updated HindsightCallSettings instance
|
||||
|
||||
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
|
||||
... )
|
||||
>>> from hindsight_litellm import configure, set_defaults
|
||||
>>> configure()
|
||||
>>> set_defaults(bank_id="my-agent", budget="high")
|
||||
"""
|
||||
global _global_defaults
|
||||
global _global_config
|
||||
|
||||
# Get current defaults or create new
|
||||
current = _global_defaults or HindsightDefaults()
|
||||
# Ensure configure() was called
|
||||
if _global_config is None:
|
||||
# Auto-configure with defaults if not configured
|
||||
configure()
|
||||
|
||||
# Update only provided values
|
||||
_global_defaults = HindsightDefaults(
|
||||
# Get current defaults
|
||||
current = _global_config.default_settings
|
||||
|
||||
# Update only provided values using dataclass fields
|
||||
updated_settings = HindsightCallSettings(
|
||||
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,
|
||||
session_id=session_id if session_id is not None else current.session_id,
|
||||
store_conversations=store_conversations
|
||||
if store_conversations is not None
|
||||
else current.store_conversations,
|
||||
inject_memories=inject_memories
|
||||
if inject_memories is not None
|
||||
else current.inject_memories,
|
||||
injection_mode=injection_mode
|
||||
if injection_mode is not None
|
||||
else current.injection_mode,
|
||||
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,
|
||||
max_memory_tokens=max_memory_tokens
|
||||
if max_memory_tokens is not None
|
||||
else current.max_memory_tokens,
|
||||
include_entities=include_entities
|
||||
if include_entities is not None
|
||||
else current.include_entities,
|
||||
trace=trace if trace is not None else current.trace,
|
||||
tags=tags if tags is not None else current.tags,
|
||||
recall_tags=recall_tags if recall_tags is not None else current.recall_tags,
|
||||
recall_tags_match=recall_tags_match
|
||||
if recall_tags_match is not None
|
||||
else current.recall_tags_match,
|
||||
use_reflect=use_reflect if use_reflect is not None else current.use_reflect,
|
||||
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,
|
||||
reflect_include_facts=reflect_include_facts
|
||||
if reflect_include_facts is not None
|
||||
else current.reflect_include_facts,
|
||||
verbose=verbose if verbose is not None else current.verbose,
|
||||
)
|
||||
|
||||
return _global_defaults
|
||||
# Update the config's default settings
|
||||
_global_config = HindsightConfig(
|
||||
hindsight_api_url=_global_config.hindsight_api_url,
|
||||
api_key=_global_config.api_key,
|
||||
excluded_models=_global_config.excluded_models,
|
||||
sync_storage=_global_config.sync_storage,
|
||||
default_settings=updated_settings,
|
||||
)
|
||||
|
||||
return updated_settings
|
||||
|
||||
|
||||
def _create_or_update_bank(
|
||||
@@ -312,12 +516,14 @@ def _create_or_update_bank(
|
||||
)
|
||||
if verbose:
|
||||
import logging
|
||||
|
||||
logging.getLogger("hindsight_litellm").info(
|
||||
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. "
|
||||
"Install with: pip install hindsight-client"
|
||||
@@ -325,13 +531,14 @@ def _create_or_update_bank(
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
import logging
|
||||
|
||||
logging.getLogger("hindsight_litellm").warning(
|
||||
f"Failed to create/update bank: {e}"
|
||||
)
|
||||
|
||||
|
||||
def get_config() -> Optional[HindsightConfig]:
|
||||
"""Get the current global static configuration.
|
||||
"""Get the current global configuration.
|
||||
|
||||
Returns:
|
||||
The current HindsightConfig instance, or None if not configured
|
||||
@@ -339,13 +546,15 @@ def get_config() -> Optional[HindsightConfig]:
|
||||
return _global_config
|
||||
|
||||
|
||||
def get_defaults() -> Optional[HindsightDefaults]:
|
||||
def get_defaults() -> Optional[HindsightCallSettings]:
|
||||
"""Get the current global defaults for per-call settings.
|
||||
|
||||
Returns:
|
||||
The current HindsightDefaults instance, or None if not set
|
||||
The current HindsightCallSettings instance, or None if not configured
|
||||
"""
|
||||
return _global_defaults
|
||||
if _global_config is not None:
|
||||
return _global_config.default_settings
|
||||
return None
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
@@ -356,121 +565,12 @@ def is_configured() -> bool:
|
||||
"""
|
||||
if _global_config is not None and _global_config.bank_id:
|
||||
return True
|
||||
return (
|
||||
_global_config is not None
|
||||
and _global_defaults is not None
|
||||
and _global_defaults.bank_id is not None
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset all global configuration to None."""
|
||||
global _global_config, _global_defaults
|
||||
global _global_config
|
||||
_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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,24 @@
|
||||
"""Unit tests for hindsight_litellm configuration and defaults."""
|
||||
|
||||
import os
|
||||
from dataclasses import fields
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_litellm import configure, wrap_openai, wrap_anthropic, reset_config, is_configured
|
||||
from hindsight_litellm import configure, wrap_openai, wrap_anthropic
|
||||
from hindsight_litellm.config import (
|
||||
DEFAULT_HINDSIGHT_API_URL,
|
||||
DEFAULT_BANK_ID,
|
||||
HINDSIGHT_API_KEY_ENV,
|
||||
reset_config,
|
||||
get_config,
|
||||
is_configured,
|
||||
)
|
||||
from hindsight_litellm.wrappers import HindsightOpenAI, HindsightAnthropic
|
||||
from hindsight_litellm.wrappers import (
|
||||
HindsightOpenAI,
|
||||
HindsightAnthropic,
|
||||
HindsightCallSettings,
|
||||
_merge_settings,
|
||||
)
|
||||
|
||||
|
||||
class TestDefaults:
|
||||
@@ -107,7 +111,7 @@ class TestWrapOpenAI:
|
||||
wrapped = wrap_openai(mock_client)
|
||||
|
||||
assert isinstance(wrapped, HindsightOpenAI)
|
||||
assert wrapped._bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
assert wrapped._api_key == "test-key"
|
||||
|
||||
@@ -117,7 +121,7 @@ class TestWrapOpenAI:
|
||||
|
||||
wrapped = wrap_openai(mock_client)
|
||||
|
||||
assert wrapped._bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
|
||||
def test_wrap_openai_reads_api_key_from_env(self):
|
||||
@@ -140,10 +144,47 @@ class TestWrapOpenAI:
|
||||
api_key="my-key",
|
||||
)
|
||||
|
||||
assert wrapped._bank_id == "my-bank"
|
||||
assert wrapped._default_settings.bank_id == "my-bank"
|
||||
assert wrapped._api_url == "http://localhost:9999"
|
||||
assert wrapped._api_key == "my-key"
|
||||
|
||||
def test_wrap_openai_all_settings_kwargs(self):
|
||||
"""Test wrap_openai() accepts all HindsightCallSettings fields as kwargs."""
|
||||
mock_client = MagicMock()
|
||||
|
||||
# Create kwargs with all settings
|
||||
settings_kwargs = {
|
||||
"bank_id": "test-bank",
|
||||
"document_id": "test-doc",
|
||||
"session_id": "test-session",
|
||||
"store_conversations": False,
|
||||
"inject_memories": False,
|
||||
"budget": "high",
|
||||
"fact_types": ["world", "experience"],
|
||||
"max_memories": 10,
|
||||
"max_memory_tokens": 2048,
|
||||
"include_entities": False,
|
||||
"trace": True,
|
||||
"tags": ["user:alice", "session:123"],
|
||||
"recall_tags": ["user:alice"],
|
||||
"recall_tags_match": "all",
|
||||
"use_reflect": True,
|
||||
"reflect_context": "test context",
|
||||
"reflect_response_schema": {"type": "object"},
|
||||
"reflect_include_facts": True,
|
||||
"query": "test query",
|
||||
"verbose": True,
|
||||
}
|
||||
|
||||
wrapped = wrap_openai(mock_client, **settings_kwargs)
|
||||
|
||||
# Verify all settings were applied
|
||||
for field_name, expected_value in settings_kwargs.items():
|
||||
actual_value = getattr(wrapped._default_settings, field_name)
|
||||
assert actual_value == expected_value, (
|
||||
f"Field {field_name}: expected {expected_value}, got {actual_value}"
|
||||
)
|
||||
|
||||
|
||||
class TestWrapAnthropic:
|
||||
"""Test wrap_anthropic() function."""
|
||||
@@ -164,7 +205,7 @@ class TestWrapAnthropic:
|
||||
wrapped = wrap_anthropic(mock_client)
|
||||
|
||||
assert isinstance(wrapped, HindsightAnthropic)
|
||||
assert wrapped._bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
assert wrapped._api_key == "test-key"
|
||||
|
||||
@@ -174,7 +215,7 @@ class TestWrapAnthropic:
|
||||
|
||||
wrapped = wrap_anthropic(mock_client)
|
||||
|
||||
assert wrapped._bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._default_settings.bank_id == DEFAULT_BANK_ID
|
||||
assert wrapped._api_url == DEFAULT_HINDSIGHT_API_URL
|
||||
|
||||
def test_wrap_anthropic_reads_api_key_from_env(self):
|
||||
@@ -197,6 +238,236 @@ class TestWrapAnthropic:
|
||||
api_key="my-key",
|
||||
)
|
||||
|
||||
assert wrapped._bank_id == "my-bank"
|
||||
assert wrapped._default_settings.bank_id == "my-bank"
|
||||
assert wrapped._api_url == "http://localhost:9999"
|
||||
assert wrapped._api_key == "my-key"
|
||||
|
||||
def test_wrap_anthropic_all_settings_kwargs(self):
|
||||
"""Test wrap_anthropic() accepts all HindsightCallSettings fields as kwargs."""
|
||||
mock_client = MagicMock()
|
||||
|
||||
# Create kwargs with all settings
|
||||
settings_kwargs = {
|
||||
"bank_id": "test-bank",
|
||||
"document_id": "test-doc",
|
||||
"session_id": "test-session",
|
||||
"store_conversations": False,
|
||||
"inject_memories": False,
|
||||
"budget": "high",
|
||||
"fact_types": ["world", "experience"],
|
||||
"max_memories": 10,
|
||||
"max_memory_tokens": 2048,
|
||||
"include_entities": False,
|
||||
"trace": True,
|
||||
"tags": ["user:alice", "session:123"],
|
||||
"recall_tags": ["user:alice"],
|
||||
"recall_tags_match": "all",
|
||||
"use_reflect": True,
|
||||
"reflect_context": "test context",
|
||||
"reflect_response_schema": {"type": "object"},
|
||||
"reflect_include_facts": True,
|
||||
"query": "test query",
|
||||
"verbose": True,
|
||||
}
|
||||
|
||||
wrapped = wrap_anthropic(mock_client, **settings_kwargs)
|
||||
|
||||
# Verify all settings were applied
|
||||
for field_name, expected_value in settings_kwargs.items():
|
||||
actual_value = getattr(wrapped._default_settings, field_name)
|
||||
assert actual_value == expected_value, (
|
||||
f"Field {field_name}: expected {expected_value}, got {actual_value}"
|
||||
)
|
||||
|
||||
|
||||
class TestMergeSettings:
|
||||
"""Test _merge_settings() function for per-call overrides."""
|
||||
|
||||
def test_merge_settings_no_overrides(self):
|
||||
"""Test _merge_settings() returns defaults when no overrides provided."""
|
||||
defaults = HindsightCallSettings(bank_id="default-bank", budget="mid")
|
||||
kwargs = {"model": "gpt-4", "messages": []} # No hindsight_* kwargs
|
||||
|
||||
merged = _merge_settings(defaults, kwargs)
|
||||
|
||||
assert merged.bank_id == "default-bank"
|
||||
assert merged.budget == "mid"
|
||||
|
||||
def test_merge_settings_with_overrides(self):
|
||||
"""Test _merge_settings() applies hindsight_* overrides."""
|
||||
defaults = HindsightCallSettings(bank_id="default-bank", budget="mid")
|
||||
kwargs = {
|
||||
"model": "gpt-4",
|
||||
"hindsight_bank_id": "override-bank",
|
||||
"hindsight_budget": "high",
|
||||
}
|
||||
|
||||
merged = _merge_settings(defaults, kwargs)
|
||||
|
||||
assert merged.bank_id == "override-bank"
|
||||
assert merged.budget == "high"
|
||||
|
||||
def test_merge_settings_partial_override(self):
|
||||
"""Test _merge_settings() only overrides specified fields."""
|
||||
defaults = HindsightCallSettings(
|
||||
bank_id="default-bank",
|
||||
budget="mid",
|
||||
verbose=True,
|
||||
max_memories=5,
|
||||
)
|
||||
kwargs = {
|
||||
"hindsight_bank_id": "override-bank",
|
||||
# budget, verbose, max_memories not overridden
|
||||
}
|
||||
|
||||
merged = _merge_settings(defaults, kwargs)
|
||||
|
||||
assert merged.bank_id == "override-bank" # Overridden
|
||||
assert merged.budget == "mid" # Default preserved
|
||||
assert merged.verbose is True # Default preserved
|
||||
assert merged.max_memories == 5 # Default preserved
|
||||
|
||||
def test_merge_settings_ignores_invalid_fields(self):
|
||||
"""Test _merge_settings() ignores hindsight_* kwargs for non-existent fields."""
|
||||
defaults = HindsightCallSettings(bank_id="default-bank")
|
||||
kwargs = {
|
||||
"hindsight_bank_id": "valid-override",
|
||||
"hindsight_nonexistent_field": "should-be-ignored",
|
||||
}
|
||||
|
||||
merged = _merge_settings(defaults, kwargs)
|
||||
|
||||
assert merged.bank_id == "valid-override"
|
||||
assert not hasattr(merged, "nonexistent_field")
|
||||
|
||||
def test_merge_settings_all_fields(self):
|
||||
"""Test _merge_settings() works with all HindsightCallSettings fields."""
|
||||
# Create defaults with non-default values
|
||||
defaults = HindsightCallSettings(
|
||||
bank_id="default-bank",
|
||||
document_id="default-doc",
|
||||
session_id="default-session",
|
||||
store_conversations=True,
|
||||
inject_memories=True,
|
||||
budget="mid",
|
||||
fact_types=None,
|
||||
max_memories=None,
|
||||
max_memory_tokens=4096,
|
||||
include_entities=True,
|
||||
trace=False,
|
||||
tags=None,
|
||||
recall_tags=None,
|
||||
recall_tags_match="any",
|
||||
use_reflect=False,
|
||||
reflect_context=None,
|
||||
reflect_response_schema=None,
|
||||
reflect_include_facts=False,
|
||||
query=None,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Override every field
|
||||
override_values = {
|
||||
"hindsight_bank_id": "override-bank",
|
||||
"hindsight_document_id": "override-doc",
|
||||
"hindsight_session_id": "override-session",
|
||||
"hindsight_store_conversations": False,
|
||||
"hindsight_inject_memories": False,
|
||||
"hindsight_budget": "high",
|
||||
"hindsight_fact_types": ["world", "experience"],
|
||||
"hindsight_max_memories": 10,
|
||||
"hindsight_max_memory_tokens": 2048,
|
||||
"hindsight_include_entities": False,
|
||||
"hindsight_trace": True,
|
||||
"hindsight_tags": ["user:bob", "org:acme"],
|
||||
"hindsight_recall_tags": ["user:bob"],
|
||||
"hindsight_recall_tags_match": "all",
|
||||
"hindsight_use_reflect": True,
|
||||
"hindsight_reflect_context": "test context",
|
||||
"hindsight_reflect_response_schema": {"type": "object"},
|
||||
"hindsight_reflect_include_facts": True,
|
||||
"hindsight_query": "test query",
|
||||
"hindsight_verbose": True,
|
||||
}
|
||||
|
||||
merged = _merge_settings(defaults, override_values)
|
||||
|
||||
# Verify all fields were overridden
|
||||
assert merged.bank_id == "override-bank"
|
||||
assert merged.document_id == "override-doc"
|
||||
assert merged.session_id == "override-session"
|
||||
assert merged.store_conversations is False
|
||||
assert merged.inject_memories is False
|
||||
assert merged.budget == "high"
|
||||
assert merged.fact_types == ["world", "experience"]
|
||||
assert merged.max_memories == 10
|
||||
assert merged.max_memory_tokens == 2048
|
||||
assert merged.include_entities is False
|
||||
assert merged.trace is True
|
||||
assert merged.tags == ["user:bob", "org:acme"]
|
||||
assert merged.recall_tags == ["user:bob"]
|
||||
assert merged.recall_tags_match == "all"
|
||||
assert merged.use_reflect is True
|
||||
assert merged.reflect_context == "test context"
|
||||
assert merged.reflect_response_schema == {"type": "object"}
|
||||
assert merged.reflect_include_facts is True
|
||||
assert merged.query == "test query"
|
||||
assert merged.verbose is True
|
||||
|
||||
|
||||
class TestHindsightCallSettingsConsistency:
|
||||
"""Test that HindsightCallSettings works consistently across wrappers."""
|
||||
|
||||
def test_same_settings_for_openai_and_anthropic(self):
|
||||
"""Test that OpenAI and Anthropic wrappers use the same HindsightCallSettings."""
|
||||
mock_openai_client = MagicMock()
|
||||
mock_anthropic_client = MagicMock()
|
||||
|
||||
settings_kwargs = {
|
||||
"bank_id": "shared-bank",
|
||||
"budget": "high",
|
||||
"use_reflect": True,
|
||||
"verbose": True,
|
||||
}
|
||||
|
||||
openai_wrapped = wrap_openai(mock_openai_client, **settings_kwargs)
|
||||
anthropic_wrapped = wrap_anthropic(mock_anthropic_client, **settings_kwargs)
|
||||
|
||||
# Both should have identical settings
|
||||
assert (
|
||||
openai_wrapped._default_settings.bank_id
|
||||
== anthropic_wrapped._default_settings.bank_id
|
||||
)
|
||||
assert (
|
||||
openai_wrapped._default_settings.budget
|
||||
== anthropic_wrapped._default_settings.budget
|
||||
)
|
||||
assert (
|
||||
openai_wrapped._default_settings.use_reflect
|
||||
== anthropic_wrapped._default_settings.use_reflect
|
||||
)
|
||||
assert (
|
||||
openai_wrapped._default_settings.verbose
|
||||
== anthropic_wrapped._default_settings.verbose
|
||||
)
|
||||
|
||||
def test_new_field_works_for_both_wrappers(self):
|
||||
"""Test that all HindsightCallSettings fields work for both wrappers."""
|
||||
mock_openai_client = MagicMock()
|
||||
mock_anthropic_client = MagicMock()
|
||||
|
||||
# Get all fields from the dataclass
|
||||
all_field_names = [f.name for f in fields(HindsightCallSettings)]
|
||||
|
||||
# Verify both wrappers can access all fields
|
||||
openai_wrapped = wrap_openai(mock_openai_client)
|
||||
anthropic_wrapped = wrap_anthropic(mock_anthropic_client)
|
||||
|
||||
for field_name in all_field_names:
|
||||
# Both should have the field accessible
|
||||
assert hasattr(openai_wrapped._default_settings, field_name), (
|
||||
f"OpenAI wrapper missing field: {field_name}"
|
||||
)
|
||||
assert hasattr(anthropic_wrapped._default_settings, field_name), (
|
||||
f"Anthropic wrapper missing field: {field_name}"
|
||||
)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Integration tests for hindsight-litellm."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from hindsight_litellm import (
|
||||
configure,
|
||||
@@ -15,8 +13,6 @@ from hindsight_litellm import (
|
||||
get_config,
|
||||
is_configured,
|
||||
reset_config,
|
||||
HindsightConfig,
|
||||
HindsightDefaults,
|
||||
MemoryInjectionMode,
|
||||
)
|
||||
from hindsight_litellm.callbacks import HindsightCallback
|
||||
@@ -129,11 +125,12 @@ class TestEnableDisable:
|
||||
with pytest.raises(RuntimeError, match="not configured"):
|
||||
enable()
|
||||
|
||||
def test_enable_without_bank_id_raises(self):
|
||||
"""Test enable raises error without bank_id."""
|
||||
def test_enable_with_default_bank_id_works(self):
|
||||
"""Test enable works with default bank_id (no explicit bank_id required)."""
|
||||
configure(hindsight_api_url="http://localhost:8888")
|
||||
with pytest.raises(RuntimeError, match="bank_id not set"):
|
||||
enable()
|
||||
# Should work - configure() provides default bank_id="default"
|
||||
enable()
|
||||
assert is_enabled() is True
|
||||
|
||||
def test_enable_sets_enabled_flag(self):
|
||||
"""Test enable sets the enabled flag."""
|
||||
@@ -209,7 +206,10 @@ class TestCallback:
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{"type": "image_url", "image_url": {"url": "http://example.com/img.png"}},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "http://example.com/img.png"},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -294,7 +294,9 @@ class TestCallback:
|
||||
]
|
||||
memory_context = "# Relevant Memories\n1. User is John"
|
||||
|
||||
result = callback._inject_memories_into_messages(messages, memory_context, config)
|
||||
result = callback._inject_memories_into_messages(
|
||||
messages, memory_context, config
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["role"] == "system"
|
||||
@@ -319,7 +321,9 @@ class TestCallback:
|
||||
]
|
||||
memory_context = "# Relevant Memories\n1. User is John"
|
||||
|
||||
result = callback._inject_memories_into_messages(messages, memory_context, config)
|
||||
result = callback._inject_memories_into_messages(
|
||||
messages, memory_context, config
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["role"] == "system"
|
||||
@@ -343,7 +347,9 @@ class TestCallback:
|
||||
]
|
||||
memory_context = "# Relevant Memories\n1. User is John"
|
||||
|
||||
result = callback._inject_memories_into_messages(messages, memory_context, config)
|
||||
result = callback._inject_memories_into_messages(
|
||||
messages, memory_context, config
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
@@ -523,6 +529,7 @@ class TestFactTypes:
|
||||
|
||||
assert defaults.fact_types is None
|
||||
|
||||
|
||||
class TestSetDefaults:
|
||||
"""Tests for set_defaults functionality."""
|
||||
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
# Hindsight-OpenAI
|
||||
|
||||
Drop-in replacement for OpenAI Python client with automatic Hindsight integration.
|
||||
|
||||
## Overview
|
||||
|
||||
`hindsight-openai` is a transparent wrapper around the official OpenAI Python client that automatically:
|
||||
- 🧠 **Injects relevant memories** from your Hindsight system into conversations
|
||||
- 💾 **Stores conversation history** to Hindsight for future retrieval
|
||||
- 🔄 **Works seamlessly** with existing OpenAI code (just change the import)
|
||||
- ⚡ **Supports both sync and async** clients
|
||||
|
||||
## Installation
|
||||
|
||||
This package is part of the Hindsight workspace. Install from the root:
|
||||
|
||||
```bash
|
||||
# From repository root
|
||||
uv sync
|
||||
|
||||
# Or install just this package
|
||||
cd hindsight-integrations/openai
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from hindsight_openai import configure, OpenAI
|
||||
|
||||
# Configure Hindsight integration once
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
agent_id="my-agent",
|
||||
store_conversations=True,
|
||||
inject_memories=True,
|
||||
)
|
||||
|
||||
# Use OpenAI client as normal - Hindsight integration happens automatically
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
{"role": "user", "content": "What did we discuss about AI last week?"}
|
||||
]
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
```
|
||||
|
||||
### Async Usage
|
||||
|
||||
```python
|
||||
from hindsight_openai import configure, AsyncOpenAI
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
agent_id="my-agent",
|
||||
)
|
||||
|
||||
client = AsyncOpenAI(api_key="sk-...")
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
{"role": "user", "content": "Remind me about my preferences"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `configure()` function accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `hindsight_api_url` | str | `"http://localhost:8888"` | URL of your Hindsight API server |
|
||||
| `agent_id` | str | `None` | **Required.** Agent identifier for memory operations |
|
||||
| `api_key` | str | `None` | Optional API key for Hindsight authentication |
|
||||
| `store_conversations` | bool | `True` | Store conversations to Hindsight |
|
||||
| `inject_memories` | bool | `True` | Inject relevant memories into prompts |
|
||||
| `document_id` | str | `None` | Optional document ID for stored conversations |
|
||||
| `enabled` | bool | `True` | Master switch to enable/disable Hindsight integration |
|
||||
|
||||
## How It Works
|
||||
|
||||
### Memory Injection
|
||||
|
||||
When `inject_memories=True`, the wrapper:
|
||||
|
||||
1. Extracts the user's query from the last message
|
||||
2. Searches Hindsight for relevant memories using the query
|
||||
3. Injects the top memories as a system message before the conversation
|
||||
4. Sends the enhanced conversation to OpenAI
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
# Your code:
|
||||
messages = [
|
||||
{"role": "user", "content": "What's my favorite programming language?"}
|
||||
]
|
||||
|
||||
# What gets sent to OpenAI (automatically):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Relevant context from your memory:\n\n1. User prefers Python for its simplicity\n (Date: 2024-01-15)\n (Type: opinion)"
|
||||
},
|
||||
{"role": "user", "content": "What's my favorite programming language?"}
|
||||
]
|
||||
```
|
||||
|
||||
### Conversation Storage
|
||||
|
||||
When `store_conversations=True`, the wrapper:
|
||||
|
||||
1. Captures the conversation context (recent messages)
|
||||
2. Captures the assistant's response
|
||||
3. Stores the complete exchange to Hindsight asynchronously
|
||||
4. Tags it with context `"openai_conversation"` for filtering
|
||||
|
||||
This creates a searchable memory of all your AI conversations.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Disable for Specific Requests
|
||||
|
||||
```python
|
||||
from hindsight_openai import configure, OpenAI, reset_config
|
||||
|
||||
# Configure globally
|
||||
configure(hindsight_api_url="http://localhost:8888", agent_id="agent-1")
|
||||
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
# Normal request with Hindsight
|
||||
response1 = client.chat.completions.create(...)
|
||||
|
||||
# Temporarily disable
|
||||
reset_config()
|
||||
response2 = client.chat.completions.create(...) # No Hindsight integration
|
||||
|
||||
# Re-enable
|
||||
configure(hindsight_api_url="http://localhost:8888", agent_id="agent-1")
|
||||
```
|
||||
|
||||
### Using Document ID
|
||||
|
||||
Group related conversations together using a document ID:
|
||||
|
||||
```python
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
agent_id="my-agent",
|
||||
document_id="meeting-2024-01-15", # All conversations tagged with this ID
|
||||
)
|
||||
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
# All these calls will be stored under the same document
|
||||
response1 = client.chat.completions.create(...)
|
||||
response2 = client.chat.completions.create(...)
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
```python
|
||||
from hindsight_openai import cleanup_interceptor
|
||||
|
||||
# Clean up resources when done
|
||||
await cleanup_interceptor()
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.10
|
||||
- openai >= 1.0.0
|
||||
- httpx >= 0.23.0
|
||||
- A running Hindsight API server
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
uv run pytest tests
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
hindsight-integrations/openai/
|
||||
├── hindsight_openai/
|
||||
│ ├── __init__.py # Main exports
|
||||
│ ├── client.py # OpenAI client wrappers
|
||||
│ ├── config.py # Global configuration
|
||||
│ └── interceptor.py # Request/response interception logic
|
||||
├── tests/
|
||||
│ └── test_client.py # Test suite
|
||||
├── pyproject.toml # Package configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Part of the Hindsight project.
|
||||
@@ -1,71 +0,0 @@
|
||||
"""Hindsight-OpenAI: Drop-in replacement for OpenAI client with automatic Hindsight integration.
|
||||
|
||||
This package provides a transparent wrapper around the OpenAI Python client that
|
||||
automatically stores conversations and injects relevant memories from your Hindsight
|
||||
memory system.
|
||||
|
||||
Basic usage:
|
||||
>>> from hindsight_openai import configure, OpenAI
|
||||
>>>
|
||||
>>> # Configure Hindsight integration
|
||||
>>> configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... )
|
||||
>>>
|
||||
>>> # Use OpenAI client as normal - Hindsight integration is automatic
|
||||
>>> client = OpenAI(api_key="sk-...")
|
||||
>>> response = client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
... )
|
||||
|
||||
Async usage:
|
||||
>>> from hindsight_openai import configure, AsyncOpenAI
|
||||
>>>
|
||||
>>> configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... agent_id="my-agent",
|
||||
... )
|
||||
>>>
|
||||
>>> client = AsyncOpenAI(api_key="sk-...")
|
||||
>>> response = await client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "Tell me about quantum computing"}]
|
||||
... )
|
||||
|
||||
Configuration options:
|
||||
- hindsight_api_url: URL of your Hindsight API server
|
||||
- agent_id: Agent identifier for memory operations
|
||||
- api_key: Optional API key for Hindsight authentication
|
||||
- store_conversations: Whether to store conversations to Hindsight (default: True)
|
||||
- inject_memories: Whether to inject relevant memories (default: True)
|
||||
- memory_search_budget: Number of memories to retrieve (default: 10)
|
||||
- context_window: Number of conversation turns to store (default: 10)
|
||||
- enabled: Master switch to disable Hindsight integration (default: True)
|
||||
"""
|
||||
|
||||
from .client import OpenAI, AsyncOpenAI
|
||||
from .config import (
|
||||
configure,
|
||||
get_config,
|
||||
is_configured,
|
||||
reset_config,
|
||||
HindsightConfig,
|
||||
)
|
||||
from .interceptor import cleanup_interceptor
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"OpenAI",
|
||||
"AsyncOpenAI",
|
||||
"configure",
|
||||
"get_config",
|
||||
"is_configured",
|
||||
"reset_config",
|
||||
"cleanup_interceptor",
|
||||
"HindsightConfig",
|
||||
]
|
||||
@@ -1,169 +0,0 @@
|
||||
"""Drop-in replacement for OpenAI client with Hindsight integration."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Optional, List, Dict
|
||||
|
||||
from openai import OpenAI as _OpenAI, AsyncOpenAI as _AsyncOpenAI
|
||||
|
||||
from .config import get_config, is_configured
|
||||
from .interceptor import get_interceptor
|
||||
|
||||
|
||||
class _CompletionsWrapper:
|
||||
"""Wrapper for chat completions with Hindsight integration (sync)."""
|
||||
|
||||
def __init__(self, original_completions):
|
||||
"""Initialize wrapper with original completions object."""
|
||||
self._original = original_completions
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
"""Create a chat completion with Hindsight integration."""
|
||||
if not is_configured():
|
||||
return self._original.create(*args, **kwargs)
|
||||
|
||||
config = get_config()
|
||||
if not config.enabled:
|
||||
return self._original.create(*args, **kwargs)
|
||||
|
||||
messages = kwargs.get("messages", [])
|
||||
if not messages:
|
||||
return self._original.create(*args, **kwargs)
|
||||
|
||||
# Check if an event loop is already running (e.g., in Jupyter)
|
||||
try:
|
||||
running_loop = asyncio.get_running_loop()
|
||||
# If we get here, a loop is already running
|
||||
print(
|
||||
"Warning: Detected running event loop (Jupyter/IPython). "
|
||||
"Hindsight features are disabled in sync mode. "
|
||||
"Please use AsyncOpenAI for full functionality in notebooks."
|
||||
)
|
||||
return self._original.create(*args, **kwargs)
|
||||
except RuntimeError:
|
||||
# No loop running, we can create our own
|
||||
pass
|
||||
|
||||
# Run async operations in a new event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
# Inject memories if configured
|
||||
if config.inject_memories:
|
||||
interceptor = get_interceptor()
|
||||
modified_messages = loop.run_until_complete(
|
||||
interceptor.inject_memories(messages, config)
|
||||
)
|
||||
kwargs["messages"] = modified_messages
|
||||
|
||||
# Call original OpenAI API
|
||||
response = self._original.create(*args, **kwargs)
|
||||
|
||||
# Store conversation if configured
|
||||
if config.store_conversations:
|
||||
interceptor = get_interceptor()
|
||||
loop.run_until_complete(
|
||||
interceptor.store_conversation(
|
||||
kwargs["messages"], response, config
|
||||
)
|
||||
)
|
||||
|
||||
return response
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate all other attributes to the original completions."""
|
||||
return getattr(self._original, name)
|
||||
|
||||
|
||||
class _AsyncCompletionsWrapper:
|
||||
"""Wrapper for chat completions with Hindsight integration (async)."""
|
||||
|
||||
def __init__(self, original_completions):
|
||||
"""Initialize wrapper with original completions object."""
|
||||
self._original = original_completions
|
||||
|
||||
async def create(self, *args, **kwargs):
|
||||
"""Create a chat completion with Hindsight integration."""
|
||||
if not is_configured():
|
||||
return await self._original.create(*args, **kwargs)
|
||||
|
||||
config = get_config()
|
||||
if not config.enabled:
|
||||
return await self._original.create(*args, **kwargs)
|
||||
|
||||
messages = kwargs.get("messages", [])
|
||||
if not messages:
|
||||
return await self._original.create(*args, **kwargs)
|
||||
|
||||
# Inject memories if configured
|
||||
if config.inject_memories:
|
||||
interceptor = get_interceptor()
|
||||
modified_messages = await interceptor.inject_memories(messages, config)
|
||||
kwargs["messages"] = modified_messages
|
||||
|
||||
# Call original OpenAI API
|
||||
response = await self._original.create(*args, **kwargs)
|
||||
|
||||
# Store conversation if configured
|
||||
if config.store_conversations:
|
||||
interceptor = get_interceptor()
|
||||
await interceptor.store_conversation(
|
||||
kwargs["messages"], response, config
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate all other attributes to the original completions."""
|
||||
return getattr(self._original, name)
|
||||
|
||||
|
||||
class OpenAI(_OpenAI):
|
||||
"""Drop-in replacement for OpenAI client with Hindsight integration.
|
||||
|
||||
Usage:
|
||||
>>> from hindsight_openai import configure, OpenAI
|
||||
>>> configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... )
|
||||
>>> client = OpenAI(api_key="sk-...")
|
||||
>>> response = client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "Hello!"}]
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize OpenAI client with Hindsight integration."""
|
||||
super().__init__(*args, **kwargs)
|
||||
# Wrap chat completions with our interceptor
|
||||
self.chat.completions = _CompletionsWrapper(self.chat.completions)
|
||||
|
||||
|
||||
class AsyncOpenAI(_AsyncOpenAI):
|
||||
"""Drop-in replacement for AsyncOpenAI client with Hindsight integration.
|
||||
|
||||
Usage:
|
||||
>>> from hindsight_openai import configure, AsyncOpenAI
|
||||
>>> configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... )
|
||||
>>> client = AsyncOpenAI(api_key="sk-...")
|
||||
>>> response = await client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "Hello!"}]
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize AsyncOpenAI client with Hindsight integration."""
|
||||
super().__init__(*args, **kwargs)
|
||||
# Wrap chat completions with our interceptor
|
||||
self.chat.completions = _AsyncCompletionsWrapper(self.chat.completions)
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Global configuration for Hindsight-OpenAI integration."""
|
||||
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class HindsightConfig:
|
||||
"""Configuration for Hindsight integration.
|
||||
|
||||
Attributes:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
agent_id: Agent ID for memory operations
|
||||
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
|
||||
document_id: Optional document ID for stored conversations
|
||||
enabled: Master switch to enable/disable Hindsight integration
|
||||
"""
|
||||
|
||||
hindsight_api_url: str = "http://localhost:8888"
|
||||
agent_id: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
store_conversations: bool = True
|
||||
inject_memories: bool = True
|
||||
document_id: Optional[str] = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
# Global configuration instance
|
||||
_global_config: Optional[HindsightConfig] = None
|
||||
|
||||
|
||||
def configure(
|
||||
hindsight_api_url: str = "http://localhost:8888",
|
||||
agent_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
document_id: Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
) -> HindsightConfig:
|
||||
"""Configure global Hindsight integration settings.
|
||||
|
||||
Args:
|
||||
hindsight_api_url: URL of the Hindsight API server
|
||||
agent_id: Agent ID for memory operations (required)
|
||||
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
|
||||
document_id: Optional document ID for stored conversations
|
||||
enabled: Master switch to enable/disable Hindsight integration
|
||||
|
||||
Returns:
|
||||
The configured HindsightConfig instance
|
||||
|
||||
Example:
|
||||
>>> from hindsight_openai import configure, OpenAI
|
||||
>>> configure(
|
||||
... hindsight_api_url="http://localhost:8888",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... document_id="conversation-123",
|
||||
... )
|
||||
>>> client = OpenAI(api_key="...")
|
||||
"""
|
||||
global _global_config
|
||||
|
||||
_global_config = HindsightConfig(
|
||||
hindsight_api_url=hindsight_api_url,
|
||||
agent_id=agent_id,
|
||||
api_key=api_key,
|
||||
store_conversations=store_conversations,
|
||||
inject_memories=inject_memories,
|
||||
document_id=document_id,
|
||||
enabled=enabled,
|
||||
)
|
||||
|
||||
return _global_config
|
||||
|
||||
|
||||
def get_config() -> Optional[HindsightConfig]:
|
||||
"""Get the current global configuration.
|
||||
|
||||
Returns:
|
||||
The current HindsightConfig instance, or None if not configured
|
||||
"""
|
||||
return _global_config
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
"""Check if Hindsight has been configured.
|
||||
|
||||
Returns:
|
||||
True if configure() has been called, False otherwise
|
||||
"""
|
||||
return _global_config is not None and _global_config.enabled
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset the global configuration to None."""
|
||||
global _global_config
|
||||
_global_config = None
|
||||
@@ -1,246 +0,0 @@
|
||||
"""Request/response interceptor for OpenAI API calls."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
from .config import get_config, HindsightConfig
|
||||
|
||||
|
||||
class HindsightInterceptor:
|
||||
"""Intercepts OpenAI API calls to integrate with Hindsight."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the interceptor with a Hindsight client."""
|
||||
self._client: Optional[Hindsight] = None
|
||||
|
||||
def get_client(self, config: HindsightConfig) -> Hindsight:
|
||||
"""Get or create the Hindsight client."""
|
||||
if self._client is None:
|
||||
self._client = Hindsight(base_url=config.hindsight_api_url, timeout=30.0)
|
||||
return self._client
|
||||
|
||||
def close(self):
|
||||
"""Close the client."""
|
||||
if self._client is not None:
|
||||
self._client.close()
|
||||
self._client = None
|
||||
|
||||
async def inject_memories(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
config: HindsightConfig,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Inject relevant memories into messages before sending to OpenAI.
|
||||
"""
|
||||
if not config.enabled:
|
||||
return messages
|
||||
|
||||
try:
|
||||
# Extract user query from messages
|
||||
user_query = self._extract_user_query(messages)
|
||||
if not user_query:
|
||||
return messages
|
||||
|
||||
# Get client
|
||||
client = self.get_client(config)
|
||||
|
||||
# Search for relevant memories
|
||||
results = client.search(
|
||||
agent_id=config.agent_id,
|
||||
query=user_query,
|
||||
max_tokens=4096,
|
||||
thinking_budget=500,
|
||||
)
|
||||
|
||||
if not results:
|
||||
return messages
|
||||
|
||||
# Format memories and add to context
|
||||
memory_context = self._format_memories(results)
|
||||
|
||||
# Add memory context to system message or create new one
|
||||
updated_messages = self._add_memory_context(messages, memory_context)
|
||||
|
||||
return updated_messages
|
||||
|
||||
except Exception as e:
|
||||
# Don't fail the request if memory retrieval fails
|
||||
print(f"Warning: Failed to inject memories: {e}")
|
||||
return messages
|
||||
|
||||
async def process_request(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Process request before sending to OpenAI.
|
||||
Retrieves relevant memories and adds them to the context.
|
||||
"""
|
||||
config = get_config()
|
||||
return await self.inject_memories(messages, config)
|
||||
|
||||
async def store_conversation(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response: Any,
|
||||
config: HindsightConfig,
|
||||
) -> None:
|
||||
"""
|
||||
Store conversation in Hindsight after receiving response from OpenAI.
|
||||
"""
|
||||
if not config.enabled or not config.store_conversations:
|
||||
return
|
||||
|
||||
try:
|
||||
# Extract conversation context
|
||||
conversation = self._extract_conversation_context(messages, response)
|
||||
if not conversation:
|
||||
return
|
||||
|
||||
# Get client
|
||||
client = self.get_client(config)
|
||||
|
||||
# Store conversation as memories
|
||||
items = [
|
||||
{
|
||||
"content": msg["content"],
|
||||
"context": f"role:{msg['role']}",
|
||||
"event_date": datetime.now(),
|
||||
}
|
||||
for msg in conversation
|
||||
]
|
||||
|
||||
# Store to Hindsight (async)
|
||||
client.put_batch(
|
||||
agent_id=config.agent_id,
|
||||
items=items,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Don't fail the request if storage fails
|
||||
print(f"Warning: Failed to store conversation: {e}")
|
||||
|
||||
async def process_response(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Process response after receiving from OpenAI.
|
||||
Stores the conversation in Hindsight.
|
||||
"""
|
||||
config = get_config()
|
||||
await self.store_conversation(messages, response, config)
|
||||
|
||||
def _extract_user_query(self, messages: List[Dict[str, Any]]) -> Optional[str]:
|
||||
"""Extract the user's query from messages."""
|
||||
# Get the last user message
|
||||
for msg in reversed(messages):
|
||||
if msg.get("role") == "user":
|
||||
content = msg.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Handle structured content
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
return item.get("text")
|
||||
return None
|
||||
|
||||
def _format_memories(self, results: List[Dict[str, Any]]) -> str:
|
||||
"""Format memory search results into context string."""
|
||||
if not results:
|
||||
return ""
|
||||
|
||||
memory_lines = []
|
||||
for i, result in enumerate(results, 1):
|
||||
text = result.get("text", "")
|
||||
if text:
|
||||
memory_lines.append(f"{i}. {text}")
|
||||
|
||||
if not memory_lines:
|
||||
return ""
|
||||
|
||||
return (
|
||||
"# Relevant Memories\n"
|
||||
"The following memories may be relevant to this conversation:\n\n"
|
||||
+ "\n".join(memory_lines)
|
||||
)
|
||||
|
||||
def _add_memory_context(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
memory_context: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Add memory context to messages."""
|
||||
if not memory_context:
|
||||
return messages
|
||||
|
||||
# Check if there's already a system message
|
||||
updated_messages = messages.copy()
|
||||
|
||||
for i, msg in enumerate(updated_messages):
|
||||
if msg.get("role") == "system":
|
||||
# Append to existing system message
|
||||
updated_messages[i] = {
|
||||
**msg,
|
||||
"content": f"{msg['content']}\n\n{memory_context}"
|
||||
}
|
||||
return updated_messages
|
||||
|
||||
# No system message found, prepend one
|
||||
system_message = {
|
||||
"role": "system",
|
||||
"content": memory_context
|
||||
}
|
||||
return [system_message] + updated_messages
|
||||
|
||||
def _extract_conversation_context(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response: Any,
|
||||
) -> List[Dict[str, str]]:
|
||||
"""Extract conversation context for storage."""
|
||||
conversation = []
|
||||
|
||||
# Add recent user messages
|
||||
for msg in messages[-3:]: # Last 3 messages
|
||||
if msg.get("role") in ("user", "assistant"):
|
||||
content = msg.get("content")
|
||||
if isinstance(content, str):
|
||||
conversation.append({
|
||||
"role": msg["role"],
|
||||
"content": content,
|
||||
})
|
||||
|
||||
# Add assistant response
|
||||
if hasattr(response, "choices") and response.choices:
|
||||
choice = response.choices[0]
|
||||
if hasattr(choice, "message"):
|
||||
content = choice.message.content
|
||||
if content:
|
||||
conversation.append({
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
})
|
||||
|
||||
return conversation
|
||||
|
||||
|
||||
# Global interceptor instance
|
||||
_interceptor = HindsightInterceptor()
|
||||
|
||||
|
||||
def get_interceptor() -> HindsightInterceptor:
|
||||
"""Get the global interceptor instance."""
|
||||
return _interceptor
|
||||
|
||||
|
||||
def cleanup_interceptor() -> None:
|
||||
"""Cleanup the global interceptor instance."""
|
||||
_interceptor.close()
|
||||
@@ -1,34 +0,0 @@
|
||||
[project]
|
||||
name = "hindsight-openai"
|
||||
version = "0.1.0"
|
||||
description = "Drop-in replacement for OpenAI client with automatic Hindsight integration"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"openai>=1.0.0",
|
||||
"hindsight-client",
|
||||
# Transitive dependency security fixes
|
||||
"aiohttp>=3.13.3", # Multiple DoS vulnerabilities
|
||||
"urllib3>=2.6.3", # Decompression-bomb safeguards bypass
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-client = { path = "../../hindsight-clients/python" }
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-mock>=3.10.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_openai"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
@@ -1,265 +0,0 @@
|
||||
"""Tests for Hindsight-OpenAI client wrapper."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from hindsight_openai import (
|
||||
configure,
|
||||
reset_config,
|
||||
OpenAI,
|
||||
AsyncOpenAI,
|
||||
is_configured,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup():
|
||||
"""Reset configuration after each test."""
|
||||
yield
|
||||
reset_config()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def groq_api_key():
|
||||
"""Get Groq API key from environment."""
|
||||
api_key = os.getenv("GROQ_API_KEY")
|
||||
if not api_key:
|
||||
pytest.skip("GROQ_API_KEY environment variable not set")
|
||||
return api_key
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hindsight_api_url():
|
||||
"""Get Hindsight API URL from environment."""
|
||||
return os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
|
||||
|
||||
class TestConfiguration:
|
||||
"""Test configuration management."""
|
||||
|
||||
def test_configure_basic(self):
|
||||
"""Test basic configuration."""
|
||||
config = configure(
|
||||
hindsight_api_url="http://test:8888",
|
||||
agent_id="test-agent",
|
||||
)
|
||||
|
||||
assert config.hindsight_api_url == "http://test:8888"
|
||||
assert config.agent_id == "test-agent"
|
||||
assert config.store_conversations is True
|
||||
assert config.inject_memories is True
|
||||
assert is_configured()
|
||||
|
||||
def test_configure_custom_options(self):
|
||||
"""Test configuration with custom options."""
|
||||
config = configure(
|
||||
hindsight_api_url="http://test:8888",
|
||||
agent_id="test-agent",
|
||||
store_conversations=False,
|
||||
inject_memories=False,
|
||||
document_id="test-doc",
|
||||
)
|
||||
|
||||
assert config.store_conversations is False
|
||||
assert config.inject_memories is False
|
||||
assert config.document_id == "test-doc"
|
||||
|
||||
def test_reset_config(self):
|
||||
"""Test resetting configuration."""
|
||||
configure(hindsight_api_url="http://test:8888", agent_id="test-agent")
|
||||
assert is_configured()
|
||||
|
||||
reset_config()
|
||||
assert not is_configured()
|
||||
|
||||
|
||||
class TestSyncClient:
|
||||
"""Test synchronous OpenAI client wrapper."""
|
||||
|
||||
def test_client_creation(self, groq_api_key):
|
||||
"""Test that client can be created."""
|
||||
configure(hindsight_api_url="http://test:8888", agent_id="test-agent")
|
||||
|
||||
client = OpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
assert client is not None
|
||||
assert hasattr(client.chat.completions, "_original")
|
||||
|
||||
def test_chat_completion_without_config(self, groq_api_key):
|
||||
"""Test that chat completion works without Hindsight configuration."""
|
||||
reset_config()
|
||||
|
||||
client = OpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'test' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
assert response.choices[0].message.content is not None
|
||||
|
||||
def test_wrapper_passthrough(self, groq_api_key, hindsight_api_url):
|
||||
"""Test that wrapper passes through when features disabled."""
|
||||
configure(
|
||||
hindsight_api_url=hindsight_api_url,
|
||||
agent_id="test-sync-passthrough",
|
||||
inject_memories=False,
|
||||
store_conversations=False,
|
||||
)
|
||||
|
||||
client = OpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'hello' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
|
||||
|
||||
class TestAsyncClient:
|
||||
"""Test asynchronous OpenAI client wrapper."""
|
||||
|
||||
def test_client_creation(self, groq_api_key):
|
||||
"""Test that async client can be created."""
|
||||
configure(hindsight_api_url="http://test:8888", agent_id="test-agent")
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
assert client is not None
|
||||
assert hasattr(client.chat.completions, "_original")
|
||||
|
||||
async def test_chat_completion_without_config(self, groq_api_key):
|
||||
"""Test that async chat completion works without Hindsight configuration."""
|
||||
reset_config()
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'test' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
assert response.choices[0].message.content is not None
|
||||
|
||||
async def test_wrapper_passthrough(self, groq_api_key, hindsight_api_url):
|
||||
"""Test that async wrapper passes through when features disabled."""
|
||||
configure(
|
||||
hindsight_api_url=hindsight_api_url,
|
||||
agent_id="test-async-passthrough",
|
||||
inject_memories=False,
|
||||
store_conversations=False,
|
||||
)
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'hello' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
|
||||
|
||||
class TestInterceptor:
|
||||
"""Test interceptor functionality."""
|
||||
|
||||
def test_extract_user_query_simple(self):
|
||||
"""Test extracting user query from simple messages."""
|
||||
from hindsight_openai.interceptor import HindsightInterceptor
|
||||
|
||||
interceptor = HindsightInterceptor()
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "What is Python?"},
|
||||
]
|
||||
|
||||
query = interceptor._extract_user_query(messages)
|
||||
assert query == "What is Python?"
|
||||
|
||||
def test_extract_user_query_structured(self):
|
||||
"""Test extracting user query from structured content."""
|
||||
from hindsight_openai.interceptor import HindsightInterceptor
|
||||
|
||||
interceptor = HindsightInterceptor()
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{"type": "image_url", "image_url": {"url": "https://..."}},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
query = interceptor._extract_user_query(messages)
|
||||
assert query == "What's in this image?"
|
||||
|
||||
def test_extract_conversation_context(self):
|
||||
"""Test extracting conversation context."""
|
||||
from hindsight_openai.interceptor import HindsightInterceptor
|
||||
from unittest.mock import Mock
|
||||
|
||||
interceptor = HindsightInterceptor()
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi! How can I help?"},
|
||||
{"role": "user", "content": "Tell me about AI"},
|
||||
]
|
||||
|
||||
# Mock response object
|
||||
mock_response = Mock()
|
||||
mock_response.choices = [Mock()]
|
||||
mock_response.choices[0].message = Mock()
|
||||
mock_response.choices[0].message.content = "AI stands for Artificial Intelligence"
|
||||
|
||||
context = interceptor._extract_conversation_context(messages, mock_response)
|
||||
|
||||
# Should include recent messages and response
|
||||
assert len(context) > 0
|
||||
assert any(msg["content"] == "Tell me about AI" for msg in context)
|
||||
assert any(msg["content"] == "AI stands for Artificial Intelligence" for msg in context)
|
||||
|
||||
def test_format_memories(self):
|
||||
"""Test formatting memories."""
|
||||
from hindsight_openai.interceptor import HindsightInterceptor
|
||||
|
||||
interceptor = HindsightInterceptor()
|
||||
memories = [
|
||||
{
|
||||
"text": "User likes Python",
|
||||
"event_date": "2024-01-01",
|
||||
"fact_type": "opinion",
|
||||
},
|
||||
{"text": "Working on AI project", "event_date": None, "fact_type": "world"},
|
||||
]
|
||||
|
||||
formatted = interceptor._format_memories(memories)
|
||||
assert "1. User likes Python" in formatted
|
||||
assert "2. Working on AI project" in formatted
|
||||
assert "Relevant Memories" in formatted
|
||||
@@ -1,163 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "# Hindsight-OpenAI Tutorial\n\n**A drop-in replacement for the OpenAI Python client with automatic memory integration**\n\n## What is Hindsight-OpenAI?\n\n`hindsight-openai` is a transparent wrapper around the official OpenAI Python client that automatically:\n\n- 🧠 **Injects relevant memories** from your Hindsight system into conversations\n- 💾 **Stores conversation history** to Hindsight for future retrieval \n- 🔄 **Works seamlessly** with existing OpenAI code (just change the import)\n- ⚡ **Supports both sync and async** clients\n\n## Why Use It?\n\n### The Problem\n\nAI assistants typically have no memory of previous conversations. Each interaction starts fresh, requiring you to:\n- Repeat context manually\n- Copy-paste relevant information\n- Build custom RAG pipelines\n- Manage conversation history yourself\n\n### The Solution\n\nHindsight-OpenAI gives your AI **automatic long-term memory**:\n- Remembers past conversations\n- Recalls user preferences and facts\n- Maintains context across sessions\n- Zero code changes to your existing OpenAI usage\n\n## Prerequisites\n\n1. **Hindsight API server running** (see main Hindsight README)\n2. **OpenAI API key** or compatible API (Groq, OpenRouter, etc.)\n3. **Python >= 3.10**"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Setup\n\nFirst, let's set up our environment and configure Hindsight integration:"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"jupyter": {
|
||||
"is_executing": true
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "import os\nfrom hindsight_openai import configure, AsyncOpenAI\n\n# Set your API keys\n# Option 1: Use Groq (fast and free)\nGROQ_API_KEY = os.getenv(\"GROQ_API_KEY\", \"your-groq-api-key\")\nif not GROQ_API_KEY:\n raise (\"GROQ_API_KEY not set\") \n\n# Option 2: Use OpenAI\n# OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\", \"sk-...\")\n\n# Configure Hindsight integration\nconfigure(\n hindsight_api_url=\"http://localhost:8888\", # Your Hindsight API server\n agent_id=\"tutorial-user\", # Unique ID for this user/agent\n store_conversations=True, # Auto-save conversations\n inject_memories=True, # Auto-inject relevant context\n)\n\nprint(\"✓ Hindsight configured successfully!\")\nprint(\"\")\nprint(\"NOTE: This tutorial uses AsyncOpenAI which works perfectly in Jupyter notebooks.\")\nprint(\"For regular Python scripts, you can use the sync OpenAI client instead.\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Example 1: Basic Usage (No Changes Needed!)\n\nUse the OpenAI client exactly as you normally would. Hindsight works transparently in the background."
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "# Create client (using Groq's OpenAI-compatible API)\nclient = AsyncOpenAI(\n api_key=GROQ_API_KEY,\n base_url=\"https://api.groq.com/openai/v1\",\n)\n\n# First conversation - establish some facts\nprint(\"=== First Conversation ===\")\nresponse = await client.chat.completions.create(\n model=\"llama-3.1-8b-instant\",\n messages=[\n {\"role\": \"user\", \"content\": \"My name is Alice and I love Python programming!\"}\n ],\n)\n\nprint(f\"Assistant: {response.choices[0].message.content}\\n\")\nprint(\"→ This conversation is now stored in Hindsight!\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Example 2: Memory Injection in Action\n\nNow ask a question that requires remembering the previous conversation:"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "print(\"=== Second Conversation (with memory) ===\")\nresponse = await client.chat.completions.create(\n model=\"llama-3.1-8b-instant\",\n messages=[\n {\"role\": \"user\", \"content\": \"What's my name and what do I like?\"}\n ],\n)\n\nprint(f\"Assistant: {response.choices[0].message.content}\\n\")\nprint(\"→ Hindsight automatically injected relevant memories before this request!\")\nprint(\"→ The AI knew your name and preferences without you repeating them.\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## How It Works\n\nBehind the scenes, Hindsight-OpenAI:\n\n### 1. **Memory Storage**\nAfter each API call:\n- Captures the full conversation context\n- Stores it in Hindsight's semantic memory system\n- Indexes it for fast retrieval\n\n### 2. **Memory Injection**\nBefore each API call:\n- Extracts the user's query\n- Searches Hindsight for relevant past conversations\n- Injects top memories as a system message\n\n### What Gets Sent to OpenAI\n\nWithout Hindsight:\n```python\nmessages = [\n {\"role\": \"user\", \"content\": \"What's my name?\"}\n]\n```\n\nWith Hindsight (automatic):\n```python\nmessages = [\n {\n \"role\": \"system\",\n \"content\": \"Relevant context from your memory:\\n\\n1. User's name is Alice\\n (Date: 2024-11-18)\\n (Type: world)\"\n },\n {\"role\": \"user\", \"content\": \"What's my name?\"}\n]\n```"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example 3: Multi-Turn Conversations\n",
|
||||
"\n",
|
||||
"Build up context over multiple interactions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Conversation 1: Share a preference\n",
|
||||
"print(\"=== Conversation 1: Sharing preferences ===\")\n",
|
||||
"response = await client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"I'm working on a machine learning project using PyTorch.\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"\n",
|
||||
"# Conversation 2: Different topic\n",
|
||||
"print(\"=== Conversation 2: Different topic ===\")\n",
|
||||
"response = await client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"I prefer functional programming over OOP.\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"\n",
|
||||
"# Conversation 3: Ask for recommendations\n",
|
||||
"print(\"=== Conversation 3: Getting personalized advice ===\")\n",
|
||||
"response = await client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"Can you recommend a good book for me based on what you know?\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"print(\"→ The AI used your programming interests and preferences to make recommendations!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Example 4: Document Grouping\n\nGroup related conversations using `document_id`:"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "from hindsight_openai import configure\n\n# Configure with document ID for a specific project\nconfigure(\n hindsight_api_url=\"http://localhost:8888\",\n agent_id=\"tutorial-user\",\n document_id=\"ml-project-2024\", # All conversations tagged with this ID\n)\n\n# All these conversations will be grouped together\nconversations = [\n \"I'm using ResNet for image classification\",\n \"My dataset has 10,000 images\",\n \"Training accuracy is stuck at 65%\",\n]\n\nfor msg in conversations:\n response = await client.chat.completions.create(\n model=\"llama-3.1-8b-instant\",\n messages=[{\"role\": \"user\", \"content\": msg}],\n )\n print(f\"User: {msg}\")\n print(f\"Assistant: {response.choices[0].message.content}\\n\")\n\nprint(\"→ All these conversations are grouped under document 'ml-project-2024'\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Example 5: Async Support\n\nWorks perfectly with AsyncOpenAI for high-throughput applications:"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "from hindsight_openai import AsyncOpenAI\nimport asyncio\n\nasync def async_example():\n # Create async client\n async_client = AsyncOpenAI(\n api_key=GROQ_API_KEY,\n base_url=\"https://api.groq.com/openai/v1\",\n )\n \n # Store a fact\n print(\"=== Storing fact ===\")\n response = await async_client.chat.completions.create(\n model=\"llama-3.1-8b-instant\",\n messages=[{\"role\": \"user\", \"content\": \"My favorite color is blue.\"}],\n )\n print(f\"Assistant: {response.choices[0].message.content}\\n\")\n \n # Query with memory\n print(\"=== Querying with memory ===\")\n response = await async_client.chat.completions.create(\n model=\"llama-3.1-8b-instant\",\n messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n )\n print(f\"Assistant: {response.choices[0].message.content}\\n\")\n\n# Run async example\nawait async_example()\nprint(\"→ Async operations work seamlessly with Hindsight!\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Configuration Options\n\nFine-tune Hindsight's behavior:"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "from hindsight_openai import configure\n\n# Full configuration example\nconfigure(\n hindsight_api_url=\"http://localhost:8888\", # Hindsight API URL\n agent_id=\"my-agent\", # Agent identifier (required)\n api_key=None, # Optional Hindsight API key\n \n # Features\n store_conversations=True, # Store conversations automatically\n inject_memories=True, # Inject memories automatically\n \n # Organization\n document_id=\"session-123\", # Optional document grouping\n \n # Control\n enabled=True, # Master on/off switch\n)\n\nprint(\"Configuration options explained:\")\nprint(\"- store_conversations: Automatically save conversations to Hindsight\")\nprint(\"- inject_memories: Automatically retrieve and inject relevant context\")\nprint(\"- document_id: Group related conversations together\")\nprint(\"- enabled=False: Disable Hindsight without changing code\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Use Cases\n\n### 1. **Personal AI Assistant**\n- Remembers your preferences, work history, and interests\n- Provides personalized recommendations\n- Maintains context across days/weeks\n\n### 2. **Customer Support Chatbot**\n- Recalls previous support tickets\n- Knows customer preferences and history\n- Provides consistent, context-aware responses\n\n### 3. **Research Assistant**\n- Remembers documents you've discussed\n- Connects related topics from different sessions\n- Builds knowledge over time\n\n### 4. **Code Review Tool**\n- Remembers project architecture decisions\n- Recalls past code review comments\n- Maintains consistency across reviews\n\n## Benefits Summary\n\n✅ **Zero Code Changes** - Drop-in replacement for OpenAI client \n✅ **Automatic Context** - No manual RAG pipeline needed \n✅ **Long-term Memory** - Conversations persist across sessions \n✅ **Smart Retrieval** - Semantic search finds relevant context \n✅ **Both Sync/Async** - Works with any OpenAI client pattern \n✅ **Configurable** - Fine-tune behavior to your needs \n✅ **Transparent** - Original OpenAI responses unchanged \n\n## Next Steps\n\n- **Explore Hindsight API**: Check out the main Hindsight README for advanced features\n- **Customize Search**: Tune `memory_search_budget` for your use case\n- **Use Document IDs**: Organize conversations by project/session\n- **Try Different Models**: Works with OpenAI, Groq, Ollama, and more\n\n## Resources\n\n- [Hindsight Main README](../README.md) - Core memory system docs\n- [Hindsight-OpenAI README](README.md) - Package documentation\n- [OpenAI API Docs](https://platform.openai.com/docs/api-reference) - Original API reference"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.10"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Generated
-1299
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.uv.workspace]
|
||||
members = ["hindsight", "hindsight-api", "hindsight-dev", "hindsight-mcp-server", "hindsight-openai", "hindsight-langmem", "hindsight-clients/python", "hindsight-embed"]
|
||||
members = ["hindsight", "hindsight-api", "hindsight-dev", "hindsight-mcp-server", "hindsight-clients/python", "hindsight-embed"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = []
|
||||
|
||||
Reference in New Issue
Block a user