hindsight-api 0.1.11__py3-none-any.whl → 0.1.13__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 +2 -0
- hindsight_api/alembic/env.py +24 -1
- hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +14 -4
- hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +54 -13
- hindsight_api/alembic/versions/rename_personality_to_disposition.py +18 -7
- hindsight_api/api/http.py +253 -230
- hindsight_api/api/mcp.py +14 -3
- hindsight_api/config.py +11 -0
- hindsight_api/daemon.py +204 -0
- hindsight_api/engine/__init__.py +12 -1
- hindsight_api/engine/entity_resolver.py +38 -37
- hindsight_api/engine/interface.py +592 -0
- hindsight_api/engine/llm_wrapper.py +176 -6
- hindsight_api/engine/memory_engine.py +1092 -293
- hindsight_api/engine/retain/bank_utils.py +13 -12
- hindsight_api/engine/retain/chunk_storage.py +3 -2
- hindsight_api/engine/retain/fact_storage.py +10 -7
- hindsight_api/engine/retain/link_utils.py +17 -16
- hindsight_api/engine/retain/observation_regeneration.py +17 -16
- hindsight_api/engine/retain/orchestrator.py +2 -3
- hindsight_api/engine/retain/types.py +25 -8
- hindsight_api/engine/search/graph_retrieval.py +6 -5
- hindsight_api/engine/search/mpfp_retrieval.py +8 -7
- hindsight_api/engine/search/reranking.py +17 -0
- hindsight_api/engine/search/retrieval.py +12 -11
- hindsight_api/engine/search/think_utils.py +1 -1
- hindsight_api/engine/search/tracer.py +1 -1
- hindsight_api/engine/task_backend.py +32 -0
- hindsight_api/extensions/__init__.py +66 -0
- hindsight_api/extensions/base.py +81 -0
- hindsight_api/extensions/builtin/__init__.py +18 -0
- hindsight_api/extensions/builtin/tenant.py +33 -0
- hindsight_api/extensions/context.py +110 -0
- hindsight_api/extensions/http.py +89 -0
- hindsight_api/extensions/loader.py +125 -0
- hindsight_api/extensions/operation_validator.py +325 -0
- hindsight_api/extensions/tenant.py +63 -0
- hindsight_api/main.py +97 -17
- hindsight_api/mcp_local.py +7 -1
- hindsight_api/migrations.py +54 -10
- hindsight_api/models.py +15 -0
- hindsight_api/pg0.py +1 -1
- {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/METADATA +1 -1
- hindsight_api-0.1.13.dist-info/RECORD +75 -0
- hindsight_api-0.1.11.dist-info/RECORD +0 -64
- {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/WHEEL +0 -0
- {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Operation Validator Extension for validating retain/recall/reflect operations."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from hindsight_api.extensions.base import Extension
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from hindsight_api.engine.memory_engine import Budget
|
|
12
|
+
from hindsight_api.engine.response_models import RecallResult as RecallResultModel
|
|
13
|
+
from hindsight_api.engine.response_models import ReflectResult
|
|
14
|
+
from hindsight_api.models import RequestContext
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OperationValidationError(Exception):
|
|
18
|
+
"""Raised when an operation fails validation."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, reason: str):
|
|
21
|
+
self.reason = reason
|
|
22
|
+
super().__init__(f"Operation validation failed: {reason}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ValidationResult:
|
|
27
|
+
"""Result of an operation validation."""
|
|
28
|
+
|
|
29
|
+
allowed: bool
|
|
30
|
+
reason: str | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def accept(cls) -> "ValidationResult":
|
|
34
|
+
"""Create an accepted validation result."""
|
|
35
|
+
return cls(allowed=True)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def reject(cls, reason: str) -> "ValidationResult":
|
|
39
|
+
"""Create a rejected validation result with a reason."""
|
|
40
|
+
return cls(allowed=False, reason=reason)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Pre-operation Contexts (all user-provided parameters)
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class RetainContext:
|
|
50
|
+
"""Context for a retain operation validation (pre-operation).
|
|
51
|
+
|
|
52
|
+
Contains ALL user-provided parameters for the retain operation.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
bank_id: str
|
|
56
|
+
contents: list[dict] # List of {content, context, event_date, document_id}
|
|
57
|
+
request_context: "RequestContext"
|
|
58
|
+
document_id: str | None = None
|
|
59
|
+
fact_type_override: str | None = None
|
|
60
|
+
confidence_score: float | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class RecallContext:
|
|
65
|
+
"""Context for a recall operation validation (pre-operation).
|
|
66
|
+
|
|
67
|
+
Contains ALL user-provided parameters for the recall operation.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
bank_id: str
|
|
71
|
+
query: str
|
|
72
|
+
request_context: "RequestContext"
|
|
73
|
+
budget: "Budget | None" = None
|
|
74
|
+
max_tokens: int = 4096
|
|
75
|
+
enable_trace: bool = False
|
|
76
|
+
fact_types: list[str] = field(default_factory=list)
|
|
77
|
+
question_date: datetime | None = None
|
|
78
|
+
include_entities: bool = False
|
|
79
|
+
max_entity_tokens: int = 500
|
|
80
|
+
include_chunks: bool = False
|
|
81
|
+
max_chunk_tokens: int = 8192
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ReflectContext:
|
|
86
|
+
"""Context for a reflect operation validation (pre-operation).
|
|
87
|
+
|
|
88
|
+
Contains ALL user-provided parameters for the reflect operation.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
bank_id: str
|
|
92
|
+
query: str
|
|
93
|
+
request_context: "RequestContext"
|
|
94
|
+
budget: "Budget | None" = None
|
|
95
|
+
context: str | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# =============================================================================
|
|
99
|
+
# Post-operation Contexts (includes results)
|
|
100
|
+
# =============================================================================
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class RetainResult:
|
|
105
|
+
"""Result context for post-retain hook.
|
|
106
|
+
|
|
107
|
+
Contains the operation parameters and the result.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
bank_id: str
|
|
111
|
+
contents: list[dict]
|
|
112
|
+
request_context: "RequestContext"
|
|
113
|
+
document_id: str | None
|
|
114
|
+
fact_type_override: str | None
|
|
115
|
+
confidence_score: float | None
|
|
116
|
+
# Result
|
|
117
|
+
unit_ids: list[list[str]] # List of unit IDs per content item
|
|
118
|
+
success: bool = True
|
|
119
|
+
error: str | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class RecallResult:
|
|
124
|
+
"""Result context for post-recall hook.
|
|
125
|
+
|
|
126
|
+
Contains the operation parameters and the result.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
bank_id: str
|
|
130
|
+
query: str
|
|
131
|
+
request_context: "RequestContext"
|
|
132
|
+
budget: "Budget | None"
|
|
133
|
+
max_tokens: int
|
|
134
|
+
enable_trace: bool
|
|
135
|
+
fact_types: list[str]
|
|
136
|
+
question_date: datetime | None
|
|
137
|
+
include_entities: bool
|
|
138
|
+
max_entity_tokens: int
|
|
139
|
+
include_chunks: bool
|
|
140
|
+
max_chunk_tokens: int
|
|
141
|
+
# Result
|
|
142
|
+
result: "RecallResultModel | None" = None
|
|
143
|
+
success: bool = True
|
|
144
|
+
error: str | None = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class ReflectResultContext:
|
|
149
|
+
"""Result context for post-reflect hook.
|
|
150
|
+
|
|
151
|
+
Contains the operation parameters and the result.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
bank_id: str
|
|
155
|
+
query: str
|
|
156
|
+
request_context: "RequestContext"
|
|
157
|
+
budget: "Budget | None"
|
|
158
|
+
context: str | None
|
|
159
|
+
# Result
|
|
160
|
+
result: "ReflectResult | None" = None
|
|
161
|
+
success: bool = True
|
|
162
|
+
error: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class OperationValidatorExtension(Extension, ABC):
|
|
166
|
+
"""
|
|
167
|
+
Validates and hooks into retain/recall/reflect operations.
|
|
168
|
+
|
|
169
|
+
This extension allows implementing custom logic such as:
|
|
170
|
+
- Rate limiting (pre-operation)
|
|
171
|
+
- Quota enforcement (pre-operation)
|
|
172
|
+
- Permission checks (pre-operation)
|
|
173
|
+
- Content filtering (pre-operation)
|
|
174
|
+
- Usage tracking (post-operation)
|
|
175
|
+
- Audit logging (post-operation)
|
|
176
|
+
- Metrics collection (post-operation)
|
|
177
|
+
|
|
178
|
+
Enable via environment variable:
|
|
179
|
+
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
|
|
180
|
+
|
|
181
|
+
Configuration is passed from prefixed environment variables:
|
|
182
|
+
HINDSIGHT_API_OPERATION_VALIDATOR_MAX_REQUESTS=100
|
|
183
|
+
-> config = {"max_requests": "100"}
|
|
184
|
+
|
|
185
|
+
Hook execution order:
|
|
186
|
+
1. validate_retain/validate_recall/validate_reflect (pre-operation)
|
|
187
|
+
2. [operation executes]
|
|
188
|
+
3. on_retain_complete/on_recall_complete/on_reflect_complete (post-operation)
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
# =========================================================================
|
|
192
|
+
# Pre-operation validation hooks (abstract - must be implemented)
|
|
193
|
+
# =========================================================================
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
|
|
197
|
+
"""
|
|
198
|
+
Validate a retain operation before execution.
|
|
199
|
+
|
|
200
|
+
Called before the retain operation is processed. Return ValidationResult.reject()
|
|
201
|
+
to prevent the operation from executing.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
ctx: Context containing all user-provided parameters:
|
|
205
|
+
- bank_id: Bank identifier
|
|
206
|
+
- contents: List of content dicts
|
|
207
|
+
- request_context: Request context with auth info
|
|
208
|
+
- document_id: Optional document ID
|
|
209
|
+
- fact_type_override: Optional fact type override
|
|
210
|
+
- confidence_score: Optional confidence score
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
ValidationResult indicating whether the operation is allowed.
|
|
214
|
+
"""
|
|
215
|
+
...
|
|
216
|
+
|
|
217
|
+
@abstractmethod
|
|
218
|
+
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
|
|
219
|
+
"""
|
|
220
|
+
Validate a recall operation before execution.
|
|
221
|
+
|
|
222
|
+
Called before the recall operation is processed. Return ValidationResult.reject()
|
|
223
|
+
to prevent the operation from executing.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
ctx: Context containing all user-provided parameters:
|
|
227
|
+
- bank_id: Bank identifier
|
|
228
|
+
- query: Search query
|
|
229
|
+
- request_context: Request context with auth info
|
|
230
|
+
- budget: Budget level
|
|
231
|
+
- max_tokens: Maximum tokens to return
|
|
232
|
+
- enable_trace: Whether to include trace info
|
|
233
|
+
- fact_types: List of fact types to search
|
|
234
|
+
- question_date: Optional date context for query
|
|
235
|
+
- include_entities: Whether to include entity data
|
|
236
|
+
- max_entity_tokens: Max tokens for entities
|
|
237
|
+
- include_chunks: Whether to include chunks
|
|
238
|
+
- max_chunk_tokens: Max tokens for chunks
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
ValidationResult indicating whether the operation is allowed.
|
|
242
|
+
"""
|
|
243
|
+
...
|
|
244
|
+
|
|
245
|
+
@abstractmethod
|
|
246
|
+
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
|
|
247
|
+
"""
|
|
248
|
+
Validate a reflect operation before execution.
|
|
249
|
+
|
|
250
|
+
Called before the reflect operation is processed. Return ValidationResult.reject()
|
|
251
|
+
to prevent the operation from executing.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
ctx: Context containing all user-provided parameters:
|
|
255
|
+
- bank_id: Bank identifier
|
|
256
|
+
- query: Question to answer
|
|
257
|
+
- request_context: Request context with auth info
|
|
258
|
+
- budget: Budget level
|
|
259
|
+
- context: Optional additional context
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
ValidationResult indicating whether the operation is allowed.
|
|
263
|
+
"""
|
|
264
|
+
...
|
|
265
|
+
|
|
266
|
+
# =========================================================================
|
|
267
|
+
# Post-operation hooks (optional - override to implement)
|
|
268
|
+
# =========================================================================
|
|
269
|
+
|
|
270
|
+
async def on_retain_complete(self, result: RetainResult) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Called after a retain operation completes (success or failure).
|
|
273
|
+
|
|
274
|
+
Override this method to implement post-operation logic such as:
|
|
275
|
+
- Usage tracking
|
|
276
|
+
- Audit logging
|
|
277
|
+
- Metrics collection
|
|
278
|
+
- Notifications
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
result: Result context containing:
|
|
282
|
+
- All original operation parameters
|
|
283
|
+
- unit_ids: List of created unit IDs (if success)
|
|
284
|
+
- success: Whether the operation succeeded
|
|
285
|
+
- error: Error message (if failed)
|
|
286
|
+
"""
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
async def on_recall_complete(self, result: RecallResult) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Called after a recall operation completes (success or failure).
|
|
292
|
+
|
|
293
|
+
Override this method to implement post-operation logic such as:
|
|
294
|
+
- Usage tracking
|
|
295
|
+
- Audit logging
|
|
296
|
+
- Metrics collection
|
|
297
|
+
- Query analytics
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
result: Result context containing:
|
|
301
|
+
- All original operation parameters
|
|
302
|
+
- result: RecallResultModel (if success)
|
|
303
|
+
- success: Whether the operation succeeded
|
|
304
|
+
- error: Error message (if failed)
|
|
305
|
+
"""
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
async def on_reflect_complete(self, result: ReflectResultContext) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Called after a reflect operation completes (success or failure).
|
|
311
|
+
|
|
312
|
+
Override this method to implement post-operation logic such as:
|
|
313
|
+
- Usage tracking
|
|
314
|
+
- Audit logging
|
|
315
|
+
- Metrics collection
|
|
316
|
+
- Response analytics
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
result: Result context containing:
|
|
320
|
+
- All original operation parameters
|
|
321
|
+
- result: ReflectResult (if success)
|
|
322
|
+
- success: Whether the operation succeeded
|
|
323
|
+
- error: Error message (if failed)
|
|
324
|
+
"""
|
|
325
|
+
pass
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Tenant Extension for multi-tenancy and API key authentication."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from hindsight_api.extensions.base import Extension
|
|
7
|
+
from hindsight_api.models import RequestContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthenticationError(Exception):
|
|
11
|
+
"""Raised when authentication fails."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, reason: str):
|
|
14
|
+
self.reason = reason
|
|
15
|
+
super().__init__(f"Authentication failed: {reason}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TenantContext:
|
|
20
|
+
"""
|
|
21
|
+
Tenant context returned by authentication.
|
|
22
|
+
|
|
23
|
+
Contains the PostgreSQL schema name for tenant isolation.
|
|
24
|
+
All database queries will use fully-qualified table names
|
|
25
|
+
with this schema (e.g., schema_name.memory_units).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
schema_name: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TenantExtension(Extension, ABC):
|
|
32
|
+
"""
|
|
33
|
+
Extension for multi-tenancy and API key authentication.
|
|
34
|
+
|
|
35
|
+
This extension validates incoming requests and returns the tenant context
|
|
36
|
+
including the PostgreSQL schema to use for database operations.
|
|
37
|
+
|
|
38
|
+
Built-in implementation:
|
|
39
|
+
hindsight_api.extensions.builtin.tenant.ApiKeyTenantExtension
|
|
40
|
+
|
|
41
|
+
Enable via environment variable:
|
|
42
|
+
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
|
|
43
|
+
HINDSIGHT_API_TENANT_API_KEY=your-secret-key
|
|
44
|
+
|
|
45
|
+
The returned schema_name is used for fully-qualified table names in queries,
|
|
46
|
+
enabling tenant isolation at the database level.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def authenticate(self, context: RequestContext) -> TenantContext:
|
|
51
|
+
"""
|
|
52
|
+
Authenticate the action context and return tenant context.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
context: The action context containing API key and other auth data.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
TenantContext with the schema_name for database operations.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
AuthenticationError: If authentication fails.
|
|
62
|
+
"""
|
|
63
|
+
...
|
hindsight_api/main.py
CHANGED
|
@@ -4,6 +4,9 @@ Command-line interface for Hindsight API.
|
|
|
4
4
|
Run the server with:
|
|
5
5
|
hindsight-api
|
|
6
6
|
|
|
7
|
+
Run as background daemon:
|
|
8
|
+
hindsight-api --daemon
|
|
9
|
+
|
|
7
10
|
Stop with Ctrl+C.
|
|
8
11
|
"""
|
|
9
12
|
|
|
@@ -21,9 +24,13 @@ from . import MemoryEngine
|
|
|
21
24
|
from .api import create_app
|
|
22
25
|
from .banner import print_banner
|
|
23
26
|
from .config import HindsightConfig, get_config
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
from .daemon import (
|
|
28
|
+
DEFAULT_DAEMON_PORT,
|
|
29
|
+
DEFAULT_IDLE_TIMEOUT,
|
|
30
|
+
DaemonLock,
|
|
31
|
+
IdleTimeoutMiddleware,
|
|
32
|
+
daemonize,
|
|
33
|
+
)
|
|
27
34
|
|
|
28
35
|
# Filter deprecation warnings from third-party libraries
|
|
29
36
|
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
|
@@ -106,8 +113,52 @@ def main():
|
|
|
106
113
|
parser.add_argument("--ssl-keyfile", default=None, help="SSL key file")
|
|
107
114
|
parser.add_argument("--ssl-certfile", default=None, help="SSL certificate file")
|
|
108
115
|
|
|
116
|
+
# Daemon mode options
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--daemon",
|
|
119
|
+
action="store_true",
|
|
120
|
+
help=f"Run as background daemon (uses port {DEFAULT_DAEMON_PORT}, auto-exits after idle)",
|
|
121
|
+
)
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
"--idle-timeout",
|
|
124
|
+
type=int,
|
|
125
|
+
default=DEFAULT_IDLE_TIMEOUT,
|
|
126
|
+
help=f"Idle timeout in seconds before auto-exit in daemon mode (default: {DEFAULT_IDLE_TIMEOUT})",
|
|
127
|
+
)
|
|
128
|
+
|
|
109
129
|
args = parser.parse_args()
|
|
110
130
|
|
|
131
|
+
# Daemon mode handling
|
|
132
|
+
if args.daemon:
|
|
133
|
+
# Use fixed daemon port
|
|
134
|
+
args.port = DEFAULT_DAEMON_PORT
|
|
135
|
+
args.host = "127.0.0.1" # Only bind to localhost for security
|
|
136
|
+
|
|
137
|
+
# Check if another daemon is already running
|
|
138
|
+
daemon_lock = DaemonLock()
|
|
139
|
+
if not daemon_lock.acquire():
|
|
140
|
+
print(f"Daemon already running (PID: {daemon_lock.get_pid()})", file=sys.stderr)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
# Fork into background
|
|
144
|
+
daemonize()
|
|
145
|
+
|
|
146
|
+
# Re-acquire lock in child process
|
|
147
|
+
daemon_lock = DaemonLock()
|
|
148
|
+
if not daemon_lock.acquire():
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
# Register cleanup to release lock
|
|
152
|
+
def release_lock():
|
|
153
|
+
daemon_lock.release()
|
|
154
|
+
|
|
155
|
+
atexit.register(release_lock)
|
|
156
|
+
|
|
157
|
+
# Print banner (not in daemon mode)
|
|
158
|
+
if not args.daemon:
|
|
159
|
+
print()
|
|
160
|
+
print_banner()
|
|
161
|
+
|
|
111
162
|
# Configure Python logging based on log level
|
|
112
163
|
# Update config with CLI override if provided
|
|
113
164
|
if args.log_level != config.log_level:
|
|
@@ -128,9 +179,12 @@ def main():
|
|
|
128
179
|
log_level=args.log_level,
|
|
129
180
|
mcp_enabled=config.mcp_enabled,
|
|
130
181
|
graph_retriever=config.graph_retriever,
|
|
182
|
+
skip_llm_verification=config.skip_llm_verification,
|
|
183
|
+
lazy_reranker=config.lazy_reranker,
|
|
131
184
|
)
|
|
132
185
|
config.configure_logging()
|
|
133
|
-
|
|
186
|
+
if not args.daemon:
|
|
187
|
+
config.log_config()
|
|
134
188
|
|
|
135
189
|
# Register cleanup handlers
|
|
136
190
|
atexit.register(_cleanup)
|
|
@@ -149,6 +203,12 @@ def main():
|
|
|
149
203
|
initialize_memory=True,
|
|
150
204
|
)
|
|
151
205
|
|
|
206
|
+
# Wrap with idle timeout middleware in daemon mode
|
|
207
|
+
idle_middleware = None
|
|
208
|
+
if args.daemon:
|
|
209
|
+
idle_middleware = IdleTimeoutMiddleware(app, idle_timeout=args.idle_timeout)
|
|
210
|
+
app = idle_middleware
|
|
211
|
+
|
|
152
212
|
# Prepare uvicorn config
|
|
153
213
|
uvicorn_config = {
|
|
154
214
|
"app": app,
|
|
@@ -172,20 +232,40 @@ def main():
|
|
|
172
232
|
if args.ssl_certfile:
|
|
173
233
|
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
|
174
234
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
235
|
+
# Print startup info (not in daemon mode)
|
|
236
|
+
if not args.daemon:
|
|
237
|
+
from .banner import print_startup_info
|
|
238
|
+
|
|
239
|
+
print_startup_info(
|
|
240
|
+
host=args.host,
|
|
241
|
+
port=args.port,
|
|
242
|
+
database_url=config.database_url,
|
|
243
|
+
llm_provider=config.llm_provider,
|
|
244
|
+
llm_model=config.llm_model,
|
|
245
|
+
embeddings_provider=config.embeddings_provider,
|
|
246
|
+
reranker_provider=config.reranker_provider,
|
|
247
|
+
mcp_enabled=config.mcp_enabled,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Start idle checker in daemon mode
|
|
251
|
+
if idle_middleware is not None:
|
|
252
|
+
# Start the idle checker in a background thread with its own event loop
|
|
253
|
+
import threading
|
|
254
|
+
|
|
255
|
+
def run_idle_checker():
|
|
256
|
+
import time
|
|
257
|
+
|
|
258
|
+
time.sleep(2) # Wait for uvicorn to start
|
|
259
|
+
try:
|
|
260
|
+
loop = asyncio.new_event_loop()
|
|
261
|
+
asyncio.set_event_loop(loop)
|
|
262
|
+
loop.run_until_complete(idle_middleware._check_idle())
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
threading.Thread(target=run_idle_checker, daemon=True).start()
|
|
187
267
|
|
|
188
|
-
uvicorn.run(**uvicorn_config)
|
|
268
|
+
uvicorn.run(**uvicorn_config) # type: ignore[invalid-argument-type] - dict kwargs
|
|
189
269
|
|
|
190
270
|
|
|
191
271
|
if __name__ == "__main__":
|
hindsight_api/mcp_local.py
CHANGED
|
@@ -87,6 +87,7 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
|
|
|
87
87
|
from hindsight_api import MemoryEngine
|
|
88
88
|
from hindsight_api.engine.memory_engine import Budget
|
|
89
89
|
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
|
90
|
+
from hindsight_api.models import RequestContext
|
|
90
91
|
|
|
91
92
|
# Create memory engine with pg0 embedded database if not provided
|
|
92
93
|
if memory is None:
|
|
@@ -115,7 +116,11 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
|
|
|
115
116
|
|
|
116
117
|
async def _retain():
|
|
117
118
|
try:
|
|
118
|
-
await memory.retain_batch_async(
|
|
119
|
+
await memory.retain_batch_async(
|
|
120
|
+
bank_id=bank_id,
|
|
121
|
+
contents=[{"content": content, "context": context}],
|
|
122
|
+
request_context=RequestContext(),
|
|
123
|
+
)
|
|
119
124
|
except Exception as e:
|
|
120
125
|
logger.error(f"Error storing memory: {e}", exc_info=True)
|
|
121
126
|
|
|
@@ -142,6 +147,7 @@ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
|
|
|
142
147
|
fact_type=list(VALID_RECALL_FACT_TYPES),
|
|
143
148
|
budget=budget_enum,
|
|
144
149
|
max_tokens=max_tokens,
|
|
150
|
+
request_context=RequestContext(),
|
|
145
151
|
)
|
|
146
152
|
|
|
147
153
|
return search_result.model_dump()
|