hindsight-api 0.3.0__py3-none-any.whl → 0.4.1__py3-none-any.whl
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_api/__init__.py +1 -1
- hindsight_api/admin/cli.py +59 -0
- hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
- hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
- hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
- hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
- hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
- hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
- hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
- hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
- hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
- hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
- hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
- hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
- hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
- hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
- hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
- hindsight_api/api/http.py +1120 -93
- hindsight_api/api/mcp.py +11 -191
- hindsight_api/config.py +174 -46
- hindsight_api/engine/consolidation/__init__.py +5 -0
- hindsight_api/engine/consolidation/consolidator.py +926 -0
- hindsight_api/engine/consolidation/prompts.py +77 -0
- hindsight_api/engine/cross_encoder.py +153 -22
- hindsight_api/engine/directives/__init__.py +5 -0
- hindsight_api/engine/directives/models.py +37 -0
- hindsight_api/engine/embeddings.py +136 -13
- hindsight_api/engine/interface.py +32 -13
- hindsight_api/engine/llm_wrapper.py +505 -43
- hindsight_api/engine/memory_engine.py +2101 -1094
- hindsight_api/engine/mental_models/__init__.py +14 -0
- hindsight_api/engine/mental_models/models.py +53 -0
- hindsight_api/engine/reflect/__init__.py +18 -0
- hindsight_api/engine/reflect/agent.py +933 -0
- hindsight_api/engine/reflect/models.py +109 -0
- hindsight_api/engine/reflect/observations.py +186 -0
- hindsight_api/engine/reflect/prompts.py +483 -0
- hindsight_api/engine/reflect/tools.py +437 -0
- hindsight_api/engine/reflect/tools_schema.py +250 -0
- hindsight_api/engine/response_models.py +130 -4
- hindsight_api/engine/retain/bank_utils.py +79 -201
- hindsight_api/engine/retain/fact_extraction.py +81 -48
- hindsight_api/engine/retain/fact_storage.py +5 -8
- hindsight_api/engine/retain/link_utils.py +5 -8
- hindsight_api/engine/retain/orchestrator.py +1 -55
- hindsight_api/engine/retain/types.py +2 -2
- hindsight_api/engine/search/graph_retrieval.py +2 -2
- hindsight_api/engine/search/link_expansion_retrieval.py +164 -29
- hindsight_api/engine/search/mpfp_retrieval.py +1 -1
- hindsight_api/engine/search/retrieval.py +14 -14
- hindsight_api/engine/search/think_utils.py +41 -140
- hindsight_api/engine/search/trace.py +0 -1
- hindsight_api/engine/search/tracer.py +2 -5
- hindsight_api/engine/search/types.py +0 -3
- hindsight_api/engine/task_backend.py +112 -196
- hindsight_api/engine/utils.py +0 -151
- hindsight_api/extensions/__init__.py +10 -1
- hindsight_api/extensions/builtin/tenant.py +11 -4
- hindsight_api/extensions/operation_validator.py +81 -4
- hindsight_api/extensions/tenant.py +26 -0
- hindsight_api/main.py +28 -5
- hindsight_api/mcp_local.py +12 -53
- hindsight_api/mcp_tools.py +494 -0
- hindsight_api/models.py +0 -2
- hindsight_api/worker/__init__.py +11 -0
- hindsight_api/worker/main.py +296 -0
- hindsight_api/worker/poller.py +486 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.1.dist-info}/METADATA +12 -6
- hindsight_api-0.4.1.dist-info/RECORD +112 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.1.dist-info}/entry_points.txt +1 -0
- hindsight_api/engine/retain/observation_regeneration.py +0 -254
- hindsight_api/engine/search/observation_utils.py +0 -125
- hindsight_api/engine/search/scoring.py +0 -159
- hindsight_api-0.3.0.dist-info/RECORD +0 -82
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Shared MCP tool implementations for Hindsight.
|
|
2
|
+
|
|
3
|
+
This module provides the core tool logic used by both:
|
|
4
|
+
- mcp_local.py (stdio transport for Claude Code)
|
|
5
|
+
- api/mcp.py (HTTP transport for API server)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
from hindsight_api import MemoryEngine
|
|
17
|
+
from hindsight_api.config import (
|
|
18
|
+
DEFAULT_MCP_RECALL_DESCRIPTION,
|
|
19
|
+
DEFAULT_MCP_RETAIN_DESCRIPTION,
|
|
20
|
+
)
|
|
21
|
+
from hindsight_api.engine.memory_engine import Budget
|
|
22
|
+
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
|
23
|
+
from hindsight_api.models import RequestContext
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class MCPToolsConfig:
|
|
30
|
+
"""Configuration for MCP tools registration."""
|
|
31
|
+
|
|
32
|
+
# How to resolve bank_id for operations
|
|
33
|
+
bank_id_resolver: Callable[[], str | None]
|
|
34
|
+
|
|
35
|
+
# Whether to include bank_id as a parameter on tools (for multi-bank support)
|
|
36
|
+
include_bank_id_param: bool = False
|
|
37
|
+
|
|
38
|
+
# Which tools to register
|
|
39
|
+
tools: set[str] | None = None # None means all tools
|
|
40
|
+
|
|
41
|
+
# Custom descriptions (if None, uses defaults)
|
|
42
|
+
retain_description: str | None = None
|
|
43
|
+
recall_description: str | None = None
|
|
44
|
+
|
|
45
|
+
# Retain behavior
|
|
46
|
+
retain_fire_and_forget: bool = False # If True, use asyncio.create_task pattern
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_timestamp(timestamp: str) -> datetime | None:
|
|
50
|
+
"""Parse an ISO format timestamp string.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
timestamp: ISO format timestamp (e.g., '2024-01-15T10:30:00Z')
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Parsed datetime or None if invalid
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If timestamp format is invalid
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
return datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Invalid timestamp format '{timestamp}'. "
|
|
66
|
+
"Expected ISO format like '2024-01-15T10:30:00' or '2024-01-15T10:30:00Z'"
|
|
67
|
+
) from e
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_content_dict(
|
|
71
|
+
content: str,
|
|
72
|
+
context: str,
|
|
73
|
+
timestamp: str | None = None,
|
|
74
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
75
|
+
"""Build a content dict for retain operations.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
content: The memory content
|
|
79
|
+
context: Category for the memory
|
|
80
|
+
timestamp: Optional ISO timestamp
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Tuple of (content_dict, error_message). error_message is None if successful.
|
|
84
|
+
"""
|
|
85
|
+
content_dict: dict[str, Any] = {"content": content, "context": context}
|
|
86
|
+
|
|
87
|
+
if timestamp:
|
|
88
|
+
try:
|
|
89
|
+
parsed_timestamp = parse_timestamp(timestamp)
|
|
90
|
+
content_dict["event_date"] = parsed_timestamp
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
return {}, str(e)
|
|
93
|
+
|
|
94
|
+
return content_dict, None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def register_mcp_tools(
|
|
98
|
+
mcp: FastMCP,
|
|
99
|
+
memory: MemoryEngine,
|
|
100
|
+
config: MCPToolsConfig,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Register MCP tools on a FastMCP server.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
mcp: FastMCP server instance
|
|
106
|
+
memory: MemoryEngine instance
|
|
107
|
+
config: Tool configuration
|
|
108
|
+
"""
|
|
109
|
+
tools_to_register = config.tools or {"retain", "recall", "reflect", "list_banks", "create_bank"}
|
|
110
|
+
|
|
111
|
+
if "retain" in tools_to_register:
|
|
112
|
+
_register_retain(mcp, memory, config)
|
|
113
|
+
|
|
114
|
+
if "recall" in tools_to_register:
|
|
115
|
+
_register_recall(mcp, memory, config)
|
|
116
|
+
|
|
117
|
+
if "reflect" in tools_to_register:
|
|
118
|
+
_register_reflect(mcp, memory, config)
|
|
119
|
+
|
|
120
|
+
if "list_banks" in tools_to_register:
|
|
121
|
+
_register_list_banks(mcp, memory, config)
|
|
122
|
+
|
|
123
|
+
if "create_bank" in tools_to_register:
|
|
124
|
+
_register_create_bank(mcp, memory, config)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _register_retain(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
|
128
|
+
"""Register the retain tool."""
|
|
129
|
+
description = config.retain_description or DEFAULT_MCP_RETAIN_DESCRIPTION
|
|
130
|
+
|
|
131
|
+
if config.include_bank_id_param:
|
|
132
|
+
if config.retain_fire_and_forget:
|
|
133
|
+
|
|
134
|
+
@mcp.tool(description=description)
|
|
135
|
+
async def retain(
|
|
136
|
+
content: str,
|
|
137
|
+
context: str = "general",
|
|
138
|
+
timestamp: str | None = None,
|
|
139
|
+
bank_id: str | None = None,
|
|
140
|
+
) -> dict:
|
|
141
|
+
"""
|
|
142
|
+
Args:
|
|
143
|
+
content: The fact/memory to store (be specific and include relevant details)
|
|
144
|
+
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
|
|
145
|
+
timestamp: When this event/fact occurred (ISO format, e.g., '2024-01-15T10:30:00Z'). Useful for timeline tracking.
|
|
146
|
+
bank_id: Optional bank to store in (defaults to session bank). Use for cross-bank operations.
|
|
147
|
+
"""
|
|
148
|
+
import asyncio
|
|
149
|
+
|
|
150
|
+
target_bank = bank_id or config.bank_id_resolver()
|
|
151
|
+
if target_bank is None:
|
|
152
|
+
return {"status": "error", "message": "No bank_id configured"}
|
|
153
|
+
|
|
154
|
+
content_dict, error = build_content_dict(content, context, timestamp)
|
|
155
|
+
if error:
|
|
156
|
+
return {"status": "error", "message": error}
|
|
157
|
+
|
|
158
|
+
async def _retain():
|
|
159
|
+
try:
|
|
160
|
+
await memory.retain_batch_async(
|
|
161
|
+
bank_id=target_bank,
|
|
162
|
+
contents=[content_dict],
|
|
163
|
+
request_context=RequestContext(),
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Error storing memory: {e}", exc_info=True)
|
|
167
|
+
|
|
168
|
+
asyncio.create_task(_retain())
|
|
169
|
+
return {"status": "accepted", "message": "Memory storage initiated"}
|
|
170
|
+
|
|
171
|
+
else:
|
|
172
|
+
|
|
173
|
+
@mcp.tool(description=description)
|
|
174
|
+
async def retain(
|
|
175
|
+
content: str,
|
|
176
|
+
context: str = "general",
|
|
177
|
+
timestamp: str | None = None,
|
|
178
|
+
async_processing: bool = True,
|
|
179
|
+
bank_id: str | None = None,
|
|
180
|
+
) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Args:
|
|
183
|
+
content: The fact/memory to store (be specific and include relevant details)
|
|
184
|
+
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
|
|
185
|
+
timestamp: When this event/fact occurred (ISO format, e.g., '2024-01-15T10:30:00Z'). Useful for timeline tracking.
|
|
186
|
+
async_processing: If True, queue for background processing and return immediately. If False, wait for completion. Default: True
|
|
187
|
+
bank_id: Optional bank to store in (defaults to session bank). Use for cross-bank operations.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
target_bank = bank_id or config.bank_id_resolver()
|
|
191
|
+
if target_bank is None:
|
|
192
|
+
return "Error: No bank_id configured"
|
|
193
|
+
|
|
194
|
+
content_dict, error = build_content_dict(content, context, timestamp)
|
|
195
|
+
if error:
|
|
196
|
+
return f"Error: {error}"
|
|
197
|
+
|
|
198
|
+
contents = [content_dict]
|
|
199
|
+
if async_processing:
|
|
200
|
+
result = await memory.submit_async_retain(
|
|
201
|
+
bank_id=target_bank, contents=contents, request_context=RequestContext()
|
|
202
|
+
)
|
|
203
|
+
return f"Memory queued for background processing (operation_id: {result.get('operation_id', 'N/A')})"
|
|
204
|
+
else:
|
|
205
|
+
await memory.retain_batch_async(
|
|
206
|
+
bank_id=target_bank,
|
|
207
|
+
contents=contents,
|
|
208
|
+
request_context=RequestContext(),
|
|
209
|
+
)
|
|
210
|
+
return f"Memory stored successfully in bank '{target_bank}'"
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Error storing memory: {e}", exc_info=True)
|
|
213
|
+
return f"Error: {str(e)}"
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
# No bank_id param - use fixed bank from resolver
|
|
217
|
+
|
|
218
|
+
@mcp.tool(description=description)
|
|
219
|
+
async def retain(
|
|
220
|
+
content: str,
|
|
221
|
+
context: str = "general",
|
|
222
|
+
timestamp: str | None = None,
|
|
223
|
+
) -> dict:
|
|
224
|
+
"""
|
|
225
|
+
Args:
|
|
226
|
+
content: The fact/memory to store (be specific and include relevant details)
|
|
227
|
+
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
|
|
228
|
+
timestamp: When this event/fact occurred (ISO format, e.g., '2024-01-15T10:30:00Z'). Useful for timeline tracking.
|
|
229
|
+
"""
|
|
230
|
+
import asyncio
|
|
231
|
+
|
|
232
|
+
target_bank = config.bank_id_resolver()
|
|
233
|
+
if target_bank is None:
|
|
234
|
+
return {"status": "error", "message": "No bank_id configured"}
|
|
235
|
+
|
|
236
|
+
content_dict, error = build_content_dict(content, context, timestamp)
|
|
237
|
+
if error:
|
|
238
|
+
return {"status": "error", "message": error}
|
|
239
|
+
|
|
240
|
+
async def _retain():
|
|
241
|
+
try:
|
|
242
|
+
await memory.retain_batch_async(
|
|
243
|
+
bank_id=target_bank,
|
|
244
|
+
contents=[content_dict],
|
|
245
|
+
request_context=RequestContext(),
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"Error storing memory: {e}", exc_info=True)
|
|
249
|
+
|
|
250
|
+
asyncio.create_task(_retain())
|
|
251
|
+
return {"status": "accepted", "message": "Memory storage initiated"}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _register_recall(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
|
255
|
+
"""Register the recall tool."""
|
|
256
|
+
description = config.recall_description or DEFAULT_MCP_RECALL_DESCRIPTION
|
|
257
|
+
|
|
258
|
+
if config.include_bank_id_param:
|
|
259
|
+
|
|
260
|
+
@mcp.tool(description=description)
|
|
261
|
+
async def recall(
|
|
262
|
+
query: str,
|
|
263
|
+
max_tokens: int = 4096,
|
|
264
|
+
bank_id: str | None = None,
|
|
265
|
+
) -> str | dict:
|
|
266
|
+
"""
|
|
267
|
+
Args:
|
|
268
|
+
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
|
|
269
|
+
max_tokens: Maximum tokens to return in results (default: 4096)
|
|
270
|
+
bank_id: Optional bank to search in (defaults to session bank). Use for cross-bank operations.
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
target_bank = bank_id or config.bank_id_resolver()
|
|
274
|
+
if target_bank is None:
|
|
275
|
+
return "Error: No bank_id configured"
|
|
276
|
+
|
|
277
|
+
recall_result = await memory.recall_async(
|
|
278
|
+
bank_id=target_bank,
|
|
279
|
+
query=query,
|
|
280
|
+
fact_type=list(VALID_RECALL_FACT_TYPES),
|
|
281
|
+
budget=Budget.HIGH,
|
|
282
|
+
max_tokens=max_tokens,
|
|
283
|
+
request_context=RequestContext(),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return recall_result.model_dump_json(indent=2)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.error(f"Error searching: {e}", exc_info=True)
|
|
289
|
+
return f'{{"error": "{e}", "results": []}}'
|
|
290
|
+
|
|
291
|
+
else:
|
|
292
|
+
|
|
293
|
+
@mcp.tool(description=description)
|
|
294
|
+
async def recall(
|
|
295
|
+
query: str,
|
|
296
|
+
max_tokens: int = 4096,
|
|
297
|
+
) -> dict:
|
|
298
|
+
"""
|
|
299
|
+
Args:
|
|
300
|
+
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
|
|
301
|
+
max_tokens: Maximum tokens to return in results (default: 4096)
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
target_bank = config.bank_id_resolver()
|
|
305
|
+
if target_bank is None:
|
|
306
|
+
return {"error": "No bank_id configured", "results": []}
|
|
307
|
+
|
|
308
|
+
recall_result = await memory.recall_async(
|
|
309
|
+
bank_id=target_bank,
|
|
310
|
+
query=query,
|
|
311
|
+
fact_type=list(VALID_RECALL_FACT_TYPES),
|
|
312
|
+
budget=Budget.HIGH,
|
|
313
|
+
max_tokens=max_tokens,
|
|
314
|
+
request_context=RequestContext(),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return recall_result.model_dump()
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Error searching: {e}", exc_info=True)
|
|
320
|
+
return {"error": str(e), "results": []}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _register_reflect(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
|
324
|
+
"""Register the reflect tool."""
|
|
325
|
+
|
|
326
|
+
if config.include_bank_id_param:
|
|
327
|
+
|
|
328
|
+
@mcp.tool()
|
|
329
|
+
async def reflect(
|
|
330
|
+
query: str,
|
|
331
|
+
context: str | None = None,
|
|
332
|
+
budget: str = "low",
|
|
333
|
+
bank_id: str | None = None,
|
|
334
|
+
) -> str:
|
|
335
|
+
"""
|
|
336
|
+
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
|
|
337
|
+
|
|
338
|
+
WHEN TO USE THIS TOOL:
|
|
339
|
+
Use reflect when you need reasoned analysis, not just fact retrieval. This tool
|
|
340
|
+
thinks through the question using everything the bank knows and its personality traits.
|
|
341
|
+
|
|
342
|
+
EXAMPLES OF GOOD QUERIES:
|
|
343
|
+
- "What patterns have emerged in how I approach debugging?"
|
|
344
|
+
- "Based on my past decisions, what architectural style do I prefer?"
|
|
345
|
+
- "What might be the best approach for this problem given what you know about me?"
|
|
346
|
+
- "How should I prioritize these tasks based on my goals?"
|
|
347
|
+
|
|
348
|
+
HOW IT DIFFERS FROM RECALL:
|
|
349
|
+
- recall: Returns raw facts matching your search (fast lookup)
|
|
350
|
+
- reflect: Reasons across memories to form a synthesized answer (deeper analysis)
|
|
351
|
+
|
|
352
|
+
Use recall for "what did I say about X?" and reflect for "what should I do about X?"
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
query: The question or topic to reflect on
|
|
356
|
+
context: Optional context about why this reflection is needed
|
|
357
|
+
budget: Search budget - 'low', 'mid', or 'high' (default: 'low')
|
|
358
|
+
bank_id: Optional bank to reflect in (defaults to session bank). Use for cross-bank operations.
|
|
359
|
+
"""
|
|
360
|
+
try:
|
|
361
|
+
target_bank = bank_id or config.bank_id_resolver()
|
|
362
|
+
if target_bank is None:
|
|
363
|
+
return "Error: No bank_id configured"
|
|
364
|
+
|
|
365
|
+
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
|
|
366
|
+
budget_enum = budget_map.get(budget.lower(), Budget.LOW)
|
|
367
|
+
|
|
368
|
+
reflect_result = await memory.reflect_async(
|
|
369
|
+
bank_id=target_bank,
|
|
370
|
+
query=query,
|
|
371
|
+
budget=budget_enum,
|
|
372
|
+
context=context,
|
|
373
|
+
request_context=RequestContext(),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
return reflect_result.model_dump_json(indent=2)
|
|
377
|
+
except Exception as e:
|
|
378
|
+
logger.error(f"Error reflecting: {e}", exc_info=True)
|
|
379
|
+
return f'{{"error": "{e}", "text": ""}}'
|
|
380
|
+
|
|
381
|
+
else:
|
|
382
|
+
|
|
383
|
+
@mcp.tool()
|
|
384
|
+
async def reflect(
|
|
385
|
+
query: str,
|
|
386
|
+
context: str | None = None,
|
|
387
|
+
budget: str = "low",
|
|
388
|
+
) -> dict:
|
|
389
|
+
"""
|
|
390
|
+
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
|
|
391
|
+
|
|
392
|
+
WHEN TO USE THIS TOOL:
|
|
393
|
+
Use reflect when you need reasoned analysis, not just fact retrieval. This tool
|
|
394
|
+
thinks through the question using everything the bank knows and its personality traits.
|
|
395
|
+
|
|
396
|
+
EXAMPLES OF GOOD QUERIES:
|
|
397
|
+
- "What patterns have emerged in how I approach debugging?"
|
|
398
|
+
- "Based on my past decisions, what architectural style do I prefer?"
|
|
399
|
+
- "What might be the best approach for this problem given what you know about me?"
|
|
400
|
+
- "How should I prioritize these tasks based on my goals?"
|
|
401
|
+
|
|
402
|
+
HOW IT DIFFERS FROM RECALL:
|
|
403
|
+
- recall: Returns raw facts matching your search (fast lookup)
|
|
404
|
+
- reflect: Reasons across memories to form a synthesized answer (deeper analysis)
|
|
405
|
+
|
|
406
|
+
Use recall for "what did I say about X?" and reflect for "what should I do about X?"
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
query: The question or topic to reflect on
|
|
410
|
+
context: Optional context about why this reflection is needed
|
|
411
|
+
budget: Search budget - 'low', 'mid', or 'high' (default: 'low')
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
target_bank = config.bank_id_resolver()
|
|
415
|
+
if target_bank is None:
|
|
416
|
+
return {"error": "No bank_id configured", "text": ""}
|
|
417
|
+
|
|
418
|
+
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
|
|
419
|
+
budget_enum = budget_map.get(budget.lower(), Budget.LOW)
|
|
420
|
+
|
|
421
|
+
reflect_result = await memory.reflect_async(
|
|
422
|
+
bank_id=target_bank,
|
|
423
|
+
query=query,
|
|
424
|
+
budget=budget_enum,
|
|
425
|
+
context=context,
|
|
426
|
+
request_context=RequestContext(),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return reflect_result.model_dump()
|
|
430
|
+
except Exception as e:
|
|
431
|
+
logger.error(f"Error reflecting: {e}", exc_info=True)
|
|
432
|
+
return {"error": str(e), "text": ""}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _register_list_banks(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
|
436
|
+
"""Register the list_banks tool."""
|
|
437
|
+
|
|
438
|
+
@mcp.tool()
|
|
439
|
+
async def list_banks() -> str:
|
|
440
|
+
"""
|
|
441
|
+
List all available memory banks.
|
|
442
|
+
|
|
443
|
+
Use this tool to discover what memory banks exist in the system.
|
|
444
|
+
Each bank is an isolated memory store (like a separate "brain").
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
JSON list of banks with their IDs, names, dispositions, and missions.
|
|
448
|
+
"""
|
|
449
|
+
try:
|
|
450
|
+
banks = await memory.list_banks(request_context=RequestContext())
|
|
451
|
+
return json.dumps({"banks": banks}, indent=2)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error(f"Error listing banks: {e}", exc_info=True)
|
|
454
|
+
return f'{{"error": "{e}", "banks": []}}'
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _register_create_bank(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
|
458
|
+
"""Register the create_bank tool."""
|
|
459
|
+
|
|
460
|
+
@mcp.tool()
|
|
461
|
+
async def create_bank(bank_id: str, name: str | None = None, mission: str | None = None) -> str:
|
|
462
|
+
"""
|
|
463
|
+
Create a new memory bank or get an existing one.
|
|
464
|
+
|
|
465
|
+
Memory banks are isolated stores - each one is like a separate "brain" for a user/agent.
|
|
466
|
+
Banks are auto-created with default settings if they don't exist.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
bank_id: Unique identifier for the bank (e.g., 'user-123', 'agent-alpha')
|
|
470
|
+
name: Optional human-friendly name for the bank
|
|
471
|
+
mission: Optional mission describing who the agent is and what they're trying to accomplish
|
|
472
|
+
"""
|
|
473
|
+
try:
|
|
474
|
+
# get_bank_profile auto-creates bank if it doesn't exist
|
|
475
|
+
profile = await memory.get_bank_profile(bank_id, request_context=RequestContext())
|
|
476
|
+
|
|
477
|
+
# Update name/mission if provided
|
|
478
|
+
if name is not None or mission is not None:
|
|
479
|
+
await memory.update_bank(
|
|
480
|
+
bank_id,
|
|
481
|
+
name=name,
|
|
482
|
+
mission=mission,
|
|
483
|
+
request_context=RequestContext(),
|
|
484
|
+
)
|
|
485
|
+
# Fetch updated profile
|
|
486
|
+
profile = await memory.get_bank_profile(bank_id, request_context=RequestContext())
|
|
487
|
+
|
|
488
|
+
# Serialize disposition if it's a Pydantic model
|
|
489
|
+
if "disposition" in profile and hasattr(profile["disposition"], "model_dump"):
|
|
490
|
+
profile["disposition"] = profile["disposition"].model_dump()
|
|
491
|
+
return json.dumps(profile, indent=2)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
logger.error(f"Error creating bank: {e}", exc_info=True)
|
|
494
|
+
return f'{{"error": "{e}"}}'
|
hindsight_api/models.py
CHANGED
|
@@ -95,7 +95,6 @@ class MemoryUnit(Base):
|
|
|
95
95
|
mentioned_at: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True)) # When fact was mentioned
|
|
96
96
|
fact_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="world")
|
|
97
97
|
confidence_score: Mapped[float | None] = mapped_column(Float)
|
|
98
|
-
access_count: Mapped[int] = mapped_column(Integer, server_default="0")
|
|
99
98
|
unit_metadata: Mapped[dict] = mapped_column(
|
|
100
99
|
"metadata", JSONB, server_default=sql_text("'{}'::jsonb")
|
|
101
100
|
) # User-defined metadata (str->str)
|
|
@@ -131,7 +130,6 @@ class MemoryUnit(Base):
|
|
|
131
130
|
Index("idx_memory_units_document_id", "document_id"),
|
|
132
131
|
Index("idx_memory_units_event_date", "event_date", postgresql_ops={"event_date": "DESC"}),
|
|
133
132
|
Index("idx_memory_units_bank_date", "bank_id", "event_date", postgresql_ops={"event_date": "DESC"}),
|
|
134
|
-
Index("idx_memory_units_access_count", "access_count", postgresql_ops={"access_count": "DESC"}),
|
|
135
133
|
Index("idx_memory_units_fact_type", "fact_type"),
|
|
136
134
|
Index("idx_memory_units_bank_fact_type", "bank_id", "fact_type"),
|
|
137
135
|
Index(
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker package for distributed task processing.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- WorkerPoller: Polls PostgreSQL for pending tasks and executes them
|
|
6
|
+
- main: CLI entry point for hindsight-worker
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .poller import WorkerPoller
|
|
10
|
+
|
|
11
|
+
__all__ = ["WorkerPoller"]
|