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.
@@ -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())