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.
- hindsight_api/admin/__init__.py +1 -0
- hindsight_api/admin/cli.py +311 -0
- hindsight_api/alembic/versions/f1a2b3c4d5e6_add_memory_links_composite_index.py +44 -0
- hindsight_api/alembic/versions/g2a3b4c5d6e7_add_tags_column.py +48 -0
- hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
- hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
- hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
- hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
- hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
- hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
- hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
- hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
- hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
- hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
- hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
- hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
- hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
- hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
- hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
- hindsight_api/api/http.py +1406 -118
- hindsight_api/api/mcp.py +11 -196
- hindsight_api/config.py +359 -27
- hindsight_api/engine/consolidation/__init__.py +5 -0
- hindsight_api/engine/consolidation/consolidator.py +859 -0
- hindsight_api/engine/consolidation/prompts.py +69 -0
- hindsight_api/engine/cross_encoder.py +706 -88
- hindsight_api/engine/db_budget.py +284 -0
- hindsight_api/engine/db_utils.py +11 -0
- hindsight_api/engine/directives/__init__.py +5 -0
- hindsight_api/engine/directives/models.py +37 -0
- hindsight_api/engine/embeddings.py +553 -29
- hindsight_api/engine/entity_resolver.py +8 -5
- hindsight_api/engine/interface.py +40 -17
- hindsight_api/engine/llm_wrapper.py +744 -68
- hindsight_api/engine/memory_engine.py +2505 -1017
- hindsight_api/engine/mental_models/__init__.py +14 -0
- hindsight_api/engine/mental_models/models.py +53 -0
- hindsight_api/engine/query_analyzer.py +4 -3
- hindsight_api/engine/reflect/__init__.py +18 -0
- hindsight_api/engine/reflect/agent.py +933 -0
- hindsight_api/engine/reflect/models.py +109 -0
- hindsight_api/engine/reflect/observations.py +186 -0
- hindsight_api/engine/reflect/prompts.py +483 -0
- hindsight_api/engine/reflect/tools.py +437 -0
- hindsight_api/engine/reflect/tools_schema.py +250 -0
- hindsight_api/engine/response_models.py +168 -4
- hindsight_api/engine/retain/bank_utils.py +79 -201
- hindsight_api/engine/retain/fact_extraction.py +424 -195
- hindsight_api/engine/retain/fact_storage.py +35 -12
- hindsight_api/engine/retain/link_utils.py +29 -24
- hindsight_api/engine/retain/orchestrator.py +24 -43
- hindsight_api/engine/retain/types.py +11 -2
- hindsight_api/engine/search/graph_retrieval.py +43 -14
- hindsight_api/engine/search/link_expansion_retrieval.py +391 -0
- hindsight_api/engine/search/mpfp_retrieval.py +362 -117
- hindsight_api/engine/search/reranking.py +2 -2
- hindsight_api/engine/search/retrieval.py +848 -201
- hindsight_api/engine/search/tags.py +172 -0
- hindsight_api/engine/search/think_utils.py +42 -141
- hindsight_api/engine/search/trace.py +12 -1
- hindsight_api/engine/search/tracer.py +26 -6
- hindsight_api/engine/search/types.py +21 -3
- hindsight_api/engine/task_backend.py +113 -106
- hindsight_api/engine/utils.py +1 -152
- hindsight_api/extensions/__init__.py +10 -1
- hindsight_api/extensions/builtin/tenant.py +5 -1
- hindsight_api/extensions/context.py +10 -1
- hindsight_api/extensions/operation_validator.py +81 -4
- hindsight_api/extensions/tenant.py +26 -0
- hindsight_api/main.py +69 -6
- hindsight_api/mcp_local.py +12 -53
- hindsight_api/mcp_tools.py +494 -0
- hindsight_api/metrics.py +433 -48
- hindsight_api/migrations.py +141 -1
- hindsight_api/models.py +3 -3
- hindsight_api/pg0.py +53 -0
- hindsight_api/server.py +39 -2
- hindsight_api/worker/__init__.py +11 -0
- hindsight_api/worker/main.py +296 -0
- hindsight_api/worker/poller.py +486 -0
- {hindsight_api-0.2.1.dist-info → hindsight_api-0.4.0.dist-info}/METADATA +16 -6
- hindsight_api-0.4.0.dist-info/RECORD +112 -0
- {hindsight_api-0.2.1.dist-info → hindsight_api-0.4.0.dist-info}/entry_points.txt +2 -0
- hindsight_api/engine/retain/observation_regeneration.py +0 -254
- hindsight_api/engine/search/observation_utils.py +0 -125
- hindsight_api/engine/search/scoring.py +0 -159
- hindsight_api-0.2.1.dist-info/RECORD +0 -75
- {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 '
|
|
14
|
-
VALID_RECALL_FACT_TYPES = frozenset(["world", "experience", "
|
|
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,
|
|
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
|
|
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
|
-
|
|
30
|
+
mission: str
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class
|
|
34
|
-
"""LLM response for
|
|
33
|
+
class MissionMergeResponse(BaseModel):
|
|
34
|
+
"""LLM response for mission merge."""
|
|
35
35
|
|
|
36
|
-
|
|
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 +
|
|
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
|
|
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,
|
|
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"],
|
|
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,
|
|
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),
|
|
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
|
|
114
|
+
async def set_bank_mission(pool, bank_id: str, mission: str) -> None:
|
|
114
115
|
"""
|
|
115
|
-
|
|
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
|
|
146
|
+
llm_config: LLM configuration for mission merging
|
|
122
147
|
bank_id: bank IDentifier
|
|
123
|
-
new_info: New
|
|
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 '
|
|
151
|
+
Dict with 'mission' (str) key
|
|
128
152
|
"""
|
|
129
153
|
# Get current profile
|
|
130
154
|
profile = await get_bank_profile(pool, bank_id)
|
|
131
|
-
|
|
155
|
+
current_mission = profile["mission"]
|
|
132
156
|
|
|
133
|
-
# Use LLM to merge
|
|
134
|
-
result = await
|
|
157
|
+
# Use LLM to merge missions
|
|
158
|
+
result = await _llm_merge_mission(llm_config, current_mission, new_info)
|
|
135
159
|
|
|
136
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
175
|
+
return {"mission": merged_mission}
|
|
173
176
|
|
|
174
177
|
|
|
175
|
-
async def
|
|
178
|
+
async def _llm_merge_mission(llm_config, current: str, new_info: str) -> dict:
|
|
176
179
|
"""
|
|
177
|
-
Use LLM to intelligently merge
|
|
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
|
|
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 '
|
|
188
|
+
Dict with 'mission' (str) key
|
|
188
189
|
"""
|
|
189
|
-
|
|
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
|
|
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
|
|
198
|
-
2. If there are conflicts
|
|
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
|
|
202
|
+
6. Return ONLY the merged mission text, no explanations
|
|
240
203
|
|
|
241
|
-
Merged
|
|
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="
|
|
210
|
+
messages=messages, scope="bank_mission", temperature=0.3, max_completion_tokens=8192
|
|
268
211
|
)
|
|
269
212
|
|
|
270
|
-
logger.info(f"LLM response for
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
"
|
|
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
|
}
|