hindsight-api 0.0.18__py3-none-any.whl → 0.0.20__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/api/__init__.py +2 -2
- hindsight_api/api/http.py +60 -60
- hindsight_api/api/mcp.py +1 -1
- hindsight_api/engine/llm_wrapper.py +140 -5
- hindsight_api/engine/memory_engine.py +33 -31
- hindsight_api/engine/response_models.py +6 -6
- hindsight_api/engine/retain/bank_utils.py +66 -66
- hindsight_api/engine/retain/fact_extraction.py +8 -8
- hindsight_api/engine/retain/fact_storage.py +1 -1
- hindsight_api/engine/retain/link_utils.py +112 -43
- hindsight_api/engine/retain/types.py +1 -1
- hindsight_api/engine/search/think_utils.py +20 -20
- hindsight_api/engine/search/trace.py +1 -1
- hindsight_api/models.py +3 -3
- {hindsight_api-0.0.18.dist-info → hindsight_api-0.0.20.dist-info}/METADATA +2 -1
- {hindsight_api-0.0.18.dist-info → hindsight_api-0.0.20.dist-info}/RECORD +18 -18
- {hindsight_api-0.0.18.dist-info → hindsight_api-0.0.20.dist-info}/WHEEL +0 -0
- {hindsight_api-0.0.18.dist-info → hindsight_api-0.0.20.dist-info}/entry_points.txt +0 -0
|
@@ -94,7 +94,7 @@ class MemoryEngine:
|
|
|
94
94
|
- Embedding generation for semantic search
|
|
95
95
|
- Entity, temporal, and semantic link creation
|
|
96
96
|
- Think operations for formulating answers with opinions
|
|
97
|
-
- bank profile and
|
|
97
|
+
- bank profile and disposition management
|
|
98
98
|
"""
|
|
99
99
|
|
|
100
100
|
def __init__(
|
|
@@ -676,7 +676,7 @@ class MemoryEngine:
|
|
|
676
676
|
context: Context about when/why this memory was formed
|
|
677
677
|
event_date: When the event occurred (defaults to now)
|
|
678
678
|
document_id: Optional document ID for tracking (always upserts if document already exists)
|
|
679
|
-
fact_type_override: Override fact type ('world', '
|
|
679
|
+
fact_type_override: Override fact type ('world', 'experience', 'opinion')
|
|
680
680
|
confidence_score: Confidence score for opinions (0.0 to 1.0)
|
|
681
681
|
|
|
682
682
|
Returns:
|
|
@@ -728,7 +728,7 @@ class MemoryEngine:
|
|
|
728
728
|
- "document_id" (optional): Document ID for this specific content item
|
|
729
729
|
document_id: **DEPRECATED** - Use "document_id" key in each content dict instead.
|
|
730
730
|
Applies the same document_id to ALL content items that don't specify their own.
|
|
731
|
-
fact_type_override: Override fact type for all facts ('world', '
|
|
731
|
+
fact_type_override: Override fact type for all facts ('world', 'experience', 'opinion')
|
|
732
732
|
confidence_score: Confidence score for opinions (0.0 to 1.0)
|
|
733
733
|
|
|
734
734
|
Returns:
|
|
@@ -896,7 +896,7 @@ class MemoryEngine:
|
|
|
896
896
|
Args:
|
|
897
897
|
bank_id: bank ID to recall for
|
|
898
898
|
query: Recall query
|
|
899
|
-
fact_type: Required filter for fact type ('world', '
|
|
899
|
+
fact_type: Required filter for fact type ('world', 'experience', or 'opinion')
|
|
900
900
|
budget: Budget level for graph traversal (low=100, mid=300, high=600 units)
|
|
901
901
|
max_tokens: Maximum tokens to return (counts only 'text' field, default 4096)
|
|
902
902
|
enable_trace: If True, returns detailed trace object
|
|
@@ -936,7 +936,7 @@ class MemoryEngine:
|
|
|
936
936
|
Args:
|
|
937
937
|
bank_id: bank ID to recall for
|
|
938
938
|
query: Recall query
|
|
939
|
-
fact_type: List of fact types to recall (e.g., ['world', '
|
|
939
|
+
fact_type: List of fact types to recall (e.g., ['world', 'experience'])
|
|
940
940
|
budget: Budget level for graph traversal (low=100, mid=300, high=600 units)
|
|
941
941
|
max_tokens: Maximum tokens to return (counts only 'text' field, default 4096)
|
|
942
942
|
Results are returned until token budget is reached, stopping before
|
|
@@ -1682,13 +1682,15 @@ class MemoryEngine:
|
|
|
1682
1682
|
|
|
1683
1683
|
Args:
|
|
1684
1684
|
bank_id: bank ID to delete
|
|
1685
|
-
fact_type: Optional fact type filter (world,
|
|
1685
|
+
fact_type: Optional fact type filter (world, experience, opinion). If provided, only deletes memories of that type.
|
|
1686
1686
|
|
|
1687
1687
|
Returns:
|
|
1688
1688
|
Dictionary with counts of deleted items
|
|
1689
1689
|
"""
|
|
1690
1690
|
pool = await self._get_pool()
|
|
1691
1691
|
async with acquire_with_retry(pool) as conn:
|
|
1692
|
+
# Ensure connection is not in read-only mode (can happen with connection poolers)
|
|
1693
|
+
await conn.execute("SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE")
|
|
1692
1694
|
async with conn.transaction():
|
|
1693
1695
|
try:
|
|
1694
1696
|
if fact_type:
|
|
@@ -1738,7 +1740,7 @@ class MemoryEngine:
|
|
|
1738
1740
|
|
|
1739
1741
|
Args:
|
|
1740
1742
|
bank_id: Filter by bank ID
|
|
1741
|
-
fact_type: Filter by fact type (world,
|
|
1743
|
+
fact_type: Filter by fact type (world, experience, opinion)
|
|
1742
1744
|
|
|
1743
1745
|
Returns:
|
|
1744
1746
|
Dict with nodes, edges, and table_rows
|
|
@@ -1912,7 +1914,7 @@ class MemoryEngine:
|
|
|
1912
1914
|
|
|
1913
1915
|
Args:
|
|
1914
1916
|
bank_id: Filter by bank ID
|
|
1915
|
-
fact_type: Filter by fact type (world,
|
|
1917
|
+
fact_type: Filter by fact type (world, experience, opinion)
|
|
1916
1918
|
search_query: Full-text search query (searches text and context fields)
|
|
1917
1919
|
limit: Maximum number of results to return
|
|
1918
1920
|
offset: Offset for pagination
|
|
@@ -2485,55 +2487,55 @@ Guidelines:
|
|
|
2485
2487
|
|
|
2486
2488
|
async def get_bank_profile(self, bank_id: str) -> "bank_utils.BankProfile":
|
|
2487
2489
|
"""
|
|
2488
|
-
Get bank profile (name,
|
|
2490
|
+
Get bank profile (name, disposition + background).
|
|
2489
2491
|
Auto-creates agent with default values if not exists.
|
|
2490
2492
|
|
|
2491
2493
|
Args:
|
|
2492
2494
|
bank_id: bank IDentifier
|
|
2493
2495
|
|
|
2494
2496
|
Returns:
|
|
2495
|
-
BankProfile with name, typed
|
|
2497
|
+
BankProfile with name, typed DispositionTraits, and background
|
|
2496
2498
|
"""
|
|
2497
2499
|
pool = await self._get_pool()
|
|
2498
2500
|
return await bank_utils.get_bank_profile(pool, bank_id)
|
|
2499
2501
|
|
|
2500
|
-
async def
|
|
2502
|
+
async def update_bank_disposition(
|
|
2501
2503
|
self,
|
|
2502
2504
|
bank_id: str,
|
|
2503
|
-
|
|
2505
|
+
disposition: Dict[str, float]
|
|
2504
2506
|
) -> None:
|
|
2505
2507
|
"""
|
|
2506
|
-
Update bank
|
|
2508
|
+
Update bank disposition traits.
|
|
2507
2509
|
|
|
2508
2510
|
Args:
|
|
2509
2511
|
bank_id: bank IDentifier
|
|
2510
|
-
|
|
2512
|
+
disposition: Dict with Big Five traits + bias_strength (all 0-1)
|
|
2511
2513
|
"""
|
|
2512
2514
|
pool = await self._get_pool()
|
|
2513
|
-
await bank_utils.
|
|
2515
|
+
await bank_utils.update_bank_disposition(pool, bank_id, disposition)
|
|
2514
2516
|
|
|
2515
2517
|
async def merge_bank_background(
|
|
2516
2518
|
self,
|
|
2517
2519
|
bank_id: str,
|
|
2518
2520
|
new_info: str,
|
|
2519
|
-
|
|
2521
|
+
update_disposition: bool = True
|
|
2520
2522
|
) -> dict:
|
|
2521
2523
|
"""
|
|
2522
2524
|
Merge new background information with existing background using LLM.
|
|
2523
2525
|
Normalizes to first person ("I") and resolves conflicts.
|
|
2524
|
-
Optionally infers
|
|
2526
|
+
Optionally infers disposition traits from the merged background.
|
|
2525
2527
|
|
|
2526
2528
|
Args:
|
|
2527
2529
|
bank_id: bank IDentifier
|
|
2528
2530
|
new_info: New background information to add/merge
|
|
2529
|
-
|
|
2531
|
+
update_disposition: If True, infer Big Five traits from background (default: True)
|
|
2530
2532
|
|
|
2531
2533
|
Returns:
|
|
2532
|
-
Dict with 'background' (str) and optionally '
|
|
2534
|
+
Dict with 'background' (str) and optionally 'disposition' (dict) keys
|
|
2533
2535
|
"""
|
|
2534
2536
|
pool = await self._get_pool()
|
|
2535
2537
|
return await bank_utils.merge_bank_background(
|
|
2536
|
-
pool, self._llm_config, bank_id, new_info,
|
|
2538
|
+
pool, self._llm_config, bank_id, new_info, update_disposition
|
|
2537
2539
|
)
|
|
2538
2540
|
|
|
2539
2541
|
async def list_banks(self) -> list:
|
|
@@ -2541,7 +2543,7 @@ Guidelines:
|
|
|
2541
2543
|
List all agents in the system.
|
|
2542
2544
|
|
|
2543
2545
|
Returns:
|
|
2544
|
-
List of dicts with bank_id, name,
|
|
2546
|
+
List of dicts with bank_id, name, disposition, background, created_at, updated_at
|
|
2545
2547
|
"""
|
|
2546
2548
|
pool = await self._get_pool()
|
|
2547
2549
|
return await bank_utils.list_banks(pool)
|
|
@@ -2559,7 +2561,7 @@ Guidelines:
|
|
|
2559
2561
|
Reflect and formulate an answer using bank identity, world facts, and opinions.
|
|
2560
2562
|
|
|
2561
2563
|
This method:
|
|
2562
|
-
1. Retrieves
|
|
2564
|
+
1. Retrieves experience (conversations and events)
|
|
2563
2565
|
2. Retrieves world facts (general knowledge)
|
|
2564
2566
|
3. Retrieves existing opinions (bank's formed perspectives)
|
|
2565
2567
|
4. Uses LLM to formulate an answer
|
|
@@ -2575,7 +2577,7 @@ Guidelines:
|
|
|
2575
2577
|
Returns:
|
|
2576
2578
|
ReflectResult containing:
|
|
2577
2579
|
- text: Plain text answer (no markdown)
|
|
2578
|
-
- based_on: Dict with 'world', '
|
|
2580
|
+
- based_on: Dict with 'world', 'experience', and 'opinion' fact lists (MemoryFact objects)
|
|
2579
2581
|
- new_opinions: List of newly formed opinions
|
|
2580
2582
|
"""
|
|
2581
2583
|
# Use cached LLM config
|
|
@@ -2589,7 +2591,7 @@ Guidelines:
|
|
|
2589
2591
|
budget=budget,
|
|
2590
2592
|
max_tokens=4096,
|
|
2591
2593
|
enable_trace=False,
|
|
2592
|
-
fact_type=['
|
|
2594
|
+
fact_type=['experience', 'world', 'opinion'],
|
|
2593
2595
|
include_entities=True
|
|
2594
2596
|
)
|
|
2595
2597
|
|
|
@@ -2597,7 +2599,7 @@ Guidelines:
|
|
|
2597
2599
|
logger.info(f"[THINK] Search returned {len(all_results)} results")
|
|
2598
2600
|
|
|
2599
2601
|
# Split results by fact type for structured response
|
|
2600
|
-
agent_results = [r for r in all_results if r.fact_type == '
|
|
2602
|
+
agent_results = [r for r in all_results if r.fact_type == 'experience']
|
|
2601
2603
|
world_results = [r for r in all_results if r.fact_type == 'world']
|
|
2602
2604
|
opinion_results = [r for r in all_results if r.fact_type == 'opinion']
|
|
2603
2605
|
|
|
@@ -2610,10 +2612,10 @@ Guidelines:
|
|
|
2610
2612
|
|
|
2611
2613
|
logger.info(f"[THINK] Formatted facts - agent: {len(agent_facts_text)} chars, world: {len(world_facts_text)} chars, opinion: {len(opinion_facts_text)} chars")
|
|
2612
2614
|
|
|
2613
|
-
# Get bank profile (name,
|
|
2615
|
+
# Get bank profile (name, disposition + background)
|
|
2614
2616
|
profile = await self.get_bank_profile(bank_id)
|
|
2615
2617
|
name = profile["name"]
|
|
2616
|
-
|
|
2618
|
+
disposition = profile["disposition"] # Typed as DispositionTraits
|
|
2617
2619
|
background = profile["background"]
|
|
2618
2620
|
|
|
2619
2621
|
# Build the prompt
|
|
@@ -2623,14 +2625,14 @@ Guidelines:
|
|
|
2623
2625
|
opinion_facts_text=opinion_facts_text,
|
|
2624
2626
|
query=query,
|
|
2625
2627
|
name=name,
|
|
2626
|
-
|
|
2628
|
+
disposition=disposition,
|
|
2627
2629
|
background=background,
|
|
2628
2630
|
context=context,
|
|
2629
2631
|
)
|
|
2630
2632
|
|
|
2631
2633
|
logger.info(f"[THINK] Full prompt length: {len(prompt)} chars")
|
|
2632
2634
|
|
|
2633
|
-
system_message = think_utils.get_system_message(
|
|
2635
|
+
system_message = think_utils.get_system_message(disposition)
|
|
2634
2636
|
|
|
2635
2637
|
answer_text = await self._llm_config.call(
|
|
2636
2638
|
messages=[
|
|
@@ -2657,7 +2659,7 @@ Guidelines:
|
|
|
2657
2659
|
text=answer_text,
|
|
2658
2660
|
based_on={
|
|
2659
2661
|
"world": world_results,
|
|
2660
|
-
"
|
|
2662
|
+
"experience": agent_results,
|
|
2661
2663
|
"opinion": opinion_results
|
|
2662
2664
|
},
|
|
2663
2665
|
new_opinions=[] # Opinions are being extracted asynchronously
|
|
@@ -2871,7 +2873,7 @@ Guidelines:
|
|
|
2871
2873
|
JOIN unit_entities ue ON mu.id = ue.unit_id
|
|
2872
2874
|
WHERE mu.bank_id = $1
|
|
2873
2875
|
AND ue.entity_id = $2
|
|
2874
|
-
AND mu.fact_type IN ('world', '
|
|
2876
|
+
AND mu.fact_type IN ('world', 'experience')
|
|
2875
2877
|
ORDER BY mu.occurred_start DESC
|
|
2876
2878
|
LIMIT 50
|
|
2877
2879
|
""",
|
|
@@ -10,9 +10,9 @@ from typing import Optional, List, Dict, Any
|
|
|
10
10
|
from pydantic import BaseModel, Field, ConfigDict
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class DispositionTraits(BaseModel):
|
|
14
14
|
"""
|
|
15
|
-
|
|
15
|
+
Disposition traits for a bank using the Big Five model.
|
|
16
16
|
|
|
17
17
|
All traits are scored 0.0-1.0 where higher values indicate stronger presence of the trait.
|
|
18
18
|
"""
|
|
@@ -21,7 +21,7 @@ class PersonalityTraits(BaseModel):
|
|
|
21
21
|
extraversion: float = Field(description="Extraversion and sociability (0.0-1.0)")
|
|
22
22
|
agreeableness: float = Field(description="Agreeableness and cooperation (0.0-1.0)")
|
|
23
23
|
neuroticism: float = Field(description="Emotional sensitivity and neuroticism (0.0-1.0)")
|
|
24
|
-
bias_strength: float = Field(description="How strongly
|
|
24
|
+
bias_strength: float = Field(description="How strongly disposition influences thinking (0.0-1.0)")
|
|
25
25
|
|
|
26
26
|
model_config = ConfigDict(json_schema_extra={
|
|
27
27
|
"example": {
|
|
@@ -61,7 +61,7 @@ class MemoryFact(BaseModel):
|
|
|
61
61
|
|
|
62
62
|
id: str = Field(description="Unique identifier for the memory fact")
|
|
63
63
|
text: str = Field(description="The actual text content of the memory")
|
|
64
|
-
fact_type: str = Field(description="Type of fact: 'world', '
|
|
64
|
+
fact_type: str = Field(description="Type of fact: 'world', 'experience', 'opinion', or 'observation'")
|
|
65
65
|
entities: Optional[List[str]] = Field(None, description="Entity names mentioned in this fact")
|
|
66
66
|
context: Optional[str] = Field(None, description="Additional context for the memory")
|
|
67
67
|
occurred_start: Optional[str] = Field(None, description="ISO format date when the event started occurring")
|
|
@@ -142,7 +142,7 @@ class ReflectResult(BaseModel):
|
|
|
142
142
|
"occurred_end": "2024-01-15T10:30:00Z"
|
|
143
143
|
}
|
|
144
144
|
],
|
|
145
|
-
"
|
|
145
|
+
"experience": [],
|
|
146
146
|
"opinion": []
|
|
147
147
|
},
|
|
148
148
|
"new_opinions": [
|
|
@@ -153,7 +153,7 @@ class ReflectResult(BaseModel):
|
|
|
153
153
|
|
|
154
154
|
text: str = Field(description="The formulated answer text")
|
|
155
155
|
based_on: Dict[str, List[MemoryFact]] = Field(
|
|
156
|
-
description="Facts used to formulate the answer, organized by type (world,
|
|
156
|
+
description="Facts used to formulate the answer, organized by type (world, experience, opinion)"
|
|
157
157
|
)
|
|
158
158
|
new_opinions: List[str] = Field(
|
|
159
159
|
default_factory=list,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
bank profile utilities for
|
|
2
|
+
bank profile utilities for disposition and background management.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import json
|
|
@@ -8,11 +8,11 @@ import re
|
|
|
8
8
|
from typing import Dict, Optional, TypedDict
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
from ..db_utils import acquire_with_retry
|
|
11
|
-
from ..response_models import
|
|
11
|
+
from ..response_models import DispositionTraits
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
DEFAULT_DISPOSITION = {
|
|
16
16
|
"openness": 0.5,
|
|
17
17
|
"conscientiousness": 0.5,
|
|
18
18
|
"extraversion": 0.5,
|
|
@@ -25,19 +25,19 @@ DEFAULT_PERSONALITY = {
|
|
|
25
25
|
class BankProfile(TypedDict):
|
|
26
26
|
"""Type for bank profile data."""
|
|
27
27
|
name: str
|
|
28
|
-
|
|
28
|
+
disposition: DispositionTraits
|
|
29
29
|
background: str
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class BackgroundMergeResponse(BaseModel):
|
|
33
|
-
"""LLM response for background merge with
|
|
33
|
+
"""LLM response for background merge with disposition inference."""
|
|
34
34
|
background: str = Field(description="Merged background in first person perspective")
|
|
35
|
-
|
|
35
|
+
disposition: DispositionTraits = Field(description="Inferred Big Five disposition traits")
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
async def get_bank_profile(pool, bank_id: str) -> BankProfile:
|
|
39
39
|
"""
|
|
40
|
-
Get bank profile (name,
|
|
40
|
+
Get bank profile (name, disposition + background).
|
|
41
41
|
Auto-creates bank with default values if not exists.
|
|
42
42
|
|
|
43
43
|
Args:
|
|
@@ -45,13 +45,13 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
|
|
|
45
45
|
bank_id: bank IDentifier
|
|
46
46
|
|
|
47
47
|
Returns:
|
|
48
|
-
BankProfile with name, typed
|
|
48
|
+
BankProfile with name, typed DispositionTraits, and background
|
|
49
49
|
"""
|
|
50
50
|
async with acquire_with_retry(pool) as conn:
|
|
51
51
|
# Try to get existing bank
|
|
52
52
|
row = await conn.fetchrow(
|
|
53
53
|
"""
|
|
54
|
-
SELECT name,
|
|
54
|
+
SELECT name, disposition, background
|
|
55
55
|
FROM banks WHERE bank_id = $1
|
|
56
56
|
""",
|
|
57
57
|
bank_id
|
|
@@ -59,48 +59,48 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
|
|
|
59
59
|
|
|
60
60
|
if row:
|
|
61
61
|
# asyncpg returns JSONB as a string, so parse it
|
|
62
|
-
|
|
63
|
-
if isinstance(
|
|
64
|
-
|
|
62
|
+
disposition_data = row["disposition"]
|
|
63
|
+
if isinstance(disposition_data, str):
|
|
64
|
+
disposition_data = json.loads(disposition_data)
|
|
65
65
|
|
|
66
66
|
return BankProfile(
|
|
67
67
|
name=row["name"],
|
|
68
|
-
|
|
68
|
+
disposition=DispositionTraits(**disposition_data),
|
|
69
69
|
background=row["background"]
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
# Bank doesn't exist, create with defaults
|
|
73
73
|
await conn.execute(
|
|
74
74
|
"""
|
|
75
|
-
INSERT INTO banks (bank_id, name,
|
|
75
|
+
INSERT INTO banks (bank_id, name, disposition, background)
|
|
76
76
|
VALUES ($1, $2, $3::jsonb, $4)
|
|
77
77
|
ON CONFLICT (bank_id) DO NOTHING
|
|
78
78
|
""",
|
|
79
79
|
bank_id,
|
|
80
80
|
bank_id, # Default name is the bank_id
|
|
81
|
-
json.dumps(
|
|
81
|
+
json.dumps(DEFAULT_DISPOSITION),
|
|
82
82
|
""
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
return BankProfile(
|
|
86
86
|
name=bank_id,
|
|
87
|
-
|
|
87
|
+
disposition=DispositionTraits(**DEFAULT_DISPOSITION),
|
|
88
88
|
background=""
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
async def
|
|
92
|
+
async def update_bank_disposition(
|
|
93
93
|
pool,
|
|
94
94
|
bank_id: str,
|
|
95
|
-
|
|
95
|
+
disposition: Dict[str, float]
|
|
96
96
|
) -> None:
|
|
97
97
|
"""
|
|
98
|
-
Update bank
|
|
98
|
+
Update bank disposition traits.
|
|
99
99
|
|
|
100
100
|
Args:
|
|
101
101
|
pool: Database connection pool
|
|
102
102
|
bank_id: bank IDentifier
|
|
103
|
-
|
|
103
|
+
disposition: Dict with Big Five traits + bias_strength (all 0-1)
|
|
104
104
|
"""
|
|
105
105
|
# Ensure bank exists first
|
|
106
106
|
await get_bank_profile(pool, bank_id)
|
|
@@ -109,12 +109,12 @@ async def update_bank_personality(
|
|
|
109
109
|
await conn.execute(
|
|
110
110
|
"""
|
|
111
111
|
UPDATE banks
|
|
112
|
-
SET
|
|
112
|
+
SET disposition = $2::jsonb,
|
|
113
113
|
updated_at = NOW()
|
|
114
114
|
WHERE bank_id = $1
|
|
115
115
|
""",
|
|
116
116
|
bank_id,
|
|
117
|
-
json.dumps(
|
|
117
|
+
json.dumps(disposition)
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
|
|
@@ -123,53 +123,53 @@ async def merge_bank_background(
|
|
|
123
123
|
llm_config,
|
|
124
124
|
bank_id: str,
|
|
125
125
|
new_info: str,
|
|
126
|
-
|
|
126
|
+
update_disposition: bool = True
|
|
127
127
|
) -> dict:
|
|
128
128
|
"""
|
|
129
129
|
Merge new background information with existing background using LLM.
|
|
130
130
|
Normalizes to first person ("I") and resolves conflicts.
|
|
131
|
-
Optionally infers
|
|
131
|
+
Optionally infers disposition traits from the merged background.
|
|
132
132
|
|
|
133
133
|
Args:
|
|
134
134
|
pool: Database connection pool
|
|
135
135
|
llm_config: LLM configuration for background merging
|
|
136
136
|
bank_id: bank IDentifier
|
|
137
137
|
new_info: New background information to add/merge
|
|
138
|
-
|
|
138
|
+
update_disposition: If True, infer Big Five traits from background (default: True)
|
|
139
139
|
|
|
140
140
|
Returns:
|
|
141
|
-
Dict with 'background' (str) and optionally '
|
|
141
|
+
Dict with 'background' (str) and optionally 'disposition' (dict) keys
|
|
142
142
|
"""
|
|
143
143
|
# Get current profile
|
|
144
144
|
profile = await get_bank_profile(pool, bank_id)
|
|
145
145
|
current_background = profile["background"]
|
|
146
146
|
|
|
147
|
-
# Use LLM to merge backgrounds and optionally infer
|
|
147
|
+
# Use LLM to merge backgrounds and optionally infer disposition
|
|
148
148
|
result = await _llm_merge_background(
|
|
149
149
|
llm_config,
|
|
150
150
|
current_background,
|
|
151
151
|
new_info,
|
|
152
|
-
|
|
152
|
+
infer_disposition=update_disposition
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
merged_background = result["background"]
|
|
156
|
-
|
|
156
|
+
inferred_disposition = result.get("disposition")
|
|
157
157
|
|
|
158
158
|
# Update in database
|
|
159
159
|
async with acquire_with_retry(pool) as conn:
|
|
160
|
-
if
|
|
161
|
-
# Update both background and
|
|
160
|
+
if inferred_disposition:
|
|
161
|
+
# Update both background and disposition
|
|
162
162
|
await conn.execute(
|
|
163
163
|
"""
|
|
164
164
|
UPDATE banks
|
|
165
165
|
SET background = $2,
|
|
166
|
-
|
|
166
|
+
disposition = $3::jsonb,
|
|
167
167
|
updated_at = NOW()
|
|
168
168
|
WHERE bank_id = $1
|
|
169
169
|
""",
|
|
170
170
|
bank_id,
|
|
171
171
|
merged_background,
|
|
172
|
-
json.dumps(
|
|
172
|
+
json.dumps(inferred_disposition)
|
|
173
173
|
)
|
|
174
174
|
else:
|
|
175
175
|
# Update only background
|
|
@@ -185,8 +185,8 @@ async def merge_bank_background(
|
|
|
185
185
|
)
|
|
186
186
|
|
|
187
187
|
response = {"background": merged_background}
|
|
188
|
-
if
|
|
189
|
-
response["
|
|
188
|
+
if inferred_disposition:
|
|
189
|
+
response["disposition"] = inferred_disposition
|
|
190
190
|
|
|
191
191
|
return response
|
|
192
192
|
|
|
@@ -195,23 +195,23 @@ async def _llm_merge_background(
|
|
|
195
195
|
llm_config,
|
|
196
196
|
current: str,
|
|
197
197
|
new_info: str,
|
|
198
|
-
|
|
198
|
+
infer_disposition: bool = False
|
|
199
199
|
) -> dict:
|
|
200
200
|
"""
|
|
201
201
|
Use LLM to intelligently merge background information.
|
|
202
|
-
Optionally infer Big Five
|
|
202
|
+
Optionally infer Big Five disposition traits from the merged background.
|
|
203
203
|
|
|
204
204
|
Args:
|
|
205
205
|
llm_config: LLM configuration to use
|
|
206
206
|
current: Current background text
|
|
207
207
|
new_info: New information to merge
|
|
208
|
-
|
|
208
|
+
infer_disposition: If True, also infer disposition traits
|
|
209
209
|
|
|
210
210
|
Returns:
|
|
211
|
-
Dict with 'background' (str) and optionally '
|
|
211
|
+
Dict with 'background' (str) and optionally 'disposition' (dict) keys
|
|
212
212
|
"""
|
|
213
|
-
if
|
|
214
|
-
prompt = f"""You are helping maintain a memory bank's background/profile and infer their
|
|
213
|
+
if infer_disposition:
|
|
214
|
+
prompt = f"""You are helping maintain a memory bank's background/profile and infer their disposition. You MUST respond with ONLY valid JSON.
|
|
215
215
|
|
|
216
216
|
Current background: {current if current else "(empty)"}
|
|
217
217
|
|
|
@@ -223,20 +223,20 @@ Instructions:
|
|
|
223
223
|
3. Keep additions that don't conflict
|
|
224
224
|
4. Output in FIRST PERSON ("I") perspective
|
|
225
225
|
5. Be concise - keep merged background under 500 characters
|
|
226
|
-
6. Infer Big Five
|
|
226
|
+
6. Infer Big Five disposition traits from the merged background:
|
|
227
227
|
- Openness: 0.0-1.0 (creativity, curiosity, openness to new ideas)
|
|
228
228
|
- Conscientiousness: 0.0-1.0 (organization, discipline, goal-directed)
|
|
229
229
|
- Extraversion: 0.0-1.0 (sociability, assertiveness, energy from others)
|
|
230
230
|
- Agreeableness: 0.0-1.0 (cooperation, empathy, consideration)
|
|
231
231
|
- Neuroticism: 0.0-1.0 (emotional sensitivity, anxiety, stress response)
|
|
232
|
-
- Bias Strength: 0.0-1.0 (how much
|
|
232
|
+
- Bias Strength: 0.0-1.0 (how much disposition influences opinions)
|
|
233
233
|
|
|
234
234
|
CRITICAL: You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations. Just the JSON.
|
|
235
235
|
|
|
236
236
|
Format:
|
|
237
237
|
{{
|
|
238
238
|
"background": "the merged background text in first person",
|
|
239
|
-
"
|
|
239
|
+
"disposition": {{
|
|
240
240
|
"openness": 0.7,
|
|
241
241
|
"conscientiousness": 0.6,
|
|
242
242
|
"extraversion": 0.5,
|
|
@@ -274,8 +274,8 @@ Merged background:"""
|
|
|
274
274
|
# Prepare messages
|
|
275
275
|
messages = [{"role": "user", "content": prompt}]
|
|
276
276
|
|
|
277
|
-
if
|
|
278
|
-
# Use structured output with Pydantic model for
|
|
277
|
+
if infer_disposition:
|
|
278
|
+
# Use structured output with Pydantic model for disposition inference
|
|
279
279
|
try:
|
|
280
280
|
parsed = await llm_config.call(
|
|
281
281
|
messages=messages,
|
|
@@ -289,13 +289,13 @@ Merged background:"""
|
|
|
289
289
|
# Convert Pydantic model to dict format
|
|
290
290
|
return {
|
|
291
291
|
"background": parsed.background,
|
|
292
|
-
"
|
|
292
|
+
"disposition": parsed.disposition.model_dump()
|
|
293
293
|
}
|
|
294
294
|
except Exception as e:
|
|
295
295
|
logger.warning(f"Structured output failed, falling back to manual parsing: {e}")
|
|
296
296
|
# Fall through to manual parsing below
|
|
297
297
|
|
|
298
|
-
# Manual parsing fallback or non-
|
|
298
|
+
# Manual parsing fallback or non-disposition merge
|
|
299
299
|
content = await llm_config.call(
|
|
300
300
|
messages=messages,
|
|
301
301
|
scope="bank_background",
|
|
@@ -305,7 +305,7 @@ Merged background:"""
|
|
|
305
305
|
|
|
306
306
|
logger.info(f"LLM response for background merge (first 500 chars): {content[:500]}")
|
|
307
307
|
|
|
308
|
-
if
|
|
308
|
+
if infer_disposition:
|
|
309
309
|
# Parse JSON response - try multiple extraction methods
|
|
310
310
|
result = None
|
|
311
311
|
|
|
@@ -330,7 +330,7 @@ Merged background:"""
|
|
|
330
330
|
# Method 3: Find nested JSON structure
|
|
331
331
|
if result is None:
|
|
332
332
|
# Look for JSON object with nested structure
|
|
333
|
-
json_match = re.search(r'\{[^{}]*"background"[^{}]*"
|
|
333
|
+
json_match = re.search(r'\{[^{}]*"background"[^{}]*"disposition"[^{}]*\{[^{}]*\}[^{}]*\}', content, re.DOTALL)
|
|
334
334
|
if json_match:
|
|
335
335
|
try:
|
|
336
336
|
result = json.loads(json_match.group())
|
|
@@ -341,23 +341,23 @@ Merged background:"""
|
|
|
341
341
|
# All parsing methods failed - use fallback
|
|
342
342
|
if result is None:
|
|
343
343
|
logger.warning(f"Failed to extract JSON from LLM response. Raw content: {content[:200]}")
|
|
344
|
-
# Fallback: use new_info as background with default
|
|
344
|
+
# Fallback: use new_info as background with default disposition
|
|
345
345
|
return {
|
|
346
346
|
"background": new_info if new_info else current if current else "",
|
|
347
|
-
"
|
|
347
|
+
"disposition": DEFAULT_DISPOSITION.copy()
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
# Validate
|
|
351
|
-
|
|
350
|
+
# Validate disposition values
|
|
351
|
+
disposition = result.get("disposition", {})
|
|
352
352
|
for key in ["openness", "conscientiousness", "extraversion",
|
|
353
353
|
"agreeableness", "neuroticism", "bias_strength"]:
|
|
354
|
-
if key not in
|
|
355
|
-
|
|
354
|
+
if key not in disposition:
|
|
355
|
+
disposition[key] = 0.5 # Default to neutral
|
|
356
356
|
else:
|
|
357
357
|
# Clamp to [0, 1]
|
|
358
|
-
|
|
358
|
+
disposition[key] = max(0.0, min(1.0, float(disposition[key])))
|
|
359
359
|
|
|
360
|
-
result["
|
|
360
|
+
result["disposition"] = disposition
|
|
361
361
|
|
|
362
362
|
# Ensure background exists
|
|
363
363
|
if "background" not in result or not result["background"]:
|
|
@@ -380,8 +380,8 @@ Merged background:"""
|
|
|
380
380
|
merged = new_info
|
|
381
381
|
|
|
382
382
|
result = {"background": merged}
|
|
383
|
-
if
|
|
384
|
-
result["
|
|
383
|
+
if infer_disposition:
|
|
384
|
+
result["disposition"] = DEFAULT_DISPOSITION.copy()
|
|
385
385
|
return result
|
|
386
386
|
|
|
387
387
|
|
|
@@ -393,12 +393,12 @@ async def list_banks(pool) -> list:
|
|
|
393
393
|
pool: Database connection pool
|
|
394
394
|
|
|
395
395
|
Returns:
|
|
396
|
-
List of dicts with bank_id, name,
|
|
396
|
+
List of dicts with bank_id, name, disposition, background, created_at, updated_at
|
|
397
397
|
"""
|
|
398
398
|
async with acquire_with_retry(pool) as conn:
|
|
399
399
|
rows = await conn.fetch(
|
|
400
400
|
"""
|
|
401
|
-
SELECT bank_id, name,
|
|
401
|
+
SELECT bank_id, name, disposition, background, created_at, updated_at
|
|
402
402
|
FROM banks
|
|
403
403
|
ORDER BY updated_at DESC
|
|
404
404
|
"""
|
|
@@ -407,14 +407,14 @@ async def list_banks(pool) -> list:
|
|
|
407
407
|
result = []
|
|
408
408
|
for row in rows:
|
|
409
409
|
# asyncpg returns JSONB as a string, so parse it
|
|
410
|
-
|
|
411
|
-
if isinstance(
|
|
412
|
-
|
|
410
|
+
disposition_data = row["disposition"]
|
|
411
|
+
if isinstance(disposition_data, str):
|
|
412
|
+
disposition_data = json.loads(disposition_data)
|
|
413
413
|
|
|
414
414
|
result.append({
|
|
415
415
|
"bank_id": row["bank_id"],
|
|
416
416
|
"name": row["name"],
|
|
417
|
-
"
|
|
417
|
+
"disposition": disposition_data,
|
|
418
418
|
"background": row["background"],
|
|
419
419
|
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
|
|
420
420
|
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
|