hindsight-api 0.0.16__py3-none-any.whl → 0.0.17__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.
@@ -62,14 +62,13 @@ def create_app(
62
62
  # Mount MCP server if enabled
63
63
  if mcp_api_enabled:
64
64
  try:
65
- from .mcp import create_mcp_server
65
+ from .mcp import create_mcp_app
66
66
 
67
- # Create MCP server with shared memory instance
68
- mcp_server = create_mcp_server(memory=memory)
69
-
70
- # Mount at specified path using http_app (modern non-SSE alternative)
71
- app.mount(mcp_mount_path, mcp_server.http_app())
72
- logger.info(f"MCP server enabled at {mcp_mount_path}")
67
+ # Create MCP app with dynamic bank_id support
68
+ # Supports: /mcp/{bank_id}/sse (bank-specific SSE endpoint)
69
+ mcp_app = create_mcp_app(memory=memory)
70
+ app.mount(mcp_mount_path, mcp_app)
71
+ logger.info(f"MCP server enabled at {mcp_mount_path}/{{bank_id}}/sse")
73
72
  except ImportError as e:
74
73
  logger.error(f"MCP server requested but dependencies not available: {e}")
75
74
  logger.error("Install with: pip install hindsight-api[mcp]")
hindsight_api/api/mcp.py CHANGED
@@ -3,6 +3,8 @@
3
3
  import json
4
4
  import logging
5
5
  import os
6
+ from contextvars import ContextVar
7
+ from typing import Optional
6
8
 
7
9
  from fastmcp import FastMCP
8
10
  from hindsight_api import MemoryEngine
@@ -17,6 +19,14 @@ logging.basicConfig(
17
19
  )
18
20
  logger = logging.getLogger(__name__)
19
21
 
22
+ # Context variable to hold the current bank_id from the URL path
23
+ _current_bank_id: ContextVar[Optional[str]] = ContextVar("current_bank_id", default=None)
24
+
25
+
26
+ def get_current_bank_id() -> Optional[str]:
27
+ """Get the current bank_id from context (set from URL path)."""
28
+ return _current_bank_id.get()
29
+
20
30
 
21
31
  def create_mcp_server(memory: MemoryEngine) -> FastMCP:
22
32
  """
@@ -28,125 +38,71 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
28
38
  Returns:
29
39
  Configured FastMCP server instance
30
40
  """
31
- # Create FastMCP server
32
41
  mcp = FastMCP("hindsight-mcp-server")
33
42
 
34
43
  @mcp.tool()
35
- async def hindsight_put(bank_id: str, content: str, context: str, explanation: str = "") -> str:
44
+ async def retain(content: str, context: str = "general") -> str:
36
45
  """
37
- **CRITICAL: Store important user information to long-term memory.**
38
-
39
- **⚠️ PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
40
- - This tool is STRICTLY per-user. Each user MUST have a unique `bank_id`.
41
- - ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `bank_id`.
42
- - DO NOT use this tool if you cannot identify the specific user.
43
- - DO NOT share memories between different users - each user's memories are isolated by their `bank_id`.
44
- - If you don't have a user identifier, DO NOT use this tool at all.
46
+ Store important information to long-term memory.
45
47
 
46
48
  Use this tool PROACTIVELY whenever the user shares:
47
- - Personal facts, preferences, or interests (e.g., "I love hiking", "I'm a vegetarian")
48
- - Important events or milestones (e.g., "I got promoted", "My birthday is June 15")
49
- - User history, experiences, or background (e.g., "I used to work at Google", "I studied CS at MIT")
50
- - Decisions, opinions, or stated preferences (e.g., "I prefer Python over JavaScript")
51
- - Goals, plans, or future intentions (e.g., "I'm planning to visit Japan next year")
52
- - Relationships or people mentioned (e.g., "My manager Sarah", "My wife Alice")
49
+ - Personal facts, preferences, or interests
50
+ - Important events or milestones
51
+ - User history, experiences, or background
52
+ - Decisions, opinions, or stated preferences
53
+ - Goals, plans, or future intentions
54
+ - Relationships or people mentioned
53
55
  - Work context, projects, or responsibilities
54
- - Any other information the user would want remembered for future conversations
55
-
56
- **When to use**: Immediately after user shares personal information. Don't ask permission - just store it naturally.
57
-
58
- **Context guidelines**: Use descriptive contexts like "personal_preferences", "work_history", "family", "hobbies",
59
- "career_goals", "project_details", etc. This helps organize and retrieve related memories later.
60
56
 
61
57
  Args:
62
- bank_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
63
- This MUST be consistent across all interactions with the same user.
64
- Example: "user_12345", "alice@example.com", "session_abc123"
65
58
  content: The fact/memory to store (be specific and include relevant details)
66
- context: Categorize the memory (e.g., 'personal_preferences', 'work_history', 'hobbies', 'family')
67
- explanation: Optional explanation for why this memory is being stored
59
+ context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
68
60
  """
69
61
  try:
70
- # Log explanation if provided
71
- if explanation:
72
- pass # Explanation provided
73
-
74
- # Store memory using put_batch_async
62
+ bank_id = get_current_bank_id()
75
63
  await memory.put_batch_async(
76
64
  bank_id=bank_id,
77
65
  contents=[{"content": content, "context": context}]
78
66
  )
79
- return f"Fact stored successfully"
67
+ return "Memory stored successfully"
80
68
  except Exception as e:
81
- logger.error(f"Error storing fact: {e}", exc_info=True)
69
+ logger.error(f"Error storing memory: {e}", exc_info=True)
82
70
  return f"Error: {str(e)}"
83
71
 
84
72
  @mcp.tool()
85
- async def hindsight_search(bank_id: str, query: str, max_tokens: int = 4096, explanation: str = "") -> str:
73
+ async def recall(query: str, max_results: int = 10) -> str:
86
74
  """
87
- **CRITICAL: Search user's memory to provide personalized, context-aware responses.**
88
-
89
- **⚠️ PER-USER TOOL - REQUIRES USER IDENTIFICATION:**
90
- - This tool is STRICTLY per-user. Each user MUST have a unique `bank_id`.
91
- - ONLY use this tool if you have a valid user identifier (user ID, email, session ID, etc.) to map to `bank_id`.
92
- - DO NOT use this tool if you cannot identify the specific user.
93
- - DO NOT search across multiple users - each user's memories are isolated by their `bank_id`.
94
- - If you don't have a user identifier, DO NOT use this tool at all.
95
-
96
- Use this tool PROACTIVELY at the start of conversations or when making recommendations to:
97
- - Check user's preferences before making suggestions (e.g., "what foods does the user like?")
98
- - Recall user's history to provide continuity (e.g., "what projects has the user worked on?")
99
- - Remember user's goals and context (e.g., "what is the user trying to accomplish?")
100
- - Avoid repeating information or asking questions you should already know
101
- - Personalize responses based on user's background, interests, and past interactions
102
- - Reference past conversations or events the user mentioned
103
-
104
- **When to use**:
105
- - Start of conversation: Search for relevant context about the user
106
- - Before recommendations: Check user preferences and past experiences
107
- - When user asks about something they may have mentioned before
108
- - To provide continuity across conversations
109
-
110
- **Search tips**: Use natural language queries like "user's programming language preferences",
111
- "user's work experience", "user's dietary restrictions", "what does the user know about X?"
75
+ Search memories to provide personalized, context-aware responses.
76
+
77
+ Use this tool PROACTIVELY to:
78
+ - Check user's preferences before making suggestions
79
+ - Recall user's history to provide continuity
80
+ - Remember user's goals and context
81
+ - Personalize responses based on past interactions
112
82
 
113
83
  Args:
114
- bank_id: **REQUIRED** - The unique, persistent identifier for this specific user (e.g., user_id, email, session_id).
115
- This MUST be consistent across all interactions with the same user.
116
- Example: "user_12345", "alice@example.com", "session_abc123"
117
- query: Natural language search query to find relevant memories
118
- max_tokens: Maximum tokens for search context (default: 4096)
119
- explanation: Optional explanation for why this search is being performed
84
+ query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
85
+ max_results: Maximum number of results to return (default: 10)
120
86
  """
121
87
  try:
122
- # Log all parameters for debugging
123
- logger.info(f"hindsight_search called with: query={query!r}, max_tokens={max_tokens}, explanation={explanation!r}")
124
-
125
- # Log explanation if provided
126
- if explanation:
127
- pass # Explanation provided
128
-
129
- # Search using recall_async
88
+ bank_id = get_current_bank_id()
130
89
  from hindsight_api.engine.memory_engine import Budget
131
90
  search_result = await memory.recall_async(
132
91
  bank_id=bank_id,
133
92
  query=query,
134
- fact_type=["world", "bank", "opinion"], # Search all fact types
135
- max_tokens=max_tokens,
93
+ fact_type=["world", "bank", "opinion"],
136
94
  budget=Budget.LOW
137
95
  )
138
96
 
139
- # Convert results to dict format
140
97
  results = [
141
98
  {
142
99
  "id": fact.id,
143
100
  "text": fact.text,
144
101
  "type": fact.fact_type,
145
102
  "context": fact.context,
146
- "event_date": fact.event_date, # Already a string from the database
147
- "document_id": fact.document_id
103
+ "event_date": fact.event_date,
148
104
  }
149
- for fact in search_result.results
105
+ for fact in search_result.results[:max_results]
150
106
  ]
151
107
 
152
108
  return json.dumps({"results": results}, indent=2)
@@ -155,3 +111,104 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
155
111
  return json.dumps({"error": str(e), "results": []})
156
112
 
157
113
  return mcp
114
+
115
+
116
+ class MCPMiddleware:
117
+ """ASGI middleware that extracts bank_id from path and sets context."""
118
+
119
+ def __init__(self, app, memory: MemoryEngine):
120
+ self.app = app
121
+ self.memory = memory
122
+ self.mcp_server = create_mcp_server(memory)
123
+ # Use sse_app - http_app requires lifespan management that's complex with middleware
124
+ import warnings
125
+ with warnings.catch_warnings():
126
+ warnings.simplefilter("ignore", DeprecationWarning)
127
+ self.mcp_app = self.mcp_server.sse_app()
128
+
129
+ async def __call__(self, scope, receive, send):
130
+ if scope["type"] != "http":
131
+ await self.mcp_app(scope, receive, send)
132
+ return
133
+
134
+ path = scope.get("path", "")
135
+
136
+ # Strip any mount prefix (e.g., /mcp) that FastAPI might not have stripped
137
+ root_path = scope.get("root_path", "")
138
+ if root_path and path.startswith(root_path):
139
+ path = path[len(root_path):] or "/"
140
+
141
+ # Also handle case where mount path wasn't stripped (e.g., /mcp/...)
142
+ if path.startswith("/mcp/"):
143
+ path = path[4:] # Remove /mcp prefix
144
+
145
+ # Extract bank_id from path: /{bank_id}/ or /{bank_id}
146
+ # http_app expects requests at /
147
+ if not path.startswith("/") or len(path) <= 1:
148
+ # No bank_id in path - return error
149
+ await self._send_error(send, 400, "bank_id required in path: /mcp/{bank_id}/")
150
+ return
151
+
152
+ # Extract bank_id from first path segment
153
+ parts = path[1:].split("/", 1)
154
+ if not parts[0]:
155
+ await self._send_error(send, 400, "bank_id required in path: /mcp/{bank_id}/")
156
+ return
157
+
158
+ bank_id = parts[0]
159
+ new_path = "/" + parts[1] if len(parts) > 1 else "/"
160
+
161
+ # Set bank_id context
162
+ token = _current_bank_id.set(bank_id)
163
+ try:
164
+ new_scope = scope.copy()
165
+ new_scope["path"] = new_path
166
+
167
+ # Wrap send to rewrite the SSE endpoint URL to include bank_id
168
+ # The SSE app sends "event: endpoint\ndata: /messages\n" but we need
169
+ # the client to POST to /{bank_id}/messages instead
170
+ async def send_wrapper(message):
171
+ if message["type"] == "http.response.body":
172
+ body = message.get("body", b"")
173
+ if body and b"/messages" in body:
174
+ # Rewrite /messages to /{bank_id}/messages in SSE endpoint event
175
+ body = body.replace(
176
+ b"data: /messages",
177
+ f"data: /{bank_id}/messages".encode()
178
+ )
179
+ message = {**message, "body": body}
180
+ await send(message)
181
+
182
+ await self.mcp_app(new_scope, receive, send_wrapper)
183
+ finally:
184
+ _current_bank_id.reset(token)
185
+
186
+ async def _send_error(self, send, status: int, message: str):
187
+ """Send an error response."""
188
+ body = json.dumps({"error": message}).encode()
189
+ await send({
190
+ "type": "http.response.start",
191
+ "status": status,
192
+ "headers": [(b"content-type", b"application/json")],
193
+ })
194
+ await send({
195
+ "type": "http.response.body",
196
+ "body": body,
197
+ })
198
+
199
+
200
+ def create_mcp_app(memory: MemoryEngine):
201
+ """
202
+ Create an ASGI app that handles MCP requests.
203
+
204
+ URL pattern: /mcp/{bank_id}/
205
+
206
+ The bank_id is extracted from the URL path and made available to tools.
207
+
208
+ Args:
209
+ memory: MemoryEngine instance
210
+
211
+ Returns:
212
+ ASGI application
213
+ """
214
+ return MCPMiddleware(None, memory)
@@ -10,13 +10,9 @@ import warnings
10
10
  warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
11
11
  warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
12
12
 
13
- import asyncio
14
- import atexit
15
13
  import logging
16
14
  import os
17
15
  import argparse
18
- import signal
19
- import sys
20
16
 
21
17
  from hindsight_api import MemoryEngine
22
18
  from hindsight_api.api import create_app
@@ -25,36 +21,6 @@ from hindsight_api.api import create_app
25
21
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
26
22
 
27
23
 
28
- def _cleanup_pg0():
29
- """Synchronous cleanup function to stop pg0 on exit."""
30
- global _memory
31
- if _memory is not None and _memory._pg0 is not None:
32
- try:
33
- # Run async stop in a new event loop
34
- loop = asyncio.new_event_loop()
35
- loop.run_until_complete(_memory._pg0.stop())
36
- loop.close()
37
- print("\npg0 stopped.")
38
- except Exception as e:
39
- print(f"\nError stopping pg0: {e}")
40
-
41
-
42
- # Register cleanup on normal exit
43
- atexit.register(_cleanup_pg0)
44
-
45
-
46
- def _signal_handler(signum, frame):
47
- """Handle SIGINT/SIGTERM to ensure pg0 cleanup."""
48
- print(f"\nReceived signal {signum}, shutting down...")
49
- _cleanup_pg0()
50
- sys.exit(0)
51
-
52
-
53
- # Register signal handlers for graceful shutdown
54
- signal.signal(signal.SIGINT, _signal_handler)
55
- signal.signal(signal.SIGTERM, _signal_handler)
56
-
57
-
58
24
  # Create app at module level (required for uvicorn import string)
59
25
  _memory = MemoryEngine(
60
26
  db_url=os.getenv("HINDSIGHT_API_DATABASE_URL", "pg0"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hindsight-api
3
- Version: 0.0.16
3
+ Version: 0.0.17
4
4
  Summary: Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: alembic>=1.17.1
@@ -4,9 +4,9 @@ hindsight_api/metrics.py,sha256=j4-eeqVjjcGQxAxS_GgEaBNm10KdUxrGS_I2d1IM1hY,7255
4
4
  hindsight_api/migrations.py,sha256=VY-ILJLWEY1IaeJgQ2jlAVUtPLzq_41Dytg_DjuF0GA,6402
5
5
  hindsight_api/models.py,sha256=1vMn9jmDQvohfmxZXr1SYnhz5vhz52nrTd93A_lkVNE,12606
6
6
  hindsight_api/pg0.py,sha256=scFcYngOwbZ2oOQb7TysnUHgNgPyiN30pjPcIqMDmao,14158
7
- hindsight_api/api/__init__.py,sha256=cDqM1tXOk1aq5Hy__vJ97O4XOQUB4Qt-ea_szTnbQ3o,3037
7
+ hindsight_api/api/__init__.py,sha256=lXxJythXFV1DXIQ--4QfIo5pHYmDYJnd41dfAssNTTA,3017
8
8
  hindsight_api/api/http.py,sha256=anjh8axWcWF1dyqW3CnE9TUObLKxryjeQxT_keQEMak,71551
9
- hindsight_api/api/mcp.py,sha256=NbRSbEGih7zCFMmwddLgD_UYv-WjvwYWHLq2-vzz4SA,7862
9
+ hindsight_api/api/mcp.py,sha256=1fqeKBh3K0lJ5jodYysOTnDOWNjSzA08g8v2_k8HOlU,7734
10
10
  hindsight_api/engine/__init__.py,sha256=5DU5DvnJdzkrgNgKchpzkiJr-37I-kE1tegJg2LF04k,1214
11
11
  hindsight_api/engine/cross_encoder.py,sha256=kfwLiqlQUfvOgLyrkRReO1wWlO020lGbLXY8U0jKiPA,2875
12
12
  hindsight_api/engine/db_utils.py,sha256=p1Ne70wPP327xdPI_XjMfnagilY8sknbkhEIZuED6DU,2724
@@ -43,8 +43,8 @@ hindsight_api/engine/search/trace.py,sha256=GT86_LVKMyG2mw6EJzPjafvbqaot6XVy5fZ0
43
43
  hindsight_api/engine/search/tracer.py,sha256=mcM9qZpj3YFudrBCESwc6YKNAiWIMx1lScXWn5ru-ok,15017
44
44
  hindsight_api/engine/search/types.py,sha256=qIeHW_gT7f291vteTZXygAM8oAaPp2dq6uEdvOyOwzs,5488
45
45
  hindsight_api/web/__init__.py,sha256=WABqyqiAVFJJWOhKCytkj5Vcb61eAsRib3Ek7IMX6_U,378
46
- hindsight_api/web/server.py,sha256=txP1OBiwxZTyDbPLUYZ9XPMysskxEceHlxbDEvIq0ok,5376
47
- hindsight_api-0.0.16.dist-info/METADATA,sha256=0Tg86KkYkFYEBFBGjUhSE-RwllDeQ5n30vctTeK2DFk,1496
48
- hindsight_api-0.0.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
49
- hindsight_api-0.0.16.dist-info/entry_points.txt,sha256=53Fn-VxtkqreZhOPTJB_FupH7e5GyiMY3gzEp22d8xs,57
50
- hindsight_api-0.0.16.dist-info/RECORD,,
46
+ hindsight_api/web/server.py,sha256=l-Tw8G9IRdcSay-KWiUT4VlIJBzxbe-TV0rjX0fwLMc,4464
47
+ hindsight_api-0.0.17.dist-info/METADATA,sha256=zan_1uOEwAxlipqrDA0Rc65zs-e-AqxRoT38ihKtLQw,1496
48
+ hindsight_api-0.0.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
49
+ hindsight_api-0.0.17.dist-info/entry_points.txt,sha256=53Fn-VxtkqreZhOPTJB_FupH7e5GyiMY3gzEp22d8xs,57
50
+ hindsight_api-0.0.17.dist-info/RECORD,,