hindsight-api 0.2.1__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 (88) hide show
  1. hindsight_api/admin/__init__.py +1 -0
  2. hindsight_api/admin/cli.py +311 -0
  3. hindsight_api/alembic/versions/f1a2b3c4d5e6_add_memory_links_composite_index.py +44 -0
  4. hindsight_api/alembic/versions/g2a3b4c5d6e7_add_tags_column.py +48 -0
  5. hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
  6. hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
  7. hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
  8. hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
  9. hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
  10. hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
  11. hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
  12. hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
  13. hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
  14. hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
  15. hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
  16. hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
  17. hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
  18. hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
  19. hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
  20. hindsight_api/api/http.py +1406 -118
  21. hindsight_api/api/mcp.py +11 -196
  22. hindsight_api/config.py +359 -27
  23. hindsight_api/engine/consolidation/__init__.py +5 -0
  24. hindsight_api/engine/consolidation/consolidator.py +859 -0
  25. hindsight_api/engine/consolidation/prompts.py +69 -0
  26. hindsight_api/engine/cross_encoder.py +706 -88
  27. hindsight_api/engine/db_budget.py +284 -0
  28. hindsight_api/engine/db_utils.py +11 -0
  29. hindsight_api/engine/directives/__init__.py +5 -0
  30. hindsight_api/engine/directives/models.py +37 -0
  31. hindsight_api/engine/embeddings.py +553 -29
  32. hindsight_api/engine/entity_resolver.py +8 -5
  33. hindsight_api/engine/interface.py +40 -17
  34. hindsight_api/engine/llm_wrapper.py +744 -68
  35. hindsight_api/engine/memory_engine.py +2505 -1017
  36. hindsight_api/engine/mental_models/__init__.py +14 -0
  37. hindsight_api/engine/mental_models/models.py +53 -0
  38. hindsight_api/engine/query_analyzer.py +4 -3
  39. hindsight_api/engine/reflect/__init__.py +18 -0
  40. hindsight_api/engine/reflect/agent.py +933 -0
  41. hindsight_api/engine/reflect/models.py +109 -0
  42. hindsight_api/engine/reflect/observations.py +186 -0
  43. hindsight_api/engine/reflect/prompts.py +483 -0
  44. hindsight_api/engine/reflect/tools.py +437 -0
  45. hindsight_api/engine/reflect/tools_schema.py +250 -0
  46. hindsight_api/engine/response_models.py +168 -4
  47. hindsight_api/engine/retain/bank_utils.py +79 -201
  48. hindsight_api/engine/retain/fact_extraction.py +424 -195
  49. hindsight_api/engine/retain/fact_storage.py +35 -12
  50. hindsight_api/engine/retain/link_utils.py +29 -24
  51. hindsight_api/engine/retain/orchestrator.py +24 -43
  52. hindsight_api/engine/retain/types.py +11 -2
  53. hindsight_api/engine/search/graph_retrieval.py +43 -14
  54. hindsight_api/engine/search/link_expansion_retrieval.py +391 -0
  55. hindsight_api/engine/search/mpfp_retrieval.py +362 -117
  56. hindsight_api/engine/search/reranking.py +2 -2
  57. hindsight_api/engine/search/retrieval.py +848 -201
  58. hindsight_api/engine/search/tags.py +172 -0
  59. hindsight_api/engine/search/think_utils.py +42 -141
  60. hindsight_api/engine/search/trace.py +12 -1
  61. hindsight_api/engine/search/tracer.py +26 -6
  62. hindsight_api/engine/search/types.py +21 -3
  63. hindsight_api/engine/task_backend.py +113 -106
  64. hindsight_api/engine/utils.py +1 -152
  65. hindsight_api/extensions/__init__.py +10 -1
  66. hindsight_api/extensions/builtin/tenant.py +5 -1
  67. hindsight_api/extensions/context.py +10 -1
  68. hindsight_api/extensions/operation_validator.py +81 -4
  69. hindsight_api/extensions/tenant.py +26 -0
  70. hindsight_api/main.py +69 -6
  71. hindsight_api/mcp_local.py +12 -53
  72. hindsight_api/mcp_tools.py +494 -0
  73. hindsight_api/metrics.py +433 -48
  74. hindsight_api/migrations.py +141 -1
  75. hindsight_api/models.py +3 -3
  76. hindsight_api/pg0.py +53 -0
  77. hindsight_api/server.py +39 -2
  78. hindsight_api/worker/__init__.py +11 -0
  79. hindsight_api/worker/main.py +296 -0
  80. hindsight_api/worker/poller.py +486 -0
  81. {hindsight_api-0.2.1.dist-info → hindsight_api-0.4.0.dist-info}/METADATA +16 -6
  82. hindsight_api-0.4.0.dist-info/RECORD +112 -0
  83. {hindsight_api-0.2.1.dist-info → hindsight_api-0.4.0.dist-info}/entry_points.txt +2 -0
  84. hindsight_api/engine/retain/observation_regeneration.py +0 -254
  85. hindsight_api/engine/search/observation_utils.py +0 -125
  86. hindsight_api/engine/search/scoring.py +0 -159
  87. hindsight_api-0.2.1.dist-info/RECORD +0 -75
  88. {hindsight_api-0.2.1.dist-info → hindsight_api-0.4.0.dist-info}/WHEEL +0 -0
