Skip to content

Commit 7097716

Browse files
authored
feat: improve mental models ux on control plane (#297)
* feat: improve mental models ux on control plane * feat: improve mental models ux on control plane * gen * feat(cli): add --id flag to mental model create command * fix(cli): revert unused variable underscore prefix that breaks compilation The underscore prefix on stdout/stderr variables was added to suppress warnings, but these variables are actually used in assert messages, causing compilation errors. Reverting to original names.
1 parent d3302c9 commit 7097716

File tree

9 files changed

+598
-169
lines changed

9 files changed

+598
-169
lines changed

hindsight-api/hindsight_api/api/http.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,7 @@ class CreateMentalModelRequest(BaseModel):
11341134
model_config = ConfigDict(
11351135
json_schema_extra={
11361136
"example": {
1137+
"id": "team-communication",
11371138
"name": "Team Communication Preferences",
11381139
"source_query": "How does the team prefer to communicate?",
11391140
"tags": ["team"],
@@ -1143,6 +1144,9 @@ class CreateMentalModelRequest(BaseModel):
11431144
}
11441145
)
11451146

1147+
id: str | None = Field(
1148+
None, description="Optional custom ID for the mental model (alphanumeric lowercase with hyphens)"
1149+
)
11461150
name: str = Field(description="Human-readable name for the mental model")
11471151
source_query: str = Field(description="The query to run to generate content")
11481152
tags: list[str] = Field(default_factory=list, description="Tags for scoped visibility")
@@ -2386,6 +2390,7 @@ async def api_create_mental_model(
23862390
name=body.name,
23872391
source_query=body.source_query,
23882392
content="Generating content...",
2393+
mental_model_id=body.id if body.id else None,
23892394
tags=body.tags if body.tags else None,
23902395
max_tokens=body.max_tokens,
23912396
trigger=body.trigger.model_dump() if body.trigger else None,

hindsight-api/tests/test_reflections.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,45 @@ async def test_delete_mental_model(self, memory: MemoryEngine, request_context):
175175
# Cleanup
176176
await memory.delete_bank(bank_id, request_context=request_context)
177177

178+
@pytest.mark.asyncio
179+
async def test_create_mental_model_with_custom_id(self, memory: MemoryEngine, request_context):
180+
"""Test creating a mental model with a custom ID."""
181+
bank_id = f"test-mental-model-custom-id-{uuid.uuid4().hex[:8]}"
182+
183+
# Create the bank first
184+
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
185+
186+
# Create a mental model with a custom ID
187+
custom_id = "team-communication-preferences"
188+
mental_model = await memory.create_mental_model(
189+
bank_id=bank_id,
190+
mental_model_id=custom_id,
191+
name="Team Communication Preferences",
192+
source_query="How does the team prefer to communicate?",
193+
content="The team prefers async communication via Slack",
194+
tags=["team", "communication"],
195+
request_context=request_context,
196+
)
197+
198+
# Verify the custom ID was used
199+
assert mental_model["id"] == custom_id
200+
assert mental_model["name"] == "Team Communication Preferences"
201+
assert mental_model["tags"] == ["team", "communication"]
202+
203+
# Verify we can retrieve it with the custom ID
204+
fetched = await memory.get_mental_model(
205+
bank_id=bank_id,
206+
mental_model_id=custom_id,
207+
request_context=request_context,
208+
)
209+
210+
assert fetched is not None
211+
assert fetched["id"] == custom_id
212+
assert fetched["name"] == "Team Communication Preferences"
213+
214+
# Cleanup
215+
await memory.delete_bank(bank_id, request_context=request_context)
216+
178217

179218
class TestObservationsAPI:
180219
"""Test observations API endpoints.

hindsight-cli/src/commands/mental_model.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub fn create(
9898
bank_id: &str,
9999
name: &str,
100100
source_query: &str,
101+
id: Option<&str>,
101102
verbose: bool,
102103
output_format: OutputFormat,
103104
) -> Result<()> {
@@ -108,6 +109,7 @@ pub fn create(
108109
};
109110

110111
let request = types::CreateMentalModelRequest {
112+
id: id.map(|s| s.to_string()),
111113
name: name.to_string(),
112114
source_query: source_query.to_string(),
113115
max_tokens: 2048,

hindsight-cli/src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,10 @@ enum MentalModelCommands {
596596

597597
/// Source query to generate the mental model from
598598
source_query: String,
599+
600+
/// Optional custom ID for the mental model (alphanumeric lowercase with hyphens)
601+
#[arg(long)]
602+
id: Option<String>,
599603
},
600604

601605
/// Update a mental model
@@ -863,8 +867,8 @@ fn run() -> Result<()> {
863867
MentalModelCommands::Get { bank_id, mental_model_id } => {
864868
commands::mental_model::get(&client, &bank_id, &mental_model_id, verbose, output_format)
865869
}
866-
MentalModelCommands::Create { bank_id, name, source_query } => {
867-
commands::mental_model::create(&client, &bank_id, &name, &source_query, verbose, output_format)
870+
MentalModelCommands::Create { bank_id, name, source_query, id } => {
871+
commands::mental_model::create(&client, &bank_id, &name, &source_query, id.as_deref(), verbose, output_format)
868872
}
869873
MentalModelCommands::Update { bank_id, mental_model_id, name } => {
870874
commands::mental_model::update(&client, &bank_id, &mental_model_id, name, verbose, output_format)

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ class CreateMentalModelRequest(BaseModel):
2828
"""
2929
Request model for creating a mental model.
3030
""" # noqa: E501
31+
id: Optional[StrictStr] = None
3132
name: StrictStr = Field(description="Human-readable name for the mental model")
3233
source_query: StrictStr = Field(description="The query to run to generate content")
3334
tags: Optional[List[StrictStr]] = Field(default=None, description="Tags for scoped visibility")
3435
max_tokens: Optional[Annotated[int, Field(le=8192, strict=True, ge=256)]] = Field(default=2048, description="Maximum tokens for generated content")
3536
trigger: Optional[MentalModelTrigger] = Field(default=None, description="Trigger settings")
36-
__properties: ClassVar[List[str]] = ["name", "source_query", "tags", "max_tokens", "trigger"]
37+
__properties: ClassVar[List[str]] = ["id", "name", "source_query", "tags", "max_tokens", "trigger"]
3738

3839
model_config = ConfigDict(
3940
populate_by_name=True,
@@ -77,6 +78,11 @@ def to_dict(self) -> Dict[str, Any]:
7778
# override the default output from pydantic by calling `to_dict()` of trigger
7879
if self.trigger:
7980
_dict['trigger'] = self.trigger.to_dict()
81+
# set to None if id (nullable) is None
82+
# and model_fields_set contains the field
83+
if self.id is None and "id" in self.model_fields_set:
84+
_dict['id'] = None
85+
8086
return _dict
8187

8288
@classmethod
@@ -89,6 +95,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
8995
return cls.model_validate(obj)
9096

9197
_obj = cls.model_validate({
98+
"id": obj.get("id"),
9299
"name": obj.get("name"),
93100
"source_query": obj.get("source_query"),
94101
"tags": obj.get("tags"),

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,12 @@ export type CreateDirectiveRequest = {
393393
* Request model for creating a mental model.
394394
*/
395395
export type CreateMentalModelRequest = {
396+
/**
397+
* Id
398+
*
399+
* Optional custom ID for the mental model (alphanumeric lowercase with hyphens)
400+
*/
401+
id?: string | null;
396402
/**
397403
* Name
398404
*

0 commit comments

Comments
 (0)