hindsight-api 0.3.0__py3-none-any.whl → 0.4.0__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.
Files changed (74) hide show
  1. hindsight_api/admin/cli.py +59 -0
  2. hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
  3. hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
  4. hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
  5. hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
  6. hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
  7. hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
  8. hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
  9. hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
  10. hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
  11. hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
  12. hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
  13. hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
  14. hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
  15. hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
  16. hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
  17. hindsight_api/api/http.py +1119 -93
  18. hindsight_api/api/mcp.py +11 -191
  19. hindsight_api/config.py +145 -45
  20. hindsight_api/engine/consolidation/__init__.py +5 -0
  21. hindsight_api/engine/consolidation/consolidator.py +859 -0
  22. hindsight_api/engine/consolidation/prompts.py +69 -0
  23. hindsight_api/engine/cross_encoder.py +114 -9
  24. hindsight_api/engine/directives/__init__.py +5 -0
  25. hindsight_api/engine/directives/models.py +37 -0
  26. hindsight_api/engine/embeddings.py +102 -5
  27. hindsight_api/engine/interface.py +32 -13
  28. hindsight_api/engine/llm_wrapper.py +505 -43
  29. hindsight_api/engine/memory_engine.py +2090 -1089
  30. hindsight_api/engine/mental_models/__init__.py +14 -0
  31. hindsight_api/engine/mental_models/models.py +53 -0
  32. hindsight_api/engine/reflect/__init__.py +18 -0
  33. hindsight_api/engine/reflect/agent.py +933 -0
  34. hindsight_api/engine/reflect/models.py +109 -0
  35. hindsight_api/engine/reflect/observations.py +186 -0
  36. hindsight_api/engine/reflect/prompts.py +483 -0
  37. hindsight_api/engine/reflect/tools.py +437 -0
  38. hindsight_api/engine/reflect/tools_schema.py +250 -0
  39. hindsight_api/engine/response_models.py +130 -4
  40. hindsight_api/engine/retain/bank_utils.py +79 -201
  41. hindsight_api/engine/retain/fact_extraction.py +81 -48
  42. hindsight_api/engine/retain/fact_storage.py +5 -8
  43. hindsight_api/engine/retain/link_utils.py +5 -8
  44. hindsight_api/engine/retain/orchestrator.py +1 -55
  45. hindsight_api/engine/retain/types.py +2 -2
  46. hindsight_api/engine/search/graph_retrieval.py +2 -2
  47. hindsight_api/engine/search/link_expansion_retrieval.py +164 -29
  48. hindsight_api/engine/search/mpfp_retrieval.py +1 -1
  49. hindsight_api/engine/search/retrieval.py +14 -14
  50. hindsight_api/engine/search/think_utils.py +41 -140
  51. hindsight_api/engine/search/trace.py +0 -1
  52. hindsight_api/engine/search/tracer.py +2 -5
  53. hindsight_api/engine/search/types.py +0 -3
  54. hindsight_api/engine/task_backend.py +112 -196
  55. hindsight_api/engine/utils.py +0 -151
  56. hindsight_api/extensions/__init__.py +10 -1
  57. hindsight_api/extensions/builtin/tenant.py +5 -1
  58. hindsight_api/extensions/operation_validator.py +81 -4
  59. hindsight_api/extensions/tenant.py +26 -0
  60. hindsight_api/main.py +16 -5
  61. hindsight_api/mcp_local.py +12 -53
  62. hindsight_api/mcp_tools.py +494 -0
  63. hindsight_api/models.py +0 -2
  64. hindsight_api/worker/__init__.py +11 -0
  65. hindsight_api/worker/main.py +296 -0
  66. hindsight_api/worker/poller.py +486 -0
  67. {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/METADATA +12 -6
  68. hindsight_api-0.4.0.dist-info/RECORD +112 -0
  69. {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/entry_points.txt +1 -0
  70. hindsight_api/engine/retain/observation_regeneration.py +0 -254
  71. hindsight_api/engine/search/observation_utils.py +0 -125
  72. hindsight_api/engine/search/scoring.py +0 -159
  73. hindsight_api-0.3.0.dist-info/RECORD +0 -82
  74. {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.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"]