@@ -10,8 +10,94 @@ from typing import Any
10
10
 
11
11
  from pydantic import BaseModel, ConfigDict, Field
12
12
 
13
- # Valid fact types for recall operations (excludes 'observation' which is internal)
14
- VALID_RECALL_FACT_TYPES = frozenset(["world", "experience", "opinion"])
13
+ # Valid fact types for recall operations (excludes 'opinion' which is deprecated)
14
+ VALID_RECALL_FACT_TYPES = frozenset(["world", "experience", "observation"])
15
+
16
+
17
+ class LLMToolCall(BaseModel):
18
+ """A tool call requested by the LLM."""
19
+
20
+ id: str = Field(description="Unique identifier for this tool call")
21
+ name: str = Field(description="Name of the tool to call")
22
+ arguments: dict[str, Any] = Field(description="Arguments to pass to the tool")
23
+
24
+
25
+ class LLMToolCallResult(BaseModel):
26
+ """Result from an LLM call that may include tool calls."""
27
+
28
+ content: str | None = Field(default=None, description="Text content if any")
29
+ tool_calls: list[LLMToolCall] = Field(default_factory=list, description="Tool calls requested by the LLM")
30
+ finish_reason: str | None = Field(default=None, description="Reason the LLM stopped: 'stop', 'tool_calls', etc.")
31
+ input_tokens: int = Field(default=0, description="Input tokens used in this call")
32
+ output_tokens: int = Field(default=0, description="Output tokens used in this call")
33
+
34
+
35
+ class ToolCallTrace(BaseModel):
36
+ """A single tool call made during reflect."""
37
+
38
+ tool: str = Field(description="Tool name: lookup, recall, learn, expand")
39
+ reason: str | None = Field(default=None, description="Agent's reasoning for making this tool call")
40
+ input: dict = Field(description="Tool input parameters")
41
+ output: dict = Field(description="Tool output/result")
42
+ duration_ms: int = Field(description="Execution time in milliseconds")
43
+ iteration: int = Field(default=0, description="Iteration number (1-based) when this tool was called")
44
+
45
+
46
+ class LLMCallTrace(BaseModel):
47
+ """A single LLM call made during reflect."""
48
+
49
+ scope: str = Field(description="Call scope: agent_1, agent_2, final, etc.")
50
+ duration_ms: int = Field(description="Execution time in milliseconds")
51
+
52
+
53
+ class ObservationRef(BaseModel):
54
+ """Reference to an observation accessed during reflect."""
55
+
56
+ id: str = Field(description="Observation ID")
57
+ name: str = Field(description="Observation name")
58
+ type: str = Field(description="Observation type: entity, concept, event")
59
+ subtype: str = Field(description="Observation subtype: structural, emergent, learned")
60
+ description: str = Field(description="Brief description")
61
+ summary: str | None = Field(default=None, description="Full summary (when looked up in detail)")
62
+
63
+
64
+ class DirectiveRef(BaseModel):
65
+ """Reference to a directive that was applied during reflect."""
66
+
67
+ id: str = Field(description="Directive mental model ID")
68
+ name: str = Field(description="Directive name")
69
+ content: str = Field(description="Directive content")
70
+
71
+
72
+ class TokenUsage(BaseModel):
73
+ """
74
+ Token usage metrics for LLM calls.
75
+
76
+ Tracks input/output tokens for a single request to enable
77
+ per-request cost tracking and monitoring.
78
+ """
79
+
80
+ model_config = ConfigDict(
81
+ json_schema_extra={
82
+ "example": {
83
+ "input_tokens": 1500,
84
+ "output_tokens": 500,
85
+ "total_tokens": 2000,
86
+ }
87
+ }
88
+ )
89
+
90
+ input_tokens: int = Field(default=0, description="Number of input/prompt tokens consumed")
91
+ output_tokens: int = Field(default=0, description="Number of output/completion tokens generated")
92
+ total_tokens: int = Field(default=0, description="Total tokens (input + output)")
93
+
94
+ def __add__(self, other: "TokenUsage") -> "TokenUsage":
95
+ """Allow aggregating token usage from multiple calls."""
96
+ return TokenUsage(
97
+ input_tokens=self.input_tokens + other.input_tokens,
98
+ output_tokens=self.output_tokens + other.output_tokens,
99
+ total_tokens=self.total_tokens + other.total_tokens,
100
+ )
15
101
 
