hindsight-all 0.4.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hindsight_all-0.4.6/.gitignore +56 -0
- hindsight_all-0.4.6/PKG-INFO +60 -0
- hindsight_all-0.4.6/README.md +48 -0
- hindsight_all-0.4.6/hindsight/__init__.py +53 -0
- hindsight_all-0.4.6/hindsight/server.py +280 -0
- hindsight_all-0.4.6/pyproject.toml +31 -0
- hindsight_all-0.4.6/tests/README.md +128 -0
- hindsight_all-0.4.6/tests/__init__.py +1 -0
- hindsight_all-0.4.6/tests/test_server_integration.py +289 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.mcp.json
|
|
9
|
+
.osgrep
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv
|
|
12
|
+
|
|
13
|
+
# Node
|
|
14
|
+
node_modules/
|
|
15
|
+
|
|
16
|
+
# Environment variables and local config
|
|
17
|
+
.env
|
|
18
|
+
docker-compose.yml
|
|
19
|
+
docker-compose.override.yml
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.idea/
|
|
23
|
+
.vscode/
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
|
|
27
|
+
# NLTK data (will be downloaded automatically)
|
|
28
|
+
nltk_data/
|
|
29
|
+
|
|
30
|
+
# Monitoring stack (Prometheus/Grafana binaries and data)
|
|
31
|
+
.monitoring/
|
|
32
|
+
.pgbouncer/
|
|
33
|
+
|
|
34
|
+
# Large benchmark datasets (will be downloaded automatically)
|
|
35
|
+
**/longmemeval_s_cleaned.json
|
|
36
|
+
|
|
37
|
+
# Debug logs
|
|
38
|
+
logs/
|
|
39
|
+
|
|
40
|
+
.DS_Store
|
|
41
|
+
|
|
42
|
+
# Generated docs files
|
|
43
|
+
hindsight-docs/static/llms-full.txt
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
hindsight-dev/benchmarks/locomo/results/
|
|
47
|
+
hindsight-dev/benchmarks/longmemeval/results/
|
|
48
|
+
hindsight-dev/benchmarks/consolidation/results/
|
|
49
|
+
benchmarks/results/
|
|
50
|
+
hindsight-cli/target
|
|
51
|
+
hindsight-clients/rust/target
|
|
52
|
+
.claude
|
|
53
|
+
whats-next.md
|
|
54
|
+
TASK.md
|
|
55
|
+
# Changelog is now tracked in hindsight-docs/src/pages/changelog.md
|
|
56
|
+
# CHANGELOG.md
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hindsight-all
|
|
3
|
+
Version: 0.4.6
|
|
4
|
+
Summary: Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: hindsight-api>=0.0.7
|
|
7
|
+
Requires-Dist: hindsight-client>=0.0.7
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
|
|
10
|
+
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# hindsight-all
|
|
14
|
+
|
|
15
|
+
All-in-one package for Hindsight - Agent Memory That Works Like Human Memory
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from hindsight import start_server, HindsightClient
|
|
21
|
+
|
|
22
|
+
# Start server with embedded PostgreSQL
|
|
23
|
+
server = start_server(
|
|
24
|
+
llm_provider="groq",
|
|
25
|
+
llm_api_key="your-api-key",
|
|
26
|
+
llm_model="openai/gpt-oss-120b"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create client
|
|
30
|
+
client = HindsightClient(base_url=server.url)
|
|
31
|
+
|
|
32
|
+
# Store memories
|
|
33
|
+
client.put(agent_id="assistant", content="User prefers Python for data analysis")
|
|
34
|
+
|
|
35
|
+
# Search memories
|
|
36
|
+
results = client.search(agent_id="assistant", query="programming preferences")
|
|
37
|
+
|
|
38
|
+
# Generate contextual response
|
|
39
|
+
response = client.think(agent_id="assistant", query="What languages should I recommend?")
|
|
40
|
+
|
|
41
|
+
# Stop server when done
|
|
42
|
+
server.stop()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Using Context Manager
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from hindsight import HindsightServer, HindsightClient
|
|
49
|
+
|
|
50
|
+
with HindsightServer(llm_provider="groq", llm_api_key="...") as server:
|
|
51
|
+
client = HindsightClient(base_url=server.url)
|
|
52
|
+
# ... use client ...
|
|
53
|
+
# Server automatically stops
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install hindsight-all
|
|
60
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# hindsight-all
|
|
2
|
+
|
|
3
|
+
All-in-one package for Hindsight - Agent Memory That Works Like Human Memory
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from hindsight import start_server, HindsightClient
|
|
9
|
+
|
|
10
|
+
# Start server with embedded PostgreSQL
|
|
11
|
+
server = start_server(
|
|
12
|
+
llm_provider="groq",
|
|
13
|
+
llm_api_key="your-api-key",
|
|
14
|
+
llm_model="openai/gpt-oss-120b"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Create client
|
|
18
|
+
client = HindsightClient(base_url=server.url)
|
|
19
|
+
|
|
20
|
+
# Store memories
|
|
21
|
+
client.put(agent_id="assistant", content="User prefers Python for data analysis")
|
|
22
|
+
|
|
23
|
+
# Search memories
|
|
24
|
+
results = client.search(agent_id="assistant", query="programming preferences")
|
|
25
|
+
|
|
26
|
+
# Generate contextual response
|
|
27
|
+
response = client.think(agent_id="assistant", query="What languages should I recommend?")
|
|
28
|
+
|
|
29
|
+
# Stop server when done
|
|
30
|
+
server.stop()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Using Context Manager
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from hindsight import HindsightServer, HindsightClient
|
|
37
|
+
|
|
38
|
+
with HindsightServer(llm_provider="groq", llm_api_key="...") as server:
|
|
39
|
+
client = HindsightClient(base_url=server.url)
|
|
40
|
+
# ... use client ...
|
|
41
|
+
# Server automatically stops
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install hindsight-all
|
|
48
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hindsight - All-in-one semantic memory system for AI agents.
|
|
3
|
+
|
|
4
|
+
This package provides a simple way to run Hindsight locally with embedded PostgreSQL.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from hindsight import start_server, HindsightClient
|
|
9
|
+
|
|
10
|
+
# Start server with embedded PostgreSQL (pg0)
|
|
11
|
+
server = start_server(
|
|
12
|
+
llm_provider="groq",
|
|
13
|
+
llm_api_key="your-api-key",
|
|
14
|
+
llm_model="openai/gpt-oss-120b"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Create client
|
|
18
|
+
client = HindsightClient(base_url=server.url)
|
|
19
|
+
|
|
20
|
+
# Store memories
|
|
21
|
+
client.put(agent_id="assistant", content="User prefers Python for data analysis")
|
|
22
|
+
|
|
23
|
+
# Search memories
|
|
24
|
+
results = client.search(agent_id="assistant", query="programming preferences")
|
|
25
|
+
|
|
26
|
+
# Generate contextual response
|
|
27
|
+
response = client.think(agent_id="assistant", query="What languages should I recommend?")
|
|
28
|
+
|
|
29
|
+
# Stop server when done
|
|
30
|
+
server.stop()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Using context manager:
|
|
34
|
+
```python
|
|
35
|
+
from hindsight import HindsightServer, HindsightClient
|
|
36
|
+
|
|
37
|
+
with HindsightServer(llm_provider="groq", llm_api_key="...") as server:
|
|
38
|
+
client = HindsightClient(base_url=server.url)
|
|
39
|
+
# ... use client ...
|
|
40
|
+
# Server automatically stops
|
|
41
|
+
```
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from .server import Server as HindsightServer, start_server
|
|
45
|
+
|
|
46
|
+
# Re-export Client from hindsight-client
|
|
47
|
+
from hindsight_client import Hindsight as HindsightClient
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"HindsightServer",
|
|
51
|
+
"start_server",
|
|
52
|
+
"HindsightClient",
|
|
53
|
+
]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Server module for running Hindsight in a background thread.
|
|
3
|
+
|
|
4
|
+
Provides a simple way to start and stop the Hindsight HTTP API server
|
|
5
|
+
without blocking the main thread.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import socket
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import uvicorn
|
|
15
|
+
from uvicorn import Config
|
|
16
|
+
|
|
17
|
+
from hindsight_api import MemoryEngine
|
|
18
|
+
from hindsight_api.api import create_app
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_free_port() -> int:
|
|
24
|
+
"""Find a free port on localhost."""
|
|
25
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
26
|
+
s.bind(("", 0))
|
|
27
|
+
s.listen(1)
|
|
28
|
+
port = s.getsockname()[1]
|
|
29
|
+
return port
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Server:
|
|
33
|
+
"""
|
|
34
|
+
Hindsight server that runs in a background thread.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```python
|
|
38
|
+
from hindsight import Server
|
|
39
|
+
|
|
40
|
+
server = Server(
|
|
41
|
+
db_url="pg0",
|
|
42
|
+
llm_provider="groq",
|
|
43
|
+
llm_api_key="your-api-key",
|
|
44
|
+
llm_model="openai/gpt-oss-120b"
|
|
45
|
+
)
|
|
46
|
+
server.start()
|
|
47
|
+
|
|
48
|
+
print(f"Server running at {server.url}")
|
|
49
|
+
|
|
50
|
+
# Use the server...
|
|
51
|
+
|
|
52
|
+
server.stop()
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
db_url: str = "pg0",
|
|
59
|
+
llm_provider: str = "groq",
|
|
60
|
+
llm_api_key: str = "",
|
|
61
|
+
llm_model: str = "openai/gpt-oss-120b",
|
|
62
|
+
llm_base_url: Optional[str] = None,
|
|
63
|
+
host: str = "127.0.0.1",
|
|
64
|
+
port: Optional[int] = None,
|
|
65
|
+
mcp_enabled: bool = False,
|
|
66
|
+
log_level: str = "info",
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize the Hindsight server.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
db_url: Database URL. Use "pg0" for embedded PostgreSQL.
|
|
73
|
+
llm_provider: LLM provider ("groq", "openai", "ollama", "gemini", "anthropic", "lmstudio")
|
|
74
|
+
llm_api_key: API key for the LLM provider
|
|
75
|
+
llm_model: Model name to use
|
|
76
|
+
llm_base_url: Optional custom base URL for LLM API
|
|
77
|
+
host: Host to bind to (default: 127.0.0.1)
|
|
78
|
+
port: Port to bind to (default: auto-select free port)
|
|
79
|
+
mcp_enabled: Whether to enable MCP server
|
|
80
|
+
log_level: Uvicorn log level (default: warning)
|
|
81
|
+
"""
|
|
82
|
+
self.db_url = db_url
|
|
83
|
+
self.llm_provider = llm_provider
|
|
84
|
+
self.llm_api_key = llm_api_key
|
|
85
|
+
self.llm_model = llm_model
|
|
86
|
+
self.llm_base_url = llm_base_url
|
|
87
|
+
self.host = host
|
|
88
|
+
self.port = port or _find_free_port()
|
|
89
|
+
self.mcp_enabled = mcp_enabled
|
|
90
|
+
self.log_level = log_level
|
|
91
|
+
|
|
92
|
+
self._memory: Optional[MemoryEngine] = None
|
|
93
|
+
self._server: Optional[uvicorn.Server] = None
|
|
94
|
+
self._thread: Optional[threading.Thread] = None
|
|
95
|
+
self._started = threading.Event()
|
|
96
|
+
self._stopped = threading.Event()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def url(self) -> str:
|
|
100
|
+
"""Get the server URL."""
|
|
101
|
+
return f"http://{self.host}:{self.port}"
|
|
102
|
+
|
|
103
|
+
def _run_server(self):
|
|
104
|
+
"""Run the server in a background thread."""
|
|
105
|
+
loop = asyncio.new_event_loop()
|
|
106
|
+
asyncio.set_event_loop(loop)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Create MemoryEngine
|
|
110
|
+
self._memory = MemoryEngine(
|
|
111
|
+
db_url=self.db_url,
|
|
112
|
+
memory_llm_provider=self.llm_provider,
|
|
113
|
+
memory_llm_api_key=self.llm_api_key,
|
|
114
|
+
memory_llm_model=self.llm_model,
|
|
115
|
+
memory_llm_base_url=self.llm_base_url,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Create FastAPI app
|
|
119
|
+
app = create_app(
|
|
120
|
+
memory=self._memory,
|
|
121
|
+
mcp_api_enabled=self.mcp_enabled,
|
|
122
|
+
initialize_memory=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Create uvicorn config and server
|
|
126
|
+
config = Config(
|
|
127
|
+
app=app,
|
|
128
|
+
host=self.host,
|
|
129
|
+
port=self.port,
|
|
130
|
+
log_level=self.log_level,
|
|
131
|
+
loop="asyncio",
|
|
132
|
+
)
|
|
133
|
+
self._server = uvicorn.Server(config)
|
|
134
|
+
|
|
135
|
+
# Signal that we're starting
|
|
136
|
+
self._started.set()
|
|
137
|
+
|
|
138
|
+
# Run the server
|
|
139
|
+
loop.run_until_complete(self._server.serve())
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Server error: {e}")
|
|
143
|
+
raise
|
|
144
|
+
finally:
|
|
145
|
+
# Cleanup
|
|
146
|
+
if self._memory:
|
|
147
|
+
loop.run_until_complete(self._memory.close())
|
|
148
|
+
loop.close()
|
|
149
|
+
self._stopped.set()
|
|
150
|
+
|
|
151
|
+
def start(self, timeout: float = 30.0) -> "Server":
|
|
152
|
+
"""
|
|
153
|
+
Start the server in a background thread.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
timeout: Maximum time to wait for server to start (seconds)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
self (for chaining)
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
RuntimeError: If server fails to start within timeout
|
|
163
|
+
"""
|
|
164
|
+
if self._thread is not None and self._thread.is_alive():
|
|
165
|
+
raise RuntimeError("Server is already running")
|
|
166
|
+
|
|
167
|
+
self._started.clear()
|
|
168
|
+
self._stopped.clear()
|
|
169
|
+
|
|
170
|
+
self._thread = threading.Thread(target=self._run_server, daemon=True)
|
|
171
|
+
self._thread.start()
|
|
172
|
+
|
|
173
|
+
# Wait for server to start
|
|
174
|
+
self._started.wait(timeout=timeout)
|
|
175
|
+
|
|
176
|
+
# Give uvicorn a moment to actually bind to the port
|
|
177
|
+
start_time = time.time()
|
|
178
|
+
while time.time() - start_time < timeout:
|
|
179
|
+
try:
|
|
180
|
+
with socket.create_connection((self.host, self.port), timeout=1):
|
|
181
|
+
logger.info(f"Hindsight server started at {self.url}")
|
|
182
|
+
return self
|
|
183
|
+
except (ConnectionRefusedError, socket.timeout, OSError):
|
|
184
|
+
time.sleep(0.1)
|
|
185
|
+
|
|
186
|
+
raise RuntimeError(f"Server failed to start within {timeout} seconds")
|
|
187
|
+
|
|
188
|
+
def stop(self, timeout: float = 10.0) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Stop the server.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
timeout: Maximum time to wait for server to stop (seconds)
|
|
194
|
+
"""
|
|
195
|
+
if self._server is None:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Signal uvicorn to shutdown
|
|
199
|
+
self._server.should_exit = True
|
|
200
|
+
|
|
201
|
+
# Wait for thread to finish
|
|
202
|
+
if self._thread is not None:
|
|
203
|
+
self._thread.join(timeout=timeout)
|
|
204
|
+
if self._thread.is_alive():
|
|
205
|
+
logger.warning("Server thread did not stop cleanly")
|
|
206
|
+
|
|
207
|
+
self._server = None
|
|
208
|
+
self._thread = None
|
|
209
|
+
logger.info("Hindsight server stopped")
|
|
210
|
+
|
|
211
|
+
def __enter__(self) -> "Server":
|
|
212
|
+
"""Context manager entry."""
|
|
213
|
+
return self.start()
|
|
214
|
+
|
|
215
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
216
|
+
"""Context manager exit."""
|
|
217
|
+
self.stop()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def start_server(
|
|
221
|
+
db_url: str = "pg0",
|
|
222
|
+
llm_provider: str = "groq",
|
|
223
|
+
llm_api_key: str = "",
|
|
224
|
+
llm_model: str = "openai/gpt-oss-120b",
|
|
225
|
+
llm_base_url: Optional[str] = None,
|
|
226
|
+
host: str = "127.0.0.1",
|
|
227
|
+
port: Optional[int] = None,
|
|
228
|
+
mcp_enabled: bool = False,
|
|
229
|
+
log_level: str = "warning",
|
|
230
|
+
timeout: float = 30.0,
|
|
231
|
+
) -> Server:
|
|
232
|
+
"""
|
|
233
|
+
Start a Hindsight server in a background thread.
|
|
234
|
+
|
|
235
|
+
This is a convenience function that creates and starts a Server instance.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
db_url: Database URL. Use "pg0" for embedded PostgreSQL.
|
|
239
|
+
llm_provider: LLM provider ("groq", "openai", "ollama", "gemini", "anthropic", "lmstudio")
|
|
240
|
+
llm_api_key: API key for the LLM provider
|
|
241
|
+
llm_model: Model name to use
|
|
242
|
+
llm_base_url: Optional custom base URL for LLM API
|
|
243
|
+
host: Host to bind to (default: 127.0.0.1)
|
|
244
|
+
port: Port to bind to (default: auto-select free port)
|
|
245
|
+
mcp_enabled: Whether to enable MCP server
|
|
246
|
+
log_level: Uvicorn log level (default: warning)
|
|
247
|
+
timeout: Maximum time to wait for server to start (seconds)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Running Server instance
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
```python
|
|
254
|
+
from hindsight import start_server, Client
|
|
255
|
+
|
|
256
|
+
server = start_server(
|
|
257
|
+
db_url="pg0",
|
|
258
|
+
llm_provider="groq",
|
|
259
|
+
llm_api_key="your-api-key",
|
|
260
|
+
llm_model="openai/gpt-oss-120b"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
client = Client(base_url=server.url)
|
|
264
|
+
client.put(agent_id="assistant", content="User likes Python")
|
|
265
|
+
|
|
266
|
+
server.stop()
|
|
267
|
+
```
|
|
268
|
+
"""
|
|
269
|
+
server = Server(
|
|
270
|
+
db_url=db_url,
|
|
271
|
+
llm_provider=llm_provider,
|
|
272
|
+
llm_api_key=llm_api_key,
|
|
273
|
+
llm_model=llm_model,
|
|
274
|
+
llm_base_url=llm_base_url,
|
|
275
|
+
host=host,
|
|
276
|
+
port=port,
|
|
277
|
+
mcp_enabled=mcp_enabled,
|
|
278
|
+
log_level=log_level,
|
|
279
|
+
)
|
|
280
|
+
return server.start(timeout=timeout)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hindsight-all"
|
|
7
|
+
version = "0.4.6"
|
|
8
|
+
description = "Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"hindsight-api>=0.0.7",
|
|
13
|
+
"hindsight-client>=0.0.7",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.uv.sources]
|
|
17
|
+
hindsight-api = { workspace = true }
|
|
18
|
+
hindsight-client = { workspace = true }
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
test = [
|
|
22
|
+
"pytest>=7.0.0",
|
|
23
|
+
"pytest-asyncio>=0.21.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["hindsight"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
asyncio_mode = "auto"
|
|
31
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Hindsight Integration Tests
|
|
2
|
+
|
|
3
|
+
This directory contains integration tests for the Hindsight all-in-one package.
|
|
4
|
+
|
|
5
|
+
## Test Overview
|
|
6
|
+
|
|
7
|
+
### `test_server_integration.py`
|
|
8
|
+
|
|
9
|
+
Comprehensive integration tests that verify the complete workflow:
|
|
10
|
+
|
|
11
|
+
1. **`test_server_context_manager_basic_workflow`**: Main integration test that:
|
|
12
|
+
- Starts the Hindsight server using a context manager
|
|
13
|
+
- Creates a memory bank with background information
|
|
14
|
+
- Stores multiple memories (both single and batch operations)
|
|
15
|
+
- Recalls memories based on different queries (programming preferences, ML topics)
|
|
16
|
+
- Reflects (generates contextual answers) multiple times with different contexts
|
|
17
|
+
- Automatically stops the server on context exit
|
|
18
|
+
|
|
19
|
+
2. **`test_server_manual_start_stop`**: Tests explicit server lifecycle management without context managers
|
|
20
|
+
|
|
21
|
+
3. **`test_server_with_client_context_manager`**: Tests nested context managers for both server and client
|
|
22
|
+
|
|
23
|
+
## Running the Tests
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
|
|
27
|
+
1. Install the hindsight package with test dependencies:
|
|
28
|
+
```bash
|
|
29
|
+
cd hindsight
|
|
30
|
+
uv pip install -e ".[test]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. Set up your LLM credentials in the `.env` file at the project root:
|
|
34
|
+
```bash
|
|
35
|
+
HINDSIGHT_API_LLM_PROVIDER=groq
|
|
36
|
+
HINDSIGHT_API_LLM_API_KEY=your-api-key
|
|
37
|
+
HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Run All Tests
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd hindsight
|
|
44
|
+
source ../.env
|
|
45
|
+
export HINDSIGHT_LLM_PROVIDER=$HINDSIGHT_API_LLM_PROVIDER
|
|
46
|
+
export HINDSIGHT_LLM_API_KEY=$HINDSIGHT_API_LLM_API_KEY
|
|
47
|
+
export HINDSIGHT_LLM_MODEL=$HINDSIGHT_API_LLM_MODEL
|
|
48
|
+
pytest tests/ -v
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Note on Parallel Execution**: These tests use embedded PostgreSQL (`pg0`), which is a singleton that cannot be shared across pytest-xdist worker processes. Therefore, these tests must run sequentially. Do not use `pytest -n` (parallel workers) with these tests.
|
|
52
|
+
|
|
53
|
+
**Why Random `bank_id` Values?**: Each test generates a unique `bank_id` using UUID. This provides several benefits:
|
|
54
|
+
- **Clean test isolation**: Tests don't interfere with each other's data
|
|
55
|
+
- **Repeatable runs**: Tests can be run multiple times without cleanup
|
|
56
|
+
- **Debugging**: Easy to identify which test created which data
|
|
57
|
+
- **Future-ready**: If you switch from `pg0` to a real PostgreSQL instance, these tests could run in parallel
|
|
58
|
+
|
|
59
|
+
### Run Specific Test
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pytest tests/test_server_integration.py::test_server_context_manager_basic_workflow -v
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Run with Verbose Output
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pytest tests/ -v -s
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `-s` flag shows print statements, which is useful to see the test progress.
|
|
72
|
+
|
|
73
|
+
### Run with Timeout
|
|
74
|
+
|
|
75
|
+
These tests involve LLM calls which can take time:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pytest tests/ --timeout=300
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Test Configuration
|
|
82
|
+
|
|
83
|
+
The tests use:
|
|
84
|
+
- **Embedded PostgreSQL** (`pg0`) for the database - no external database required
|
|
85
|
+
- **LLM provider** configured via environment variables
|
|
86
|
+
- **Automatic port allocation** for the server (no port conflicts)
|
|
87
|
+
|
|
88
|
+
## Expected Behavior
|
|
89
|
+
|
|
90
|
+
When tests run successfully, you should see:
|
|
91
|
+
1. Server starting on a random available port
|
|
92
|
+
2. Memory bank creation with background information
|
|
93
|
+
3. Multiple memories being stored (retain operations)
|
|
94
|
+
4. Memories being recalled based on different queries
|
|
95
|
+
5. Multiple reflection responses with contextual answers
|
|
96
|
+
6. Server automatically stopping (for context manager tests)
|
|
97
|
+
|
|
98
|
+
## Sample Output
|
|
99
|
+
|
|
100
|
+
The main test workflow demonstrates:
|
|
101
|
+
- **Step 1**: Create memory bank
|
|
102
|
+
- **Step 2**: Store 5 memories (3 individual + 2 batch)
|
|
103
|
+
- **Step 3**: Recall memories about programming preferences
|
|
104
|
+
- **Step 4**: Recall memories about machine learning
|
|
105
|
+
- **Step 5**: Reflect on tool recommendations
|
|
106
|
+
- **Step 6**: Reflect with additional context about framework choices
|
|
107
|
+
- **Step 7**: Server auto-stops via context manager
|
|
108
|
+
|
|
109
|
+
## Skipping Tests
|
|
110
|
+
|
|
111
|
+
Tests will automatically skip if:
|
|
112
|
+
- `HINDSIGHT_LLM_API_KEY` is not set
|
|
113
|
+
|
|
114
|
+
## Troubleshooting
|
|
115
|
+
|
|
116
|
+
### Test hangs or times out
|
|
117
|
+
- Increase timeout: `pytest tests/ --timeout=600`
|
|
118
|
+
- Check your LLM API key is valid
|
|
119
|
+
- Verify network connectivity to LLM provider
|
|
120
|
+
|
|
121
|
+
### Database errors
|
|
122
|
+
- The tests use embedded PostgreSQL (`pg0`) which should handle cleanup automatically
|
|
123
|
+
- If you see "database system is shutting down" errors, wait a few seconds and retry
|
|
124
|
+
- Between test runs, the embedded database needs time to properly shut down
|
|
125
|
+
|
|
126
|
+
### Port conflicts
|
|
127
|
+
- Tests automatically find free ports, so conflicts should be rare
|
|
128
|
+
- If you see port binding errors, check for other processes using high-numbered ports
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for the hindsight package."""
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration test for Hindsight server with context manager.
|
|
3
|
+
|
|
4
|
+
Tests the full workflow:
|
|
5
|
+
1. Starting server using context manager
|
|
6
|
+
2. Creating a memory bank
|
|
7
|
+
3. Storing memories (retain)
|
|
8
|
+
4. Recalling memories
|
|
9
|
+
5. Reflecting on memories
|
|
10
|
+
|
|
11
|
+
Note: These tests use embedded PostgreSQL (pg0) with a shared server instance
|
|
12
|
+
across all tests. Each test uses random bank_ids to avoid conflicts, allowing
|
|
13
|
+
safe parallel execution.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import uuid
|
|
18
|
+
import pytest
|
|
19
|
+
from hindsight import HindsightServer, HindsightClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(scope="session")
|
|
23
|
+
def llm_config():
|
|
24
|
+
"""Get LLM configuration from environment (session-scoped)."""
|
|
25
|
+
provider = os.getenv("HINDSIGHT_LLM_PROVIDER", "groq")
|
|
26
|
+
api_key = os.getenv("HINDSIGHT_LLM_API_KEY", "")
|
|
27
|
+
model = os.getenv("HINDSIGHT_LLM_MODEL", "openai/gpt-oss-120b")
|
|
28
|
+
|
|
29
|
+
if not api_key:
|
|
30
|
+
raise Exception("LLM API key not configured. Set HINDSIGHT_LLM_API_KEY environment variable.")
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
"llm_provider": provider,
|
|
34
|
+
"llm_api_key": api_key,
|
|
35
|
+
"llm_model": model,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture(scope="session")
|
|
40
|
+
def shared_server(llm_config):
|
|
41
|
+
"""
|
|
42
|
+
Shared server instance for all tests (session-scoped).
|
|
43
|
+
|
|
44
|
+
This allows tests to run in parallel by sharing the same pg0 instance,
|
|
45
|
+
while using different bank_ids to avoid data conflicts.
|
|
46
|
+
"""
|
|
47
|
+
server = HindsightServer(db_url="pg0", **llm_config)
|
|
48
|
+
server.start()
|
|
49
|
+
yield server
|
|
50
|
+
server.stop()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def client(shared_server):
|
|
55
|
+
"""Create a client connected to the shared server."""
|
|
56
|
+
return HindsightClient(base_url=shared_server.url)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_server_context_manager_basic_workflow(client):
|
|
60
|
+
"""
|
|
61
|
+
Test complete workflow using shared server.
|
|
62
|
+
|
|
63
|
+
This test:
|
|
64
|
+
1. Uses a shared server instance
|
|
65
|
+
2. Creates a memory bank with unique ID
|
|
66
|
+
3. Stores multiple memories
|
|
67
|
+
4. Recalls memories based on a query
|
|
68
|
+
5. Reflects (generates contextual answers) based on stored memories
|
|
69
|
+
"""
|
|
70
|
+
# Use random bank_id to allow parallel test execution
|
|
71
|
+
bank_id = f"test_assistant_{uuid.uuid4().hex[:8]}"
|
|
72
|
+
|
|
73
|
+
# Step 1: Create a memory bank with background information
|
|
74
|
+
print(f"\n1. Creating memory bank: {bank_id}")
|
|
75
|
+
bank_response = client.create_bank(
|
|
76
|
+
bank_id=bank_id,
|
|
77
|
+
name="Test Assistant",
|
|
78
|
+
background="An AI assistant that helps with programming and data analysis tasks."
|
|
79
|
+
)
|
|
80
|
+
assert "bank_id" in bank_response
|
|
81
|
+
assert bank_response["bank_id"] == bank_id
|
|
82
|
+
|
|
83
|
+
# Step 2: Store some memories about user preferences
|
|
84
|
+
print("\n2. Storing memories...")
|
|
85
|
+
|
|
86
|
+
# Store first memory
|
|
87
|
+
retain_response1 = client.retain(
|
|
88
|
+
bank_id=bank_id,
|
|
89
|
+
content="User prefers Python over JavaScript for data analysis projects.",
|
|
90
|
+
context="User conversation about programming languages"
|
|
91
|
+
)
|
|
92
|
+
assert retain_response1.get("success") is True
|
|
93
|
+
|
|
94
|
+
# Store second memory
|
|
95
|
+
retain_response2 = client.retain(
|
|
96
|
+
bank_id=bank_id,
|
|
97
|
+
content="User is working on a machine learning project using scikit-learn.",
|
|
98
|
+
context="Discussion about ML frameworks"
|
|
99
|
+
)
|
|
100
|
+
assert retain_response2.get("success") is True
|
|
101
|
+
|
|
102
|
+
# Store third memory
|
|
103
|
+
retain_response3 = client.retain(
|
|
104
|
+
bank_id=bank_id,
|
|
105
|
+
content="User likes visualizing data with matplotlib and seaborn.",
|
|
106
|
+
context="Conversation about data visualization"
|
|
107
|
+
)
|
|
108
|
+
assert retain_response3.get("success") is True
|
|
109
|
+
|
|
110
|
+
# Store batch memories
|
|
111
|
+
batch_response = client.retain_batch(
|
|
112
|
+
bank_id=bank_id,
|
|
113
|
+
items=[
|
|
114
|
+
{"content": "User is interested in neural networks and deep learning."},
|
|
115
|
+
{"content": "User asked about best practices for training models."},
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
# Check if the batch was submitted successfully (items_count shows how many were submitted)
|
|
119
|
+
assert batch_response.get("items_count", 0) >= 2
|
|
120
|
+
|
|
121
|
+
# Step 3: Recall memories based on a query
|
|
122
|
+
print("\n3. Recalling memories about programming preferences...")
|
|
123
|
+
recall_results = client.recall(
|
|
124
|
+
bank_id=bank_id,
|
|
125
|
+
query="What programming languages and tools does the user prefer?",
|
|
126
|
+
max_tokens=4096
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Verify recall results
|
|
130
|
+
assert isinstance(recall_results, list)
|
|
131
|
+
assert len(recall_results) > 0
|
|
132
|
+
print(f" Found {len(recall_results)} relevant memories")
|
|
133
|
+
|
|
134
|
+
# Check that results have expected structure
|
|
135
|
+
for result in recall_results:
|
|
136
|
+
assert "content" in result or "text" in result
|
|
137
|
+
print(f" - {result.get('content') or result.get('text', '')[:100]}")
|
|
138
|
+
|
|
139
|
+
# Step 4: Recall memories about machine learning
|
|
140
|
+
print("\n4. Recalling memories about machine learning...")
|
|
141
|
+
ml_recall_results = client.recall(
|
|
142
|
+
bank_id=bank_id,
|
|
143
|
+
query="machine learning and neural networks",
|
|
144
|
+
max_tokens=4096
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Verify recall results
|
|
148
|
+
assert isinstance(ml_recall_results, list)
|
|
149
|
+
assert len(ml_recall_results) > 0
|
|
150
|
+
print(f" Found {len(ml_recall_results)} ML-related memories")
|
|
151
|
+
for result in ml_recall_results[:3]: # Show first 3
|
|
152
|
+
print(f" - {result.get('content') or result.get('text', '')[:100]}")
|
|
153
|
+
|
|
154
|
+
# Step 5: Reflect (generate contextual answer based on memories)
|
|
155
|
+
print("\n5. Reflecting on query about recommendations...")
|
|
156
|
+
reflect_response = client.reflect(
|
|
157
|
+
bank_id=bank_id,
|
|
158
|
+
query="What tools and libraries should I recommend for this user's data analysis work?",
|
|
159
|
+
budget="mid"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Verify reflection response
|
|
163
|
+
assert isinstance(reflect_response, dict)
|
|
164
|
+
assert "answer" in reflect_response or "text" in reflect_response
|
|
165
|
+
|
|
166
|
+
answer = reflect_response.get("answer") or reflect_response.get("text", "")
|
|
167
|
+
assert len(answer) > 0
|
|
168
|
+
print(f" Answer: {answer[:200]}...")
|
|
169
|
+
|
|
170
|
+
# Verify the answer mentions relevant tools/libraries
|
|
171
|
+
answer_lower = answer.lower()
|
|
172
|
+
assert any(term in answer_lower for term in ["python", "scikit-learn", "matplotlib", "seaborn", "data"])
|
|
173
|
+
|
|
174
|
+
# Step 6: Another reflection with different context
|
|
175
|
+
print("\n6. Reflecting with additional context...")
|
|
176
|
+
reflect_with_context = client.reflect(
|
|
177
|
+
bank_id=bank_id,
|
|
178
|
+
query="Should I use TensorFlow or PyTorch?",
|
|
179
|
+
budget="low",
|
|
180
|
+
context="The user is starting a new deep learning project"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assert isinstance(reflect_with_context, dict)
|
|
184
|
+
context_answer = reflect_with_context.get("answer") or reflect_with_context.get("text", "")
|
|
185
|
+
assert len(context_answer) > 0
|
|
186
|
+
print(f" Context-aware answer: {context_answer[:150]}...")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_server_manual_start_stop(client):
|
|
190
|
+
"""
|
|
191
|
+
Test basic operations on shared server.
|
|
192
|
+
|
|
193
|
+
Verifies that basic bank operations work correctly.
|
|
194
|
+
"""
|
|
195
|
+
# Use random bank_id to allow parallel test execution
|
|
196
|
+
bank_id = f"test_manual_{uuid.uuid4().hex[:8]}"
|
|
197
|
+
|
|
198
|
+
# Create bank
|
|
199
|
+
bank_response = client.create_bank(
|
|
200
|
+
bank_id=bank_id,
|
|
201
|
+
name="Manual Test"
|
|
202
|
+
)
|
|
203
|
+
assert bank_response["bank_id"] == bank_id
|
|
204
|
+
|
|
205
|
+
# Store a memory
|
|
206
|
+
retain_response = client.retain(
|
|
207
|
+
bank_id=bank_id,
|
|
208
|
+
content="Testing manual server lifecycle."
|
|
209
|
+
)
|
|
210
|
+
assert retain_response.get("success") is True
|
|
211
|
+
|
|
212
|
+
# Recall the memory
|
|
213
|
+
recall_results = client.recall(
|
|
214
|
+
bank_id=bank_id,
|
|
215
|
+
query="server testing"
|
|
216
|
+
)
|
|
217
|
+
assert len(recall_results) >= 0 # May or may not find results immediately
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_server_with_client_context_manager(client):
|
|
221
|
+
"""
|
|
222
|
+
Test client context manager with shared server.
|
|
223
|
+
"""
|
|
224
|
+
# Use random bank_id to allow parallel test execution
|
|
225
|
+
bank_id = f"test_nested_context_{uuid.uuid4().hex[:8]}"
|
|
226
|
+
|
|
227
|
+
# Use client context manager (client fixture already provides this)
|
|
228
|
+
# Create bank
|
|
229
|
+
client.create_bank(bank_id=bank_id, name="Nested Context Test")
|
|
230
|
+
|
|
231
|
+
# Store memory
|
|
232
|
+
response = client.retain(
|
|
233
|
+
bank_id=bank_id,
|
|
234
|
+
content="Testing nested context managers."
|
|
235
|
+
)
|
|
236
|
+
assert response.get("success") is True
|
|
237
|
+
|
|
238
|
+
# Verify we can recall
|
|
239
|
+
results = client.recall(bank_id=bank_id, query="context")
|
|
240
|
+
assert isinstance(results, list)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_list_banks(client, shared_server):
|
|
244
|
+
"""
|
|
245
|
+
Test listing banks to verify bank_id field mapping.
|
|
246
|
+
|
|
247
|
+
This test verifies that the list_banks endpoint correctly returns
|
|
248
|
+
bank_id (not agent_id) in the response.
|
|
249
|
+
"""
|
|
250
|
+
# Create a couple of banks with random IDs to allow parallel test execution
|
|
251
|
+
test_suffix = uuid.uuid4().hex[:8]
|
|
252
|
+
bank1_id = f"test_bank_1_{test_suffix}"
|
|
253
|
+
bank2_id = f"test_bank_2_{test_suffix}"
|
|
254
|
+
|
|
255
|
+
client.create_bank(bank_id=bank1_id, name="Test Bank 1", background="First test bank")
|
|
256
|
+
client.create_bank(bank_id=bank2_id, name="Test Bank 2", background="Second test bank")
|
|
257
|
+
|
|
258
|
+
# List all banks using the generated client
|
|
259
|
+
import hindsight_client_api
|
|
260
|
+
from hindsight_client_api.api import default_api
|
|
261
|
+
|
|
262
|
+
config = hindsight_client_api.Configuration(host=shared_server.url)
|
|
263
|
+
api_client = hindsight_client_api.ApiClient(config)
|
|
264
|
+
api = default_api.DefaultApi(api_client)
|
|
265
|
+
|
|
266
|
+
# Call list_banks endpoint
|
|
267
|
+
import asyncio
|
|
268
|
+
loop = asyncio.get_event_loop()
|
|
269
|
+
response = loop.run_until_complete(api.list_banks())
|
|
270
|
+
|
|
271
|
+
# Verify response structure
|
|
272
|
+
assert hasattr(response, 'banks'), "Response should have 'banks' attribute"
|
|
273
|
+
assert len(response.banks) >= 2, f"Should have at least 2 banks, got {len(response.banks)}"
|
|
274
|
+
|
|
275
|
+
# Verify each bank has bank_id (not agent_id)
|
|
276
|
+
for bank in response.banks:
|
|
277
|
+
bank_dict = bank.to_dict() if hasattr(bank, 'to_dict') else bank
|
|
278
|
+
assert 'bank_id' in bank_dict, f"Bank should have 'bank_id' field, got: {bank_dict.keys()}"
|
|
279
|
+
assert 'agent_id' not in bank_dict, f"Bank should NOT have 'agent_id' field, got: {bank_dict.keys()}"
|
|
280
|
+
|
|
281
|
+
# Find our test banks
|
|
282
|
+
bank_ids = [b.bank_id if hasattr(b, 'bank_id') else b['bank_id'] for b in response.banks]
|
|
283
|
+
assert bank1_id in bank_ids, f"Should find {bank1_id} in bank list"
|
|
284
|
+
assert bank2_id in bank_ids, f"Should find {bank2_id} in bank list"
|
|
285
|
+
|
|
286
|
+
print(f"✓ Successfully listed {len(response.banks)} banks with correct bank_id field")
|
|
287
|
+
|
|
288
|
+
# Cleanup
|
|
289
|
+
loop.run_until_complete(api_client.close())
|