Skip to content

Commit c4ef090

Browse files
authored
feat: support markdown in reflect and mental models (#307)
* feat: support markdown in reflect and mental models * chore: regenerate clients and OpenAPI spec with markdown field descriptions
1 parent 96f4872 commit c4ef090

File tree

18 files changed

+226
-37
lines changed

18 files changed

+226
-37
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Fix mental_models primary key to be scoped per bank
2+
3+
Revision ID: w8r9s0t1u2v3
4+
Revises: v7q8r9s0t1u2
5+
Create Date: 2026-02-05
6+
7+
This migration fixes a critical bank isolation bug where mental_models.id was
8+
globally unique across all banks instead of being scoped per bank. This caused
9+
conflicts when different banks tried to use the same custom ID.
10+
11+
CRITICAL FIX: Changes primary key from (id) to (bank_id, id) to ensure proper isolation.
12+
"""
13+
14+
from collections.abc import Sequence
15+
16+
from alembic import context, op
17+
18+
revision: str = "w8r9s0t1u2v3"
19+
down_revision: str | Sequence[str] | None = "v7q8r9s0t1u2"
20+
branch_labels: str | Sequence[str] | None = None
21+
depends_on: str | Sequence[str] | None = None
22+
23+
24+
def _get_schema_prefix() -> str:
25+
"""Get schema prefix for table names (required for multi-tenant support)."""
26+
schema = context.config.get_main_option("target_schema")
27+
return f'"{schema}".' if schema else ""
28+
29+
30+
def upgrade() -> None:
31+
"""Change mental_models primary key from (id) to (bank_id, id) for proper bank isolation."""
32+
schema = _get_schema_prefix()
33+
34+
# Drop the old primary key constraint (just id)
35+
# Note: The constraint might be named differently on different DBs
36+
# Try both old names (pinned_reflections_pkey from original, mental_models_pkey from rename)
37+
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS pinned_reflections_pkey")
38+
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS mental_models_pkey")
39+
40+
# Create the new composite primary key (bank_id, id)
41+
# This ensures IDs are scoped per bank, not globally
42+
op.execute(f"""
43+
ALTER TABLE {schema}mental_models
44+
ADD CONSTRAINT mental_models_pkey PRIMARY KEY (bank_id, id)
45+
""")
46+
47+
48+
def downgrade() -> None:
49+
"""Revert mental_models primary key from (bank_id, id) to (id)."""
50+
schema = _get_schema_prefix()
51+
52+
# Drop the composite primary key
53+
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS mental_models_pkey")
54+
55+
# Restore the old primary key (just id)
56+
# WARNING: This downgrade will fail if there are duplicate IDs across banks
57+
op.execute(f"""
58+
ALTER TABLE {schema}mental_models
59+
ADD CONSTRAINT mental_models_pkey PRIMARY KEY (id)
60+
""")

hindsight-api/hindsight_api/api/http.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,9 @@ class ReflectFact(BaseModel):
523523
)
524524

525525
id: str | None = None
526-
text: str
526+
text: str = Field(
527+
description="Fact text. When type='observation', this contains markdown-formatted consolidated knowledge"
528+
)
527529
type: str | None = None # fact type: world, experience, observation
528530
context: str | None = None
529531
occurred_start: str | None = None
@@ -588,7 +590,7 @@ class ReflectResponse(BaseModel):
588590
model_config = ConfigDict(
589591
json_schema_extra={
590592
"example": {
591-
"text": "Based on my understanding, AI is a transformative technology...",
593+
"text": "## AI Overview\n\nBased on my understanding, AI is a **transformative technology**:\n\n- Used extensively in healthcare\n- Discussed in recent conversations\n- Continues to evolve rapidly",
592594
"based_on": {
593595
"memories": [
594596
{"id": "123", "text": "AI is used in healthcare", "type": "world"},
@@ -616,7 +618,9 @@ class ReflectResponse(BaseModel):
616618
}
617619
)
618620

619-
text: str
621+
text: str = Field(
622+
description="The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)"
623+
)
620624
based_on: ReflectBasedOn | None = Field(
621625
default=None,
622626
description="Evidence used to generate the response. Only present when include.facts is set.",
@@ -1114,7 +1118,9 @@ class MentalModelResponse(BaseModel):
11141118
bank_id: str
11151119
name: str
11161120
source_query: str
1117-
content: str
1121+
content: str = Field(
1122+
description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)"
1123+
)
11181124
tags: list[str] = Field(default_factory=list)
11191125
max_tokens: int = Field(default=2048)
11201126
trigger: MentalModelTrigger = Field(default_factory=MentalModelTrigger)

hindsight-api/hindsight_api/engine/consolidation/prompts.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
CONSOLIDATION_SYSTEM_PROMPT = """You are a memory consolidation system. Your job is to convert facts into durable knowledge (observations) and merge with existing knowledge when appropriate.
44
5-
You must output ONLY valid JSON with no markdown formatting, no code blocks, and no additional text.
5+
You must output ONLY valid JSON with no markdown code blocks or additional text. However, the "text" field within each observation should use markdown formatting (headers, lists, bold, etc.) for clarity and readability.
66
77
## EXTRACT DURABLE KNOWLEDGE, NOT EPHEMERAL STATE
88
Facts often describe events or actions. Extract the DURABLE KNOWLEDGE implied by the fact, not the transient state.
@@ -71,10 +71,15 @@
7171
- New topic → CREATE new observation
7272
- Purely ephemeral → return []
7373
74-
Output JSON array of actions:
74+
Output JSON array of actions (the "text" field should use markdown formatting for structure):
7575
[
76-
{{"action": "update", "learning_id": "uuid-from-observations", "text": "updated knowledge", "reason": "..."}},
77-
{{"action": "create", "text": "new durable knowledge", "reason": "..."}}
76+
{{"action": "update", "learning_id": "uuid-from-observations", "text": "## Updated Knowledge\n\n**Key point**: details here\n\n- Supporting detail 1\n- Supporting detail 2", "reason": "..."}},
77+
{{"action": "create", "text": "## New Durable Knowledge\n\nDescription with **emphasis** and proper structure", "reason": "..."}}
7878
]
7979
80-
Return [] if fact contains no durable knowledge."""
80+
Return [] if fact contains no durable knowledge.
81+
82+
IMPORTANT: Format the "text" field with markdown for better readability:
83+
- Use headers, lists, bold/italic, tables where appropriate
84+
- CRITICAL: Add blank lines before and after block elements (tables, code blocks, lists)
85+
- Ensure proper spacing for markdown to render correctly"""

hindsight-api/hindsight_api/engine/reflect/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class ReflectAction(BaseModel):
3131
default=None, description="Observation sections for done action (when output_mode=observations)"
3232
)
3333
# Plain text answer fields (for output_mode=answer)
34-
answer: str | None = Field(default=None, description="Plain text answer for done action (no markdown)")
34+
answer: str | None = Field(default=None, description="Well-formatted markdown answer for done action")
3535
answer_memory_ids: list[str] | None = Field(
3636
default=None, description="Memory IDs supporting the answer", alias="memory_ids"
3737
)

hindsight-api/hindsight_api/engine/reflect/prompts.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,11 @@ def build_system_prompt_for_tools(
300300
parts.extend(
301301
[
302302
"",
303-
"## Output Format: Plain Text Answer",
304-
"Call done() with a plain text 'answer' field.",
305-
"- Do NOT use markdown formatting",
303+
"## Output Format: Well-Formatted Markdown Answer",
304+
"Call done() with a well-formatted markdown 'answer' field.",
305+
"- USE markdown formatting for structure (headers, lists, bold, italic, code blocks, tables, etc.)",
306+
"- CRITICAL: Add blank lines before and after block elements (tables, code blocks, lists)",
307+
"- Format for clarity and readability with proper spacing and hierarchy",
306308
"- NEVER include memory IDs, UUIDs, or 'Memory references' in the answer text",
307309
"- Put IDs ONLY in the memory_ids/mental_model_ids/observation_ids arrays, not in the answer",
308310
]
@@ -485,8 +487,17 @@ def build_final_prompt(
485487
Only say "I don't have information" if the retrieved data is truly unrelated to the question.
486488
Do NOT fabricate information that has no basis in the retrieved data.
487489
490+
FORMATTING: Use proper markdown formatting in your answer:
491+
- Headers (##, ###) for sections
492+
- Lists (bullet or numbered) for enumerations
493+
- Bold/italic for emphasis
494+
- Tables with proper syntax (ensure blank line before and after)
495+
- Code blocks where appropriate
496+
- CRITICAL: Always add blank lines before and after block elements (tables, code blocks, lists)
497+
- Proper spacing between sections
498+
488499
CRITICAL: Output ONLY the final synthesized answer. Do NOT include:
489500
- Meta-commentary about what you're doing ("I'll search...", "Let me analyze...")
490501
- Explanations of your reasoning process
491502
- Descriptions of your approach
492-
Just provide the direct answer."""
503+
Just provide the direct answer with proper markdown formatting."""

hindsight-api/hindsight_api/engine/reflect/tools_schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
"properties": {
140140
"answer": {
141141
"type": "string",
142-
"description": "Your response as plain text. Do NOT use markdown formatting. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
142+
"description": "Your response as well-formatted markdown. Use headers, lists, bold/italic, and code blocks for clarity. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
143143
},
144144
"memory_ids": {
145145
"type": "array",
@@ -190,7 +190,7 @@ def _build_done_tool_with_directives(directive_rules: list[str]) -> dict:
190190
"properties": {
191191
"answer": {
192192
"type": "string",
193-
"description": "Your response as plain text. Do NOT use markdown formatting. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
193+
"description": "Your response as well-formatted markdown. Use headers, lists, bold/italic, and code blocks for clarity. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
194194
},
195195
"memory_ids": {
196196
"type": "array",

hindsight-clients/python/hindsight_client_api/models/mental_model_response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import re # noqa: F401
1818
import json
1919

20-
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
20+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
2121
from typing import Any, ClassVar, Dict, List, Optional
2222
from hindsight_client_api.models.mental_model_trigger import MentalModelTrigger
2323
from typing import Optional, Set
@@ -31,7 +31,7 @@ class MentalModelResponse(BaseModel):
3131
bank_id: StrictStr
3232
name: StrictStr
3333
source_query: StrictStr
34-
content: StrictStr
34+
content: StrictStr = Field(description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)")
3535
tags: Optional[List[StrictStr]] = None
3636
max_tokens: Optional[StrictInt] = 2048
3737
trigger: Optional[MentalModelTrigger] = None

hindsight-clients/python/hindsight_client_api/models/reflect_fact.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import re # noqa: F401
1818
import json
1919

20-
from pydantic import BaseModel, ConfigDict, StrictStr
20+
from pydantic import BaseModel, ConfigDict, Field, StrictStr
2121
from typing import Any, ClassVar, Dict, List, Optional
2222
from typing import Optional, Set
2323
from typing_extensions import Self
@@ -27,7 +27,7 @@ class ReflectFact(BaseModel):
2727
A fact used in think response.
2828
""" # noqa: E501
2929
id: Optional[StrictStr] = None
30-
text: StrictStr
30+
text: StrictStr = Field(description="Fact text. When type='observation', this contains markdown-formatted consolidated knowledge")
3131
type: Optional[StrictStr] = None
3232
context: Optional[StrictStr] = None
3333
occurred_start: Optional[StrictStr] = None

hindsight-clients/python/hindsight_client_api/models/reflect_response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import re # noqa: F401
1818
import json
1919

20-
from pydantic import BaseModel, ConfigDict, StrictStr
20+
from pydantic import BaseModel, ConfigDict, Field, StrictStr
2121
from typing import Any, ClassVar, Dict, List, Optional
2222
from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
2323
from hindsight_client_api.models.reflect_trace import ReflectTrace
@@ -29,7 +29,7 @@ class ReflectResponse(BaseModel):
2929
"""
3030
Response model for think endpoint.
3131
""" # noqa: E501
32-
text: StrictStr
32+
text: StrictStr = Field(description="The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)")
3333
based_on: Optional[ReflectBasedOn] = None
3434
structured_output: Optional[Dict[str, Any]] = None
3535
usage: Optional[TokenUsage] = None

hindsight-clients/typescript/generated/types.gen.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,8 @@ export type MentalModelResponse = {
10341034
source_query: string;
10351035
/**
10361036
* Content
1037+
*
1038+
* The mental model content as well-formatted markdown (auto-generated from reflect endpoint)
10371039
*/
10381040
content: string;
10391041
/**
@@ -1382,6 +1384,8 @@ export type ReflectFact = {
13821384
id?: string | null;
13831385
/**
13841386
* Text
1387+
*
1388+
* Fact text. When type='observation', this contains markdown-formatted consolidated knowledge
13851389
*/
13861390
text: string;
13871391
/**
@@ -1523,6 +1527,8 @@ export type ReflectRequest = {
15231527
export type ReflectResponse = {
15241528
/**
15251529
* Text
1530+
*
1531+
* The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)
15261532
*/
15271533
text: string;
15281534
/**

0 commit comments

Comments
 (0)