16
102
 
17
103
  class DispositionTraits(BaseModel):
@@ -54,6 +140,7 @@ class MemoryFact(BaseModel):
54
140
  "metadata": {"source": "slack"},
55
141
  "chunk_id": "bank123_session_abc123_0",
56
142
  "activation": 0.95,
143
+ "tags": ["user_a", "session_123"],
57
144
  }
58
145
  }
59
146
  )
@@ -71,6 +158,7 @@ class MemoryFact(BaseModel):
71
158
  chunk_id: str | None = Field(
72
159
  None, description="ID of the chunk this fact was extracted from (format: bank_id_document_id_chunk_index)"
73
160
  )
161
+ tags: list[str] | None = Field(None, description="Visibility scope tags associated with this fact")
74
162
 
75
163
 
76
164
  class ChunkInfo(BaseModel):
@@ -81,6 +169,28 @@ class ChunkInfo(BaseModel):
81
169
  truncated: bool = Field(default=False, description="Whether the chunk was truncated due to token limits")
82
170
 
83
171
 
172
+ class ObservationResult(BaseModel):
173
+ """An observation result from recall (consolidated knowledge synthesized from facts)."""
174
+
175
+ id: str = Field(description="Unique observation ID")
176
+ text: str = Field(description="The observation text")
177
+ proof_count: int = Field(description="Number of facts supporting this observation")
178
+ relevance: float = Field(default=0.0, description="Relevance score to the query")
179
+ tags: list[str] | None = Field(default=None, description="Tags for visibility scoping")
180
+ source_memory_ids: list[str] = Field(
181
+ default_factory=list, description="IDs of facts that contribute to this observation"
182
+ )
183
+
184
+
185
+ class MentalModelResult(BaseModel):
186
+ """A mental model result from recall (stored reflect response)."""
187
+
188
+ id: str = Field(description="Unique mental model ID")
189
+ name: str = Field(description="Human-readable name")
190
+ content: str = Field(description="The synthesized content")
191
+ relevance: float = Field(default=0.0, description="Relevance score to the query")
192
+
193
+
84
194
  class RecallResult(BaseModel):
85
195
  """
86
196
  Result from a recall operation.
@@ -144,22 +254,47 @@ class ReflectResult(BaseModel):
144
254
  ],
145
255
  "experience": [],
146
256
  "opinion": [],
257
+ "mental_models": [],
258
+ "directives": [
259
+ {
260
+ "id": "directive-123",
261
+ "name": "Response Style",
262
+ "rules": ["Always be concise"],
263
+ }
264
+ ],
147
265
  },
148
266
  "new_opinions": ["Machine learning has great potential in healthcare"],
149
267
  "structured_output": {"summary": "ML in healthcare", "confidence": 0.9},
268
+ "usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000},
150
269
  }
151
270
  }
152
271
  )
153
272
 
154
273
  text: str = Field(description="The formulated answer text")
155
- based_on: dict[str, list[MemoryFact]] = Field(
156
- description="Facts used to formulate the answer, organized by type (world, experience, opinion)"
274
+ based_on: dict[str, Any] = Field(
275
+ description="Facts used to formulate the answer, organized by type (world, experience, opinion, mental_models, directives)"
157
276
  )
158
277
  new_opinions: list[str] = Field(default_factory=list, description="List of newly formed opinions during reflection")
159
278
  structured_output: dict[str, Any] | None = Field(
160
279
  default=None,
161
280
  description="Structured output parsed according to the provided response schema. Only present when response_schema was provided.",
162
281
  )
282
+ usage: TokenUsage | None = Field(
283
+ default=None,
284
+ description="Token usage metrics for the LLM calls made during this reflect operation.",
285
+ )
286
+ tool_trace: list[ToolCallTrace] = Field(
287
+ default_factory=list,
288
+ description="Trace of tool calls made during reflection. Only present when include.tool_calls is enabled.",
289
+ )
290
+ llm_trace: list[LLMCallTrace] = Field(
291
+ default_factory=list,
292
+ description="Trace of LLM calls made during reflection. Only present when include.tool_calls is enabled.",
293
+ )
294
+ directives_applied: list[DirectiveRef] = Field(
295
+ default_factory=list,
296
+ description="Directive mental models that were applied during this reflection.",
297
+ )
163
298
 
164
299
 
165
300
  class Opinion(BaseModel):
@@ -223,3 +358,32 @@ class EntityState(BaseModel):
223
358
  observations: list[EntityObservation] = Field(
224
359
  default_factory=list, description="List of observations about this entity"
225
360
  )
361
+
362
+
363
+ class MentalModel(BaseModel):
364
+ """
365
+ A manually configured mental model for tracking specific topics/areas.
366
+
367
+ Mental models are user-defined focus areas that the agent should track
368
+ and maintain summaries for, unlike auto-extracted entities.
369
+ """
370
+
371
+ model_config = ConfigDict(
372
+ json_schema_extra={
373
+ "example": {
374
+ "id": "team-dynamics",
375
+ "name": "Team Dynamics",
376
+ "description": "Track how the team collaborates, communication patterns, conflicts, and resolutions",
377
+ "summary": "The team has strong collaboration...",
378
+ "summary_updated_at": "2024-01-15T10:30:00Z",
379
+ "created_at": "2024-01-10T08:00:00Z",
380
+ }
381
+ }
382
+ )
383
+
384
+ id: str = Field(description="Unique identifier (alphanumeric lowercase)")
385
+ name: str = Field(description="Display name for the mental model")
386
+ description: str = Field(description="Prompt/directions for what to track and summarize")
387
+ summary: str | None = Field(None, description="Generated summary based on relevant facts")
388
+ summary_updated_at: str | None = Field(None, description="ISO format date when summary was last updated")
389
+ created_at: str = Field(description="ISO format date when the mental model was created")
@@ -1,5 +1,5 @@
1
1
  """
2
- bank profile utilities for disposition and background management.
2
+ bank profile utilities for disposition and mission management.
3
3
  """
4
4
 
5
5
  import json
@@ -27,19 +27,18 @@ class BankProfile(TypedDict):
27
27
 
28
28
  name: str
29
29
  disposition: DispositionTraits
30
- background: str
30
+ mission: str
31
31
 
32
32
 
33
- class BackgroundMergeResponse(BaseModel):
34
- """LLM response for background merge with disposition inference."""
33
+ class MissionMergeResponse(BaseModel):
34
+ """LLM response for mission merge."""
35
35
 
36
- background: str = Field(description="Merged background in first person perspective")
37
- disposition: DispositionTraits = Field(description="Inferred disposition traits (skepticism, literalism, empathy)")
36
+ mission: str = Field(description="Merged mission in first person perspective")
38
37
 
39
38
 
40
39
  async def get_bank_profile(pool, bank_id: str) -> BankProfile:
41
40
  """
42
- Get bank profile (name, disposition + background).
41
+ Get bank profile (name, disposition + mission).
43
42
  Auto-creates bank with default values if not exists.
44
43
 
45
44
  Args:
@@ -47,13 +46,13 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
47
46
  bank_id: bank IDentifier
48
47
 
49
48
  Returns:
50
- BankProfile with name, typed DispositionTraits, and background
49
+ BankProfile with name, typed DispositionTraits, and mission
51
50
  """
52
51
  async with acquire_with_retry(pool) as conn:
53
52
  # Try to get existing bank
54
53
  row = await conn.fetchrow(
55
54
  f"""
56
- SELECT name, disposition, background
55
+ SELECT name, disposition, mission
57
56
  FROM {fq_table("banks")} WHERE bank_id = $1
58
57
  """,
59
58
  bank_id,
@@ -66,13 +65,15 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
66
65
  disposition_data = json.loads(disposition_data)
67
66
 
68
67
  return BankProfile(
69
- name=row["name"], disposition=DispositionTraits(**disposition_data), background=row["background"]
68
+ name=row["name"],
69
+ disposition=DispositionTraits(**disposition_data),
70
+ mission=row["mission"] or "",
70
71
  )
71
72
 
72
73
  # Bank doesn't exist, create with defaults
73
74
  await conn.execute(
74
75
  f"""
75
- INSERT INTO {fq_table("banks")} (bank_id, name, disposition, background)
76
+ INSERT INTO {fq_table("banks")} (bank_id, name, disposition, mission)
76
77
  VALUES ($1, $2, $3::jsonb, $4)
77
78
  ON CONFLICT (bank_id) DO NOTHING
78
79
  """,
@@ -82,7 +83,7 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
82
83
  "",
83
84
  )
84
85
 
85
- return BankProfile(name=bank_id, disposition=DispositionTraits(**DEFAULT_DISPOSITION), background="")
86
+ return BankProfile(name=bank_id, disposition=DispositionTraits(**DEFAULT_DISPOSITION), mission="")
86
87
 
87
88
 
88
89
  async def update_bank_disposition(pool, bank_id: str, disposition: dict[str, int]) -> None:
@@ -110,244 +111,121 @@ async def update_bank_disposition(pool, bank_id: str, disposition: dict[str, int
110
111
  )
111
112
 
112
113
 
113
- async def merge_bank_background(pool, llm_config, bank_id: str, new_info: str, update_disposition: bool = True) -> dict:
114
+ async def set_bank_mission(pool, bank_id: str, mission: str) -> None:
114
115
  """
115
- Merge new background information with existing background using LLM.
116
+ Set bank mission (replacing any existing mission).
117
+
118
+ Args:
119
+ pool: Database connection pool
120
+ bank_id: bank IDentifier
121
+ mission: The mission text
122
+ """
123
+ # Ensure bank exists first
124
+ await get_bank_profile(pool, bank_id)
125
+
126
+ async with acquire_with_retry(pool) as conn:
127
+ await conn.execute(
128
+ f"""
129
+ UPDATE {fq_table("banks")}
130
+ SET mission = $2,
131
+ updated_at = NOW()
132
+ WHERE bank_id = $1
133
+ """,
134
+ bank_id,
135
+ mission,
136
+ )
137
+
138
+
139
+ async def merge_bank_mission(pool, llm_config, bank_id: str, new_info: str) -> dict:
140
+ """
141
+ Merge new mission information with existing mission using LLM.
116
142
  Normalizes to first person ("I") and resolves conflicts.
117
- Optionally infers disposition traits from the merged background.
118
143
 
119
144
  Args:
120
145
  pool: Database connection pool
121
- llm_config: LLM configuration for background merging
146
+ llm_config: LLM configuration for mission merging
122
147
  bank_id: bank IDentifier
123
- new_info: New background information to add/merge
124
- update_disposition: If True, infer Big Five traits from background (default: True)
148
+ new_info: New mission information to add/merge
125
149
 
126
150
  Returns:
127
- Dict with 'background' (str) and optionally 'disposition' (dict) keys
151
+ Dict with 'mission' (str) key
128
152
  """
129
153
  # Get current profile
130
154
  profile = await get_bank_profile(pool, bank_id)
131
- current_background = profile["background"]
155
+ current_mission = profile["mission"]
132
156
 
133
- # Use LLM to merge backgrounds and optionally infer disposition
134
- result = await _llm_merge_background(llm_config, current_background, new_info, infer_disposition=update_disposition)
157
+ # Use LLM to merge missions
158
+ result = await _llm_merge_mission(llm_config, current_mission, new_info)
135
159
 
136
- merged_background = result["background"]
137
- inferred_disposition = result.get("disposition")
160
+ merged_mission = result["mission"]
138
161
 
139
162
  # Update in database
140
163
  async with acquire_with_retry(pool) as conn:
141
- if inferred_disposition:
142
- # Update both background and disposition
143
- await conn.execute(
144
- f"""
145
- UPDATE {fq_table("banks")}
146
- SET background = $2,
147
- disposition = $3::jsonb,
148
- updated_at = NOW()
149
- WHERE bank_id = $1
150
- """,
151
- bank_id,
152
- merged_background,
153
- json.dumps(inferred_disposition),
154
- )
155
- else:
156
- # Update only background
157
- await conn.execute(
158
- f"""
159
- UPDATE {fq_table("banks")}
160
- SET background = $2,
161
- updated_at = NOW()
162
- WHERE bank_id = $1
163
- """,
164
- bank_id,
165
- merged_background,
166
- )
167
-
168
- response = {"background": merged_background}
169
- if inferred_disposition:
170
- response["disposition"] = inferred_disposition
164
+ await conn.execute(
165
+ f"""
166
+ UPDATE {fq_table("banks")}
167
+ SET mission = $2,
168
+ updated_at = NOW()
169
+ WHERE bank_id = $1
170
+ """,
171
+ bank_id,
172
+ merged_mission,
173
+ )
171
174
 
172
- return response
175
+ return {"mission": merged_mission}
173
176
 
174
177
 
175
- async def _llm_merge_background(llm_config, current: str, new_info: str, infer_disposition: bool = False) -> dict:
178
+ async def _llm_merge_mission(llm_config, current: str, new_info: str) -> dict:
176
179
  """
177
- Use LLM to intelligently merge background information.
178
- Optionally infer Big Five disposition traits from the merged background.
180
+ Use LLM to intelligently merge mission information.
179
181
 
180
182
  Args:
181
183
  llm_config: LLM configuration to use
182
- current: Current background text
184
+ current: Current mission text
183
185
  new_info: New information to merge
184
- infer_disposition: If True, also infer disposition traits
185
186
 
186
187
  Returns:
187
- Dict with 'background' (str) and optionally 'disposition' (dict) keys
188
+ Dict with 'mission' (str) key
188
189
  """
189
- if infer_disposition:
190
- prompt = f"""You are helping maintain a memory bank's background/profile and infer their disposition. You MUST respond with ONLY valid JSON.
190
+ prompt = f"""You are helping maintain an agent's mission statement.
191
191
 
192
- Current background: {current if current else "(empty)"}
192
+ Current mission: {current if current else "(empty)"}
193
193
 
194
194
  New information to add: {new_info}
195
195
 
196
196
  Instructions:
197
- 1. Merge the new information with the current background
198
- 2. If there are conflicts (e.g., different birthplaces), the NEW information overwrites the old
199
- 3. Keep additions that don't conflict
200
- 4. Output in FIRST PERSON ("I") perspective
201
- 5. Be concise - keep merged background under 500 characters
202
- 6. Infer disposition traits from the merged background (each 1-5 integer):
203
- - Skepticism: 1-5 (1=trusting, takes things at face value; 5=skeptical, questions everything)
204
- - Literalism: 1-5 (1=flexible interpretation, reads between lines; 5=literal, exact interpretation)
205
- - Empathy: 1-5 (1=detached, focuses on facts; 5=empathetic, considers emotional context)
206
-
207
- CRITICAL: You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations. Just the JSON.
208
-
209
- Format:
210
- {{
211
- "background": "the merged background text in first person",
212
- "disposition": {{
213
- "skepticism": 3,
214
- "literalism": 3,
215
- "empathy": 3
216
- }}
217
- }}
218
-
219
- Trait inference examples:
220
- - "I'm a lawyer" → skepticism: 4, literalism: 5, empathy: 2
221
- - "I'm a therapist" → skepticism: 2, literalism: 2, empathy: 5
222
- - "I'm an engineer" → skepticism: 3, literalism: 4, empathy: 3
223
- - "I've been burned before by trusting people" → skepticism: 5, literalism: 3, empathy: 3
224
- - "I try to understand what people really mean" → skepticism: 3, literalism: 2, empathy: 4
225
- - "I take contracts very seriously" → skepticism: 4, literalism: 5, empathy: 2"""
226
- else:
227
- prompt = f"""You are helping maintain a memory bank's background/profile.
228
-
229
- Current background: {current if current else "(empty)"}
230
-
231
- New information to add: {new_info}
232
-
233
- Instructions:
234
- 1. Merge the new information with the current background
235
- 2. If there are conflicts (e.g., different birthplaces), the NEW information overwrites the old
197
+ 1. Merge the new information with the current mission
198
+ 2. If there are conflicts, the NEW information overwrites the old
236
199
  3. Keep additions that don't conflict
237
200
  4. Output in FIRST PERSON ("I") perspective
238
201
  5. Be concise - keep it under 500 characters
239
- 6. Return ONLY the merged background text, no explanations
202
+ 6. Return ONLY the merged mission text, no explanations
240
203
 
241
- Merged background:"""
204
+ Merged mission:"""
242
205
 
243
206
  try:
244
- # Prepare messages
245
207
  messages = [{"role": "user", "content": prompt}]
246
208
 
247
- if infer_disposition:
248
- # Use structured output with Pydantic model for disposition inference
249
- try:
250
- parsed = await llm_config.call(
251
- messages=messages,
252
- response_format=BackgroundMergeResponse,
253
- scope="bank_background",
254
- temperature=0.3,
255
- max_completion_tokens=8192,
256
- )
257
- logger.info(f"Successfully got structured response: background={parsed.background[:100]}")
258
-
259
- # Convert Pydantic model to dict format
260
- return {"background": parsed.background, "disposition": parsed.disposition.model_dump()}
261
- except Exception as e:
262
- logger.warning(f"Structured output failed, falling back to manual parsing: {e}")
263
- # Fall through to manual parsing below
264
-
265
- # Manual parsing fallback or non-disposition merge
266
209
  content = await llm_config.call(
267
- messages=messages, scope="bank_background", temperature=0.3, max_completion_tokens=8192
210
+ messages=messages, scope="bank_mission", temperature=0.3, max_completion_tokens=8192
268
211
  )
269
212
 
270
- logger.info(f"LLM response for background merge (first 500 chars): {content[:500]}")
271
-
272
- if infer_disposition:
273
- # Parse JSON response - try multiple extraction methods
274
- result = None
275
-
276
- # Method 1: Direct parse
277
- try:
278
- result = json.loads(content)
279
- logger.info("Successfully parsed JSON directly")
280
- except json.JSONDecodeError:
281
- pass
282
-
283
- # Method 2: Extract from markdown code blocks
284
- if result is None:
285
- # Remove markdown code blocks
286
- code_block_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", content, re.DOTALL)
287
- if code_block_match:
288
- try:
289
- result = json.loads(code_block_match.group(1))
290
- logger.info("Successfully extracted JSON from markdown code block")
291
- except json.JSONDecodeError:
292
- pass
293
-
294
- # Method 3: Find nested JSON structure
295
- if result is None:
296
- # Look for JSON object with nested structure
297
- json_match = re.search(
298
- r'\{[^{}]*"background"[^{}]*"disposition"[^{}]*\{[^{}]*\}[^{}]*\}', content, re.DOTALL
299
- )
300
- if json_match:
301
- try:
302
- result = json.loads(json_match.group())
303
- logger.info("Successfully extracted JSON using nested pattern")
304
- except json.JSONDecodeError:
305
- pass
306
-
307
- # All parsing methods failed - use fallback
308
- if result is None:
309
- logger.warning(f"Failed to extract JSON from LLM response. Raw content: {content[:200]}")
310
- # Fallback: use new_info as background with default disposition
311
- return {
312
- "background": new_info if new_info else current if current else "",
313
- "disposition": DEFAULT_DISPOSITION.copy(),
314
- }
213
+ logger.info(f"LLM response for mission merge (first 500 chars): {content[:500]}")
315
214
 
316
- # Validate disposition values
317
- disposition = result.get("disposition", {})
318
- for key in ["skepticism", "literalism", "empathy"]:
319
- if key not in disposition:
320
- disposition[key] = 3 # Default to neutral
321
- else:
322
- # Clamp to [1, 5] and convert to int
323
- disposition[key] = max(1, min(5, int(disposition[key])))
324
-
325
- result["disposition"] = disposition
326
-
327
- # Ensure background exists
328
- if "background" not in result or not result["background"]:
329
- result["background"] = new_info if new_info else ""
330
-
331
- return result
332
- else:
333
- # Just background merge
334
- merged = content
335
- if not merged or merged.lower() in ["(empty)", "none", "n/a"]:
336
- merged = new_info if new_info else ""
337
- return {"background": merged}
215
+ merged = content.strip()
216
+ if not merged or merged.lower() in ["(empty)", "none", "n/a"]:
217
+ merged = new_info if new_info else ""
218
+ return {"mission": merged}
338
219
 
339
220
  except Exception as e:
340
- logger.error(f"Error merging background with LLM: {e}")
221
+ logger.error(f"Error merging mission with LLM: {e}")
341
222
  # Fallback: just append new info
342
223
  if current:
343
224
  merged = f"{current} {new_info}".strip()
344
225
  else:
345
226
  merged = new_info
346
227
 
347
- result = {"background": merged}
348
- if infer_disposition:
349
- result["disposition"] = DEFAULT_DISPOSITION.copy()
350
- return result
228
+ return {"mission": merged}
351
229
 
352
230
 
353
231
  async def list_banks(pool) -> list:
@@ -358,12 +236,12 @@ async def list_banks(pool) -> list:
358
236
  pool: Database connection pool
359
237
 
360
238
  Returns:
361
- List of dicts with bank_id, name, disposition, background, created_at, updated_at
239
+ List of dicts with bank_id, name, disposition, mission, created_at, updated_at
362
240
  """
363
241
  async with acquire_with_retry(pool) as conn:
364
242
  rows = await conn.fetch(
365
243
  f"""
366
- SELECT bank_id, name, disposition, background, created_at, updated_at
244
+ SELECT bank_id, name, disposition, mission, created_at, updated_at
367
245
  FROM {fq_table("banks")}
368
246
  ORDER BY updated_at DESC
369
247
  """
@@ -381,7 +259,7 @@ async def list_banks(pool) -> list:
381
259
  "bank_id": row["bank_id"],
382
260
  "name": row["name"],
383
261
  "disposition": disposition_data,
384
- "background": row["background"],
262
+ "mission": row["mission"] or "",
385
263
  "created_at": row["created_at"].isoformat() if row["created_at"] else None,
386
264
  "updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
387
265
  }