hindsight-api 0.1.10__py3-none-any.whl → 0.1.12__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 (44) hide show
  1. hindsight_api/__init__.py +2 -0
  2. hindsight_api/alembic/env.py +24 -1
  3. hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +14 -4
  4. hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +54 -13
  5. hindsight_api/alembic/versions/rename_personality_to_disposition.py +18 -7
  6. hindsight_api/api/http.py +234 -228
  7. hindsight_api/api/mcp.py +14 -3
  8. hindsight_api/engine/__init__.py +12 -1
  9. hindsight_api/engine/entity_resolver.py +38 -37
  10. hindsight_api/engine/interface.py +592 -0
  11. hindsight_api/engine/llm_wrapper.py +176 -6
  12. hindsight_api/engine/memory_engine.py +993 -217
  13. hindsight_api/engine/retain/bank_utils.py +13 -12
  14. hindsight_api/engine/retain/chunk_storage.py +3 -2
  15. hindsight_api/engine/retain/fact_storage.py +10 -7
  16. hindsight_api/engine/retain/link_utils.py +17 -16
  17. hindsight_api/engine/retain/observation_regeneration.py +17 -16
  18. hindsight_api/engine/retain/orchestrator.py +2 -3
  19. hindsight_api/engine/retain/types.py +25 -8
  20. hindsight_api/engine/search/graph_retrieval.py +6 -5
  21. hindsight_api/engine/search/mpfp_retrieval.py +8 -7
  22. hindsight_api/engine/search/retrieval.py +12 -11
  23. hindsight_api/engine/search/think_utils.py +1 -1
  24. hindsight_api/engine/search/tracer.py +1 -1
  25. hindsight_api/engine/task_backend.py +32 -0
  26. hindsight_api/extensions/__init__.py +66 -0
  27. hindsight_api/extensions/base.py +81 -0
  28. hindsight_api/extensions/builtin/__init__.py +18 -0
  29. hindsight_api/extensions/builtin/tenant.py +33 -0
  30. hindsight_api/extensions/context.py +110 -0
  31. hindsight_api/extensions/http.py +89 -0
  32. hindsight_api/extensions/loader.py +125 -0
  33. hindsight_api/extensions/operation_validator.py +325 -0
  34. hindsight_api/extensions/tenant.py +63 -0
  35. hindsight_api/main.py +1 -1
  36. hindsight_api/mcp_local.py +7 -1
  37. hindsight_api/migrations.py +54 -10
  38. hindsight_api/models.py +15 -0
  39. hindsight_api/pg0.py +1 -1
  40. {hindsight_api-0.1.10.dist-info → hindsight_api-0.1.12.dist-info}/METADATA +1 -1
  41. hindsight_api-0.1.12.dist-info/RECORD +74 -0
  42. hindsight_api-0.1.10.dist-info/RECORD +0 -64
  43. {hindsight_api-0.1.10.dist-info → hindsight_api-0.1.12.dist-info}/WHEEL +0 -0
  44. {hindsight_api-0.1.10.dist-info → hindsight_api-0.1.12.dist-info}/entry_points.txt +0 -0
hindsight_api/__init__.py CHANGED
@@ -21,9 +21,11 @@ from .engine.search.trace import (
21
21
  WeightComponents,
22
22
  )
23
23
  from .engine.search.tracer import SearchTracer
24
+ from .models import RequestContext
24
25
 
25
26
  __all__ = [
26
27
  "MemoryEngine",
28
+ "RequestContext",
27
29
  "HindsightConfig",
28
30
  "get_config",
29
31
  "SearchTrace",
@@ -109,6 +109,9 @@ def run_migrations_online() -> None:
109
109
 
110
110
  get_database_url() # Process and set the database URL in config
111
111
 
112
+ # Check if we're targeting a specific schema (for multi-tenant isolation)
113
+ target_schema = config.get_main_option("target_schema")
114
+
112
115
  connectable = engine_from_config(
113
116
  config.get_section(config.config_ini_section, {}),
114
117
  prefix="sqlalchemy.",
@@ -121,14 +124,34 @@ def run_migrations_online() -> None:
121
124
  def set_read_write_mode(dbapi_connection, connection_record):
122
125
  cursor = dbapi_connection.cursor()
123
126
  cursor.execute("SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE")
127
+ # If targeting a specific schema, set search_path
128
+ # Include public in search_path for access to shared extensions (pgvector)
129
+ if target_schema:
130
+ cursor.execute(f'CREATE SCHEMA IF NOT EXISTS "{target_schema}"')
131
+ cursor.execute(f'SET search_path TO "{target_schema}", public')
124
132
  cursor.close()
125
133
 
126
134
  with connectable.connect() as connection:
127
135
  # Also explicitly set read-write mode on this connection
128
136
  connection.execute(text("SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE"))
137
+
138
+ # If targeting a specific schema, set search_path
139
+ # Include public in search_path for access to shared extensions (pgvector)
140
+ if target_schema:
141
+ connection.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{target_schema}"'))
142
+ connection.execute(text(f'SET search_path TO "{target_schema}", public'))
143
+
129
144
  connection.commit() # Commit the SET command
130
145
 
131
- context.configure(connection=connection, target_metadata=target_metadata)
146
+ # Configure context with version_table_schema if using a specific schema
147
+ context_opts = {
148
+ "connection": connection,
149
+ "target_metadata": target_metadata,
150
+ }
151
+ if target_schema:
152
+ context_opts["version_table_schema"] = target_schema
153
+
154
+ context.configure(**context_opts)
132
155
 
133
156
  with context.begin_transaction():
134
157
  context.run_migrations()
@@ -6,7 +6,7 @@ Create Date: 2024-12-04 15:00:00.000000
6
6
 
7
7
  """
8
8
 
9
- from alembic import op
9
+ from alembic import context, op
10
10
 
11
11
  # revision identifiers, used by Alembic.
12
12
  revision = "d9f6a3b4c5e2"
@@ -15,14 +15,22 @@ branch_labels = None
15
15
  depends_on = None
16
16
 
17
17
 
18
+ def _get_schema_prefix() -> str:
19
+ """Get schema prefix for table names (e.g., 'tenant_x.' or '' for public)."""
20
+ schema = context.config.get_main_option("target_schema")
21
+ return f'"{schema}".' if schema else ""
22
+
23
+
18
24
  def upgrade():
25
+ schema = _get_schema_prefix()
26
+
19
27
  # Drop old check constraint FIRST (before updating data)
20
28
  op.drop_constraint("memory_units_fact_type_check", "memory_units", type_="check")
21
29
 
22
30
  # Update existing 'bank' values to 'experience'
23
- op.execute("UPDATE memory_units SET fact_type = 'experience' WHERE fact_type = 'bank'")
31
+ op.execute(f"UPDATE {schema}memory_units SET fact_type = 'experience' WHERE fact_type = 'bank'")
24
32
  # Also update any 'interactions' values (in case of partial migration)
25
- op.execute("UPDATE memory_units SET fact_type = 'experience' WHERE fact_type = 'interactions'")
33
+ op.execute(f"UPDATE {schema}memory_units SET fact_type = 'experience' WHERE fact_type = 'interactions'")
26
34
 
27
35
  # Create new check constraint with 'experience' instead of 'bank'
28
36
  op.create_check_constraint(
@@ -31,11 +39,13 @@ def upgrade():
31
39
 
32
40
 
33
41
  def downgrade():
42
+ schema = _get_schema_prefix()
43
+
34
44
  # Drop new check constraint FIRST
35
45
  op.drop_constraint("memory_units_fact_type_check", "memory_units", type_="check")
36
46
 
37
47
  # Update 'experience' back to 'bank'
38
- op.execute("UPDATE memory_units SET fact_type = 'bank' WHERE fact_type = 'experience'")
48
+ op.execute(f"UPDATE {schema}memory_units SET fact_type = 'bank' WHERE fact_type = 'experience'")
39
49
 
40
50
  # Recreate old check constraint
41
51
  op.create_check_constraint(
@@ -12,7 +12,7 @@ system (skepticism, literalism, empathy with 1-5 integer values).
12
12
  from collections.abc import Sequence
13
13
 
14
14
  import sqlalchemy as sa
15
- from alembic import op
15
+ from alembic import context, op
16
16
 
17
17
  # revision identifiers, used by Alembic.
18
18
  revision: str = "e0a1b2c3d4e5"
@@ -21,9 +21,36 @@ branch_labels: str | Sequence[str] | None = None
21
21
  depends_on: str | Sequence[str] | None = None
22
22
 
23
23
 
24
+ def _get_schema_prefix() -> str:
25
+ """Get schema prefix for table names (e.g., 'tenant_x.' or '' for public)."""
26
+ schema = context.config.get_main_option("target_schema")
27
+ return f'"{schema}".' if schema else ""
28
+
29
+
30
+ def _get_target_schema() -> str:
31
+ """Get the target schema name (tenant schema or 'public')."""
32
+ schema = context.config.get_main_option("target_schema")
33
+ return schema if schema else "public"
34
+
35
+
24
36
  def upgrade() -> None:
25
37
  """Convert Big Five disposition to 3-trait disposition."""
26
38
  conn = op.get_bind()
39
+ schema = _get_schema_prefix()
40
+ target_schema = _get_target_schema()
41
+
42
+ # Check if disposition column exists (should have been created by previous migration)
43
+ result = conn.execute(
44
+ sa.text("""
45
+ SELECT column_name
46
+ FROM information_schema.columns
47
+ WHERE table_schema = :schema AND table_name = 'banks' AND column_name = 'disposition'
48
+ """),
49
+ {"schema": target_schema},
50
+ )
51
+ if not result.fetchone():
52
+ # Column doesn't exist yet (shouldn't happen but be safe)
53
+ return
27
54
 
28
55
  # Update all existing banks to use the new disposition format
29
56
  # Convert from old format to new format with reasonable mappings:
@@ -32,18 +59,18 @@ def upgrade() -> None:
32
59
  # - empathy: derived from agreeableness + inverse of neuroticism
33
60
  # Default all to 3 (neutral) for simplicity
34
61
  conn.execute(
35
- sa.text("""
36
- UPDATE banks
37
- SET disposition = '{"skepticism": 3, "literalism": 3, "empathy": 3}'::jsonb
62
+ sa.text(f"""
63
+ UPDATE {schema}banks
64
+ SET disposition = '{{"skepticism": 3, "literalism": 3, "empathy": 3}}'::jsonb
38
65
  WHERE disposition IS NOT NULL
39
66
  """)
40
67
  )
41
68
 
42
69
  # Update the default for new banks
43
70
  conn.execute(
44
- sa.text("""
45
- ALTER TABLE banks
46
- ALTER COLUMN disposition SET DEFAULT '{"skepticism": 3, "literalism": 3, "empathy": 3}'::jsonb
71
+ sa.text(f"""
72
+ ALTER TABLE {schema}banks
73
+ ALTER COLUMN disposition SET DEFAULT '{{"skepticism": 3, "literalism": 3, "empathy": 3}}'::jsonb
47
74
  """)
48
75
  )
49
76
 
@@ -51,20 +78,34 @@ def upgrade() -> None:
51
78
  def downgrade() -> None:
52
79
  """Convert back to Big Five disposition."""
53
80
  conn = op.get_bind()
81
+ schema = _get_schema_prefix()
82
+ target_schema = _get_target_schema()
83
+
84
+ # Check if disposition column exists
85
+ result = conn.execute(
86
+ sa.text("""
87
+ SELECT column_name
88
+ FROM information_schema.columns
89
+ WHERE table_schema = :schema AND table_name = 'banks' AND column_name = 'disposition'
90
+ """),
91
+ {"schema": target_schema},
92
+ )
93
+ if not result.fetchone():
94
+ return
54
95
 
55
96
  # Revert to Big Five format with default values
56
97
  conn.execute(
57
- sa.text("""
58
- UPDATE banks
59
- SET disposition = '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb
98
+ sa.text(f"""
99
+ UPDATE {schema}banks
100
+ SET disposition = '{{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}}'::jsonb
60
101
  WHERE disposition IS NOT NULL
61
102
  """)
62
103
  )
63
104
 
64
105
  # Update the default for new banks
65
106
  conn.execute(
66
- sa.text("""
67
- ALTER TABLE banks
68
- ALTER COLUMN disposition SET DEFAULT '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb
107
+ sa.text(f"""
108
+ ALTER TABLE {schema}banks
109
+ ALTER COLUMN disposition SET DEFAULT '{{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}}'::jsonb
69
110
  """)
70
111
  )
@@ -9,7 +9,7 @@ Create Date: 2024-12-04
9
9
  from collections.abc import Sequence
10
10
 
11
11
  import sqlalchemy as sa
12
- from alembic import op
12
+ from alembic import context, op
13
13
  from sqlalchemy.dialects import postgresql
14
14
 
15
15
  # revision identifiers, used by Alembic.
@@ -19,17 +19,25 @@ branch_labels: str | Sequence[str] | None = None
19
19
  depends_on: str | Sequence[str] | None = None
20
20
 
21
21
 
22
+ def _get_target_schema() -> str:
23
+ """Get the target schema name (tenant schema or 'public')."""
24
+ schema = context.config.get_main_option("target_schema")
25
+ return schema if schema else "public"
26
+
27
+
22
28
  def upgrade() -> None:
23
29
  """Rename personality column to disposition in banks table (if it exists)."""
24
30
  conn = op.get_bind()
31
+ target_schema = _get_target_schema()
25
32
 
26
33
  # Check if 'personality' column exists (old database)
27
34
  result = conn.execute(
28
35
  sa.text("""
29
36
  SELECT column_name
30
37
  FROM information_schema.columns
31
- WHERE table_name = 'banks' AND column_name = 'personality'
32
- """)
38
+ WHERE table_schema = :schema AND table_name = 'banks' AND column_name = 'personality'
39
+ """),
40
+ {"schema": target_schema},
33
41
  )
34
42
  has_personality = result.fetchone() is not None
35
43
 
@@ -38,8 +46,9 @@ def upgrade() -> None:
38
46
  sa.text("""
39
47
  SELECT column_name
40
48
  FROM information_schema.columns
41
- WHERE table_name = 'banks' AND column_name = 'disposition'
42
- """)
49
+ WHERE table_schema = :schema AND table_name = 'banks' AND column_name = 'disposition'
50
+ """),
51
+ {"schema": target_schema},
43
52
  )
44
53
  has_disposition = result.fetchone() is not None
45
54
 
@@ -63,12 +72,14 @@ def upgrade() -> None:
63
72
  def downgrade() -> None:
64
73
  """Revert disposition column back to personality."""
65
74
  conn = op.get_bind()
75
+ target_schema = _get_target_schema()
66
76
  result = conn.execute(
67
77
  sa.text("""
68
78
  SELECT column_name
69
79
  FROM information_schema.columns
70
- WHERE table_name = 'banks' AND column_name = 'disposition'
71
- """)
80
+ WHERE table_schema = :schema AND table_name = 'banks' AND column_name = 'disposition'
81
+ """),
82
+ {"schema": target_schema},
72
83
  )
73
84
  if result.fetchone():
74
85
  op.alter_column("banks", "disposition", new_column_name="personality")