diff --git a/hindsight-api-slim/hindsight_api/api/http.py b/hindsight-api-slim/hindsight_api/api/http.py index e6a6e2be..e9463535 100644 --- a/hindsight-api-slim/hindsight_api/api/http.py +++ b/hindsight-api-slim/hindsight_api/api/http.py @@ -1534,13 +1534,14 @@ class MentalModelResponse(BaseModel): id: str bank_id: str name: str - source_query: str - content: str = Field( - description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)" + source_query: str | None = None + content: str | None = Field( + default=None, + description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)", ) tags: list[str] = FieldWithDefault(list) - max_tokens: int = Field(default=2048) - trigger: MentalModelTrigger = FieldWithDefault(MentalModelTrigger) + max_tokens: int | None = Field(default=None) + trigger: MentalModelTrigger | None = Field(default=None) last_refreshed_at: str | None = None created_at: str | None = None reflect_response: dict | None = Field( @@ -3041,6 +3042,10 @@ def _register_routes(app: FastAPI): bank_id: str, tags_filter: list[str] | None = Query(None, alias="tags", description="Filter by tags"), tags_match: Literal["any", "all", "exact"] = Query("any", description="How to match tags"), + detail: Literal["metadata", "content", "full"] = Query( + "full", + description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)", + ), limit: int = Query(100, ge=1, le=1000), offset: int = Query(0, ge=0), request_context: RequestContext = Depends(get_request_context), @@ -3051,6 +3056,7 @@ def _register_routes(app: FastAPI): bank_id=bank_id, tags=tags_filter, tags_match=tags_match, + detail=detail, limit=limit, offset=offset, request_context=request_context, @@ -3078,6 +3084,10 @@ def _register_routes(app: FastAPI): async def api_get_mental_model( bank_id: str, mental_model_id: str, + detail: Literal["metadata", "content", "full"] = Query( + "full", + description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)", + ), request_context: RequestContext = Depends(get_request_context), ): """Get a mental model by ID.""" @@ -3085,6 +3095,7 @@ def _register_routes(app: FastAPI): mental_model = await app.state.memory.get_mental_model( bank_id=bank_id, mental_model_id=mental_model_id, + detail=detail, request_context=request_context, ) if mental_model is None: diff --git a/hindsight-api-slim/hindsight_api/engine/memory_engine.py b/hindsight-api-slim/hindsight_api/engine/memory_engine.py index 557e4445..56d6180f 100644 --- a/hindsight-api-slim/hindsight_api/engine/memory_engine.py +++ b/hindsight-api-slim/hindsight_api/engine/memory_engine.py @@ -6351,6 +6351,7 @@ class MemoryEngine(MemoryEngineInterface): *, tags: list[str] | None = None, tags_match: str = "any", + detail: str = "full", limit: int = 100, offset: int = 0, request_context: "RequestContext", @@ -6361,6 +6362,7 @@ class MemoryEngine(MemoryEngineInterface): bank_id: Bank identifier tags: Optional tags to filter by tags_match: How to match tags - 'any', 'all', or 'exact' + detail: Detail level - 'metadata', 'content', or 'full' limit: Maximum number of results offset: Offset for pagination request_context: Request context for authentication @@ -6402,13 +6404,14 @@ class MemoryEngine(MemoryEngineInterface): *params, ) - return [self._row_to_mental_model(row) for row in rows] + return [self._row_to_mental_model(row, detail=detail) for row in rows] async def get_mental_model( self, bank_id: str, mental_model_id: str, *, + detail: str = "full", request_context: "RequestContext", ) -> dict[str, Any] | None: """Get a single pinned mental model by ID. @@ -6416,6 +6419,7 @@ class MemoryEngine(MemoryEngineInterface): Args: bank_id: Bank identifier mental_model_id: Pinned mental model UUID + detail: Detail level - 'metadata', 'content', or 'full' request_context: Request context for authentication Returns: @@ -6449,7 +6453,7 @@ class MemoryEngine(MemoryEngineInterface): mental_model_id, ) - result = self._row_to_mental_model(row) if row else None + result = self._row_to_mental_model(row, detail=detail) if row else None # Post-operation hook (usage recording) if result and self._operation_validator: @@ -6847,34 +6851,47 @@ class MemoryEngine(MemoryEngineInterface): return result == "DELETE 1" - def _row_to_mental_model(self, row) -> dict[str, Any]: - """Convert a database row to a mental model dict.""" - reflect_response = row.get("reflect_response") - # Parse JSON string to dict if needed (asyncpg may return JSONB as string) - if isinstance(reflect_response, str): - try: - reflect_response = json.loads(reflect_response) - except json.JSONDecodeError: - reflect_response = None + _MENTAL_MODEL_METADATA_FIELDS = frozenset({"id", "bank_id", "name", "tags", "last_refreshed_at", "created_at"}) + + def _row_to_mental_model(self, row, *, detail: str = "full") -> dict[str, Any]: + """Convert a database row to a mental model dict. + + Args: + row: Database row + detail: Detail level - 'metadata', 'content', or 'full' + """ + result: dict[str, Any] = { + "id": str(row["id"]), + "bank_id": row["bank_id"], + "name": row["name"], + "tags": row["tags"] or [], + "last_refreshed_at": row["last_refreshed_at"].isoformat() if row["last_refreshed_at"] else None, + "created_at": row["created_at"].isoformat() if row["created_at"] else None, + } + if detail == "metadata": + return result + trigger = row.get("trigger") if isinstance(trigger, str): try: trigger = json.loads(trigger) except json.JSONDecodeError: trigger = None - return { - "id": str(row["id"]), - "bank_id": row["bank_id"], - "name": row["name"], - "source_query": row["source_query"], - "content": row["content"], - "tags": row["tags"] or [], - "max_tokens": row.get("max_tokens"), - "trigger": trigger, - "last_refreshed_at": row["last_refreshed_at"].isoformat() if row["last_refreshed_at"] else None, - "created_at": row["created_at"].isoformat() if row["created_at"] else None, - "reflect_response": reflect_response, - } + result["source_query"] = row["source_query"] + result["content"] = row["content"] + result["max_tokens"] = row.get("max_tokens") + result["trigger"] = trigger + + if detail == "full": + reflect_response = row.get("reflect_response") + if isinstance(reflect_response, str): + try: + reflect_response = json.loads(reflect_response) + except json.JSONDecodeError: + reflect_response = None + result["reflect_response"] = reflect_response + + return result # ========================================================================= # Directives - Hard rules injected into prompts diff --git a/hindsight-api-slim/hindsight_api/mcp_tools.py b/hindsight-api-slim/hindsight_api/mcp_tools.py index 2ac95bbc..3ccec38d 100644 --- a/hindsight-api-slim/hindsight_api/mcp_tools.py +++ b/hindsight-api-slim/hindsight_api/mcp_tools.py @@ -1002,6 +1002,7 @@ def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCP @mcp.tool() async def list_mental_models( tags: list[str] | None = None, + detail: str = "full", bank_id: str | None = None, ) -> str: """ @@ -1013,6 +1014,7 @@ def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCP Args: tags: Optional tags to filter by (returns models matching any tag) + detail: Detail level - 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response). Default: 'full' bank_id: Optional bank to list from (defaults to session bank). Use for cross-bank operations. """ try: @@ -1023,6 +1025,7 @@ def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCP models = await memory.list_mental_models( bank_id=target_bank, tags=tags, + detail=detail, request_context=_get_request_context(config), ) return json.dumps({"items": models}, indent=2, default=str) @@ -1038,6 +1041,7 @@ def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCP @mcp.tool() async def list_mental_models( tags: list[str] | None = None, + detail: str = "full", ) -> dict: """ List mental models (pinned reflections) for this memory bank. @@ -1048,6 +1052,7 @@ def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCP Args: tags: Optional tags to filter by (returns models matching any tag) + detail: Detail level - 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response). Default: 'full' """ try: target_bank = config.bank_id_resolver() @@ -1057,6 +1062,7 @@ def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCP models = await memory.list_mental_models( bank_id=target_bank, tags=tags, + detail=detail, request_context=_get_request_context(config), ) return {"items": models} @@ -1076,16 +1082,18 @@ def _register_get_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPTo @mcp.tool() async def get_mental_model( mental_model_id: str, + detail: str = "full", bank_id: str | None = None, ) -> str: """ Get a specific mental model by ID. - Returns the full mental model including its generated content, source query, - and metadata. Use list_mental_models first to discover available model IDs. + Returns the mental model with the requested detail level. Use list_mental_models + first to discover available model IDs. Args: mental_model_id: The ID of the mental model to retrieve + detail: Detail level - 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response). Default: 'full' bank_id: Optional bank (defaults to session bank). Use for cross-bank operations. """ try: @@ -1096,6 +1104,7 @@ def _register_get_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPTo model = await memory.get_mental_model( bank_id=target_bank, mental_model_id=mental_model_id, + detail=detail, request_context=_get_request_context(config), ) if model is None: @@ -1113,15 +1122,17 @@ def _register_get_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPTo @mcp.tool() async def get_mental_model( mental_model_id: str, + detail: str = "full", ) -> dict: """ Get a specific mental model by ID. - Returns the full mental model including its generated content, source query, - and metadata. Use list_mental_models first to discover available model IDs. + Returns the mental model with the requested detail level. Use list_mental_models + first to discover available model IDs. Args: mental_model_id: The ID of the mental model to retrieve + detail: Detail level - 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response). Default: 'full' """ try: target_bank = config.bank_id_resolver() @@ -1131,6 +1142,7 @@ def _register_get_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPTo model = await memory.get_mental_model( bank_id=target_bank, mental_model_id=mental_model_id, + detail=detail, request_context=_get_request_context(config), ) if model is None: diff --git a/hindsight-api-slim/tests/test_mcp_tools.py b/hindsight-api-slim/tests/test_mcp_tools.py index 29cc6044..34967d27 100644 --- a/hindsight-api-slim/tests/test_mcp_tools.py +++ b/hindsight-api-slim/tests/test_mcp_tools.py @@ -1,5 +1,6 @@ """Tests for the shared MCP tools module.""" +import json from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock @@ -75,25 +76,63 @@ class TestBuildContentDict: # ========================================================================= +_MENTAL_MODEL_METADATA_FIELDS = frozenset({"id", "bank_id", "name", "tags", "last_refreshed_at", "created_at"}) + +_FULL_MENTAL_MODELS = [ + { + "id": "mm-1", + "bank_id": "test-bank", + "name": "Coding Prefs", + "source_query": "coding preferences?", + "content": "Prefers Python", + "tags": ["coding"], + "max_tokens": 2048, + "trigger": {"interval": "daily"}, + "last_refreshed_at": "2026-01-01T00:00:00", + "created_at": "2026-01-01T00:00:00", + "reflect_response": {"text": "Prefers Python", "based_on": {"world_facts": [{"id": "f1", "text": "Python is popular"}]}}, + }, + { + "id": "mm-2", + "bank_id": "test-bank", + "name": "Goals", + "source_query": "current goals?", + "content": "Ship v2", + "tags": [], + "max_tokens": 2048, + "trigger": None, + "last_refreshed_at": "2026-01-01T00:00:00", + "created_at": "2026-01-01T00:00:00", + "reflect_response": {"text": "Ship v2", "based_on": {}}, + }, +] + + +def _apply_detail(model: dict, detail: str) -> dict: + """Simulate engine detail filtering for mocks.""" + if detail == "metadata": + return {k: v for k, v in model.items() if k in _MENTAL_MODEL_METADATA_FIELDS} + if detail == "content": + return {k: v for k, v in model.items() if k != "reflect_response"} + return model + + @pytest.fixture def mock_memory(): """Create a mock MemoryEngine with all MCP tool methods.""" memory = MagicMock() - # Mental model methods - memory.list_mental_models = AsyncMock( - return_value=[ - {"id": "mm-1", "name": "Coding Prefs", "source_query": "coding preferences?", "content": "Prefers Python"}, - {"id": "mm-2", "name": "Goals", "source_query": "current goals?", "content": "Ship v2"}, - ] - ) - memory.get_mental_model = AsyncMock( - return_value={ - "id": "mm-1", - "name": "Coding Prefs", - "source_query": "coding preferences?", - "content": "Prefers Python", - } - ) + + # Mental model methods — simulate engine detail filtering + async def _list_mental_models(**kwargs): + detail = kwargs.get("detail", "full") + return [_apply_detail(m, detail) for m in _FULL_MENTAL_MODELS] + + async def _get_mental_model(**kwargs): + detail = kwargs.get("detail", "full") + return _apply_detail(_FULL_MENTAL_MODELS[0], detail) + + memory.list_mental_models = AsyncMock(side_effect=_list_mental_models) + memory.get_mental_model = AsyncMock(side_effect=_get_mental_model) memory.create_mental_model = AsyncMock(return_value={"id": "mm-new"}) memory.submit_async_refresh_mental_model = AsyncMock(return_value={"operation_id": "op-123"}) memory.update_mental_model = AsyncMock( @@ -390,12 +429,12 @@ class TestGetMentalModel: assert mock_memory.get_mental_model.call_args.kwargs["bank_id"] == "other-bank" async def test_get_not_found_multi_bank(self, mcp_server_with_mental_models, mock_memory): - mock_memory.get_mental_model.return_value = None + mock_memory.get_mental_model.side_effect = AsyncMock(return_value=None) result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="missing") assert "not found" in result async def test_get_not_found_single_bank(self, mcp_server_single_bank, mock_memory): - mock_memory.get_mental_model.return_value = None + mock_memory.get_mental_model.side_effect = AsyncMock(return_value=None) result = await _tools(mcp_server_single_bank)["get_mental_model"].fn(mental_model_id="missing") assert isinstance(result, dict) assert "not found" in result["error"] @@ -415,6 +454,97 @@ class TestGetMentalModel: assert "error" in result +@pytest.mark.asyncio +class TestListMentalModelsDetail: + """Test the detail parameter for list_mental_models.""" + + async def test_list_detail_full_includes_reflect_response(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["list_mental_models"].fn(detail="full") + parsed = json.loads(result) + item = parsed["items"][0] + assert "reflect_response" in item + assert "content" in item + assert "source_query" in item + + async def test_list_detail_content_excludes_reflect_response(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["list_mental_models"].fn(detail="content") + parsed = json.loads(result) + item = parsed["items"][0] + assert "reflect_response" not in item + assert "content" in item + assert "source_query" in item + assert "trigger" in item + + async def test_list_detail_metadata_only_has_core_fields(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["list_mental_models"].fn(detail="metadata") + parsed = json.loads(result) + item = parsed["items"][0] + assert item["id"] == "mm-1" + assert item["name"] == "Coding Prefs" + assert "tags" in item + assert "content" not in item + assert "source_query" not in item + assert "reflect_response" not in item + assert "trigger" not in item + + async def test_list_detail_default_is_full(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["list_mental_models"].fn() + parsed = json.loads(result) + item = parsed["items"][0] + assert "reflect_response" in item + + async def test_list_detail_single_bank_metadata(self, mcp_server_single_bank, mock_memory): + result = await _tools(mcp_server_single_bank)["list_mental_models"].fn(detail="metadata") + assert isinstance(result, dict) + item = result["items"][0] + assert "id" in item + assert "name" in item + assert "content" not in item + assert "reflect_response" not in item + + +@pytest.mark.asyncio +class TestGetMentalModelDetail: + """Test the detail parameter for get_mental_model.""" + + async def test_get_detail_full_includes_reflect_response(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn( + mental_model_id="mm-1", detail="full" + ) + parsed = json.loads(result) + assert "reflect_response" in parsed + assert "content" in parsed + + async def test_get_detail_content_excludes_reflect_response(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn( + mental_model_id="mm-1", detail="content" + ) + parsed = json.loads(result) + assert "reflect_response" not in parsed + assert "content" in parsed + assert "source_query" in parsed + + async def test_get_detail_metadata_only_has_core_fields(self, mcp_server_with_mental_models, mock_memory): + result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn( + mental_model_id="mm-1", detail="metadata" + ) + parsed = json.loads(result) + assert parsed["id"] == "mm-1" + assert parsed["name"] == "Coding Prefs" + assert "tags" in parsed + assert "content" not in parsed + assert "reflect_response" not in parsed + assert "trigger" not in parsed + + async def test_get_detail_single_bank_content(self, mcp_server_single_bank, mock_memory): + result = await _tools(mcp_server_single_bank)["get_mental_model"].fn( + mental_model_id="mm-1", detail="content" + ) + assert isinstance(result, dict) + assert "content" in result + assert "reflect_response" not in result + + @pytest.mark.asyncio class TestCreateMentalModel: async def test_create_multi_bank(self, mcp_server_with_mental_models, mock_memory): @@ -702,12 +832,12 @@ class TestMentalModelInputValidation: mock_memory.update_mental_model.assert_not_called() async def test_not_found_error_includes_bank_id_multi_bank(self, mcp_server_with_mental_models, mock_memory): - mock_memory.get_mental_model.return_value = None + mock_memory.get_mental_model.side_effect = AsyncMock(return_value=None) result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="missing") assert "test-bank" in result async def test_not_found_error_includes_bank_id_single_bank(self, mcp_server_single_bank, mock_memory): - mock_memory.get_mental_model.return_value = None + mock_memory.get_mental_model.side_effect = AsyncMock(return_value=None) result = await _tools(mcp_server_single_bank)["get_mental_model"].fn(mental_model_id="missing") assert isinstance(result, dict) assert "fixed-bank" in result["error"] diff --git a/hindsight-cli/src/api.rs b/hindsight-cli/src/api.rs index a81857a1..2519059c 100644 --- a/hindsight-cli/src/api.rs +++ b/hindsight-cli/src/api.rs @@ -550,14 +550,14 @@ impl ApiClient { pub fn list_mental_models(&self, bank_id: &str, _verbose: bool) -> Result { self.runtime.block_on(async { - let response = self.client.list_mental_models(bank_id, None, None, None, None, None).await?; + let response = self.client.list_mental_models(bank_id, None, None, None, None, None, None).await?; Ok(response.into_inner()) }) } pub fn get_mental_model(&self, bank_id: &str, mental_model_id: &str, _verbose: bool) -> Result { self.runtime.block_on(async { - let response = self.client.get_mental_model(bank_id, mental_model_id, None).await?; + let response = self.client.get_mental_model(bank_id, mental_model_id, None, None).await?; Ok(response.into_inner()) }) } diff --git a/hindsight-cli/src/commands/mental_model.rs b/hindsight-cli/src/commands/mental_model.rs index da5ca667..bf063fc1 100644 --- a/hindsight-cli/src/commands/mental_model.rs +++ b/hindsight-cli/src/commands/mental_model.rs @@ -43,9 +43,11 @@ pub fn list( ); // Show content preview - let preview: String = mental_model.content.chars().take(80).collect(); - let ellipsis = if mental_model.content.len() > 80 { "..." } else { "" }; - println!(" {}{}", ui::dim(&preview), ellipsis); + if let Some(ref content) = mental_model.content { + let preview: String = content.chars().take(80).collect(); + let ellipsis = if content.len() > 80 { "..." } else { "" }; + println!(" {}{}", ui::dim(&preview), ellipsis); + } println!(); } @@ -326,11 +328,15 @@ fn print_mental_model_detail(mental_model: &types::MentalModelResponse) { ui::print_section_header(&mental_model.name); println!(" {} {}", ui::dim("ID:"), ui::gradient_start(&mental_model.id)); - println!(" {} {}", ui::dim("Source Query:"), &mental_model.source_query); + if let Some(ref source_query) = mental_model.source_query { + println!(" {} {}", ui::dim("Source Query:"), source_query); + } - println!(); - println!("{}", ui::gradient_text("─── Content ───")); - println!(); - println!("{}", &mental_model.content); - println!(); + if let Some(ref content) = mental_model.content { + println!(); + println!("{}", ui::gradient_text("─── Content ───")); + println!(); + println!("{}", content); + println!(); + } } diff --git a/hindsight-clients/go/api/openapi.yaml b/hindsight-clients/go/api/openapi.yaml index 6f0e28a1..455e3d22 100644 --- a/hindsight-clients/go/api/openapi.yaml +++ b/hindsight-clients/go/api/openapi.yaml @@ -656,6 +656,23 @@ paths: title: Tags Match type: string style: form + - description: "Detail level: 'metadata' (names/tags only), 'content' (adds\ + \ content/config), 'full' (includes reflect_response)" + explode: true + in: query + name: detail + required: false + schema: + default: full + description: "Detail level: 'metadata' (names/tags only), 'content' (adds\ + \ content/config), 'full' (includes reflect_response)" + enum: + - metadata + - content + - full + title: Detail + type: string + style: form - explode: true in: query name: limit @@ -810,6 +827,23 @@ paths: title: Mental Model Id type: string style: simple + - description: "Detail level: 'metadata' (names/tags only), 'content' (adds\ + \ content/config), 'full' (includes reflect_response)" + explode: true + in: query + name: detail + required: false + schema: + default: full + description: "Detail level: 'metadata' (names/tags only), 'content' (adds\ + \ content/config), 'full' (includes reflect_response)" + enum: + - metadata + - content + - full + title: Detail + type: string + style: form - explode: false in: header name: authorization @@ -4534,12 +4568,10 @@ components: title: Name type: string source_query: - title: Source Query + nullable: true type: string content: - description: The mental model content as well-formatted markdown (auto-generated - from reflect endpoint) - title: Content + nullable: true type: string tags: default: [] @@ -4547,8 +4579,7 @@ components: type: string type: array max_tokens: - default: 2048 - title: Max Tokens + nullable: true type: integer trigger: $ref: '#/components/schemas/MentalModelTrigger-Output' @@ -4563,10 +4594,8 @@ components: nullable: true required: - bank_id - - content - id - name - - source_query title: MentalModelResponse MentalModelTrigger-Input: description: Trigger settings for a mental model. diff --git a/hindsight-clients/go/api_mental_models.go b/hindsight-clients/go/api_mental_models.go index 699adbc7..7955cc4e 100644 --- a/hindsight-clients/go/api_mental_models.go +++ b/hindsight-clients/go/api_mental_models.go @@ -288,9 +288,16 @@ type ApiGetMentalModelRequest struct { ApiService *MentalModelsAPIService bankId string mentalModelId string + detail *string authorization *string } +// Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) +func (r ApiGetMentalModelRequest) Detail(detail string) ApiGetMentalModelRequest { + r.detail = &detail + return r +} + func (r ApiGetMentalModelRequest) Authorization(authorization string) ApiGetMentalModelRequest { r.authorization = &authorization return r @@ -342,6 +349,12 @@ func (a *MentalModelsAPIService) GetMentalModelExecute(r ApiGetMentalModelReques localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.detail != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "detail", r.detail, "form", "") + } else { + var defaultValue string = "full" + r.detail = &defaultValue + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -541,6 +554,7 @@ type ApiListMentalModelsRequest struct { bankId string tags *[]string tagsMatch *string + detail *string limit *int32 offset *int32 authorization *string @@ -558,6 +572,12 @@ func (r ApiListMentalModelsRequest) TagsMatch(tagsMatch string) ApiListMentalMod return r } +// Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) +func (r ApiListMentalModelsRequest) Detail(detail string) ApiListMentalModelsRequest { + r.detail = &detail + return r +} + func (r ApiListMentalModelsRequest) Limit(limit int32) ApiListMentalModelsRequest { r.limit = &limit return r @@ -633,6 +653,12 @@ func (a *MentalModelsAPIService) ListMentalModelsExecute(r ApiListMentalModelsRe var defaultValue string = "any" r.tagsMatch = &defaultValue } + if r.detail != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "detail", r.detail, "form", "") + } else { + var defaultValue string = "full" + r.detail = &defaultValue + } if r.limit != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "") } else { diff --git a/hindsight-clients/go/model_mental_model_response.go b/hindsight-clients/go/model_mental_model_response.go index c197b0ed..a95802a7 100644 --- a/hindsight-clients/go/model_mental_model_response.go +++ b/hindsight-clients/go/model_mental_model_response.go @@ -24,12 +24,11 @@ type MentalModelResponse struct { Id string `json:"id"` BankId string `json:"bank_id"` Name string `json:"name"` - SourceQuery string `json:"source_query"` - // The mental model content as well-formatted markdown (auto-generated from reflect endpoint) - Content string `json:"content"` + SourceQuery NullableString `json:"source_query,omitempty"` + Content NullableString `json:"content,omitempty"` Tags []string `json:"tags,omitempty"` - MaxTokens *int32 `json:"max_tokens,omitempty"` - Trigger *MentalModelTriggerOutput `json:"trigger,omitempty"` + MaxTokens NullableInt32 `json:"max_tokens,omitempty"` + Trigger NullableMentalModelTriggerOutput `json:"trigger,omitempty"` LastRefreshedAt NullableString `json:"last_refreshed_at,omitempty"` CreatedAt NullableString `json:"created_at,omitempty"` ReflectResponse map[string]interface{} `json:"reflect_response,omitempty"` @@ -41,15 +40,11 @@ type _MentalModelResponse MentalModelResponse // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed -func NewMentalModelResponse(id string, bankId string, name string, sourceQuery string, content string) *MentalModelResponse { +func NewMentalModelResponse(id string, bankId string, name string) *MentalModelResponse { this := MentalModelResponse{} this.Id = id this.BankId = bankId this.Name = name - this.SourceQuery = sourceQuery - this.Content = content - var maxTokens int32 = 2048 - this.MaxTokens = &maxTokens return &this } @@ -58,8 +53,6 @@ func NewMentalModelResponse(id string, bankId string, name string, sourceQuery s // but it doesn't guarantee that properties required by API are set func NewMentalModelResponseWithDefaults() *MentalModelResponse { this := MentalModelResponse{} - var maxTokens int32 = 2048 - this.MaxTokens = &maxTokens return &this } @@ -135,52 +128,88 @@ func (o *MentalModelResponse) SetName(v string) { o.Name = v } -// GetSourceQuery returns the SourceQuery field value +// GetSourceQuery returns the SourceQuery field value if set, zero value otherwise (both if not set or set to explicit null). func (o *MentalModelResponse) GetSourceQuery() string { - if o == nil { + if o == nil || IsNil(o.SourceQuery.Get()) { var ret string return ret } - - return o.SourceQuery + return *o.SourceQuery.Get() } -// GetSourceQueryOk returns a tuple with the SourceQuery field value +// GetSourceQueryOk returns a tuple with the SourceQuery field value if set, nil otherwise // and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned func (o *MentalModelResponse) GetSourceQueryOk() (*string, bool) { if o == nil { return nil, false } - return &o.SourceQuery, true + return o.SourceQuery.Get(), o.SourceQuery.IsSet() } -// SetSourceQuery sets field value +// HasSourceQuery returns a boolean if a field has been set. +func (o *MentalModelResponse) HasSourceQuery() bool { + if o != nil && o.SourceQuery.IsSet() { + return true + } + + return false +} + +// SetSourceQuery gets a reference to the given NullableString and assigns it to the SourceQuery field. func (o *MentalModelResponse) SetSourceQuery(v string) { - o.SourceQuery = v + o.SourceQuery.Set(&v) +} +// SetSourceQueryNil sets the value for SourceQuery to be an explicit nil +func (o *MentalModelResponse) SetSourceQueryNil() { + o.SourceQuery.Set(nil) } -// GetContent returns the Content field value +// UnsetSourceQuery ensures that no value is present for SourceQuery, not even an explicit nil +func (o *MentalModelResponse) UnsetSourceQuery() { + o.SourceQuery.Unset() +} + +// GetContent returns the Content field value if set, zero value otherwise (both if not set or set to explicit null). func (o *MentalModelResponse) GetContent() string { - if o == nil { + if o == nil || IsNil(o.Content.Get()) { var ret string return ret } - - return o.Content + return *o.Content.Get() } -// GetContentOk returns a tuple with the Content field value +// GetContentOk returns a tuple with the Content field value if set, nil otherwise // and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned func (o *MentalModelResponse) GetContentOk() (*string, bool) { if o == nil { return nil, false } - return &o.Content, true + return o.Content.Get(), o.Content.IsSet() } -// SetContent sets field value +// HasContent returns a boolean if a field has been set. +func (o *MentalModelResponse) HasContent() bool { + if o != nil && o.Content.IsSet() { + return true + } + + return false +} + +// SetContent gets a reference to the given NullableString and assigns it to the Content field. func (o *MentalModelResponse) SetContent(v string) { - o.Content = v + o.Content.Set(&v) +} +// SetContentNil sets the value for Content to be an explicit nil +func (o *MentalModelResponse) SetContentNil() { + o.Content.Set(nil) +} + +// UnsetContent ensures that no value is present for Content, not even an explicit nil +func (o *MentalModelResponse) UnsetContent() { + o.Content.Unset() } // GetTags returns the Tags field value if set, zero value otherwise. @@ -215,68 +244,88 @@ func (o *MentalModelResponse) SetTags(v []string) { o.Tags = v } -// GetMaxTokens returns the MaxTokens field value if set, zero value otherwise. +// GetMaxTokens returns the MaxTokens field value if set, zero value otherwise (both if not set or set to explicit null). func (o *MentalModelResponse) GetMaxTokens() int32 { - if o == nil || IsNil(o.MaxTokens) { + if o == nil || IsNil(o.MaxTokens.Get()) { var ret int32 return ret } - return *o.MaxTokens + return *o.MaxTokens.Get() } // GetMaxTokensOk returns a tuple with the MaxTokens field value if set, nil otherwise // and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned func (o *MentalModelResponse) GetMaxTokensOk() (*int32, bool) { - if o == nil || IsNil(o.MaxTokens) { + if o == nil { return nil, false } - return o.MaxTokens, true + return o.MaxTokens.Get(), o.MaxTokens.IsSet() } // HasMaxTokens returns a boolean if a field has been set. func (o *MentalModelResponse) HasMaxTokens() bool { - if o != nil && !IsNil(o.MaxTokens) { + if o != nil && o.MaxTokens.IsSet() { return true } return false } -// SetMaxTokens gets a reference to the given int32 and assigns it to the MaxTokens field. +// SetMaxTokens gets a reference to the given NullableInt32 and assigns it to the MaxTokens field. func (o *MentalModelResponse) SetMaxTokens(v int32) { - o.MaxTokens = &v + o.MaxTokens.Set(&v) +} +// SetMaxTokensNil sets the value for MaxTokens to be an explicit nil +func (o *MentalModelResponse) SetMaxTokensNil() { + o.MaxTokens.Set(nil) } -// GetTrigger returns the Trigger field value if set, zero value otherwise. +// UnsetMaxTokens ensures that no value is present for MaxTokens, not even an explicit nil +func (o *MentalModelResponse) UnsetMaxTokens() { + o.MaxTokens.Unset() +} + +// GetTrigger returns the Trigger field value if set, zero value otherwise (both if not set or set to explicit null). func (o *MentalModelResponse) GetTrigger() MentalModelTriggerOutput { - if o == nil || IsNil(o.Trigger) { + if o == nil || IsNil(o.Trigger.Get()) { var ret MentalModelTriggerOutput return ret } - return *o.Trigger + return *o.Trigger.Get() } // GetTriggerOk returns a tuple with the Trigger field value if set, nil otherwise // and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned func (o *MentalModelResponse) GetTriggerOk() (*MentalModelTriggerOutput, bool) { - if o == nil || IsNil(o.Trigger) { + if o == nil { return nil, false } - return o.Trigger, true + return o.Trigger.Get(), o.Trigger.IsSet() } // HasTrigger returns a boolean if a field has been set. func (o *MentalModelResponse) HasTrigger() bool { - if o != nil && !IsNil(o.Trigger) { + if o != nil && o.Trigger.IsSet() { return true } return false } -// SetTrigger gets a reference to the given MentalModelTriggerOutput and assigns it to the Trigger field. +// SetTrigger gets a reference to the given NullableMentalModelTriggerOutput and assigns it to the Trigger field. func (o *MentalModelResponse) SetTrigger(v MentalModelTriggerOutput) { - o.Trigger = &v + o.Trigger.Set(&v) +} +// SetTriggerNil sets the value for Trigger to be an explicit nil +func (o *MentalModelResponse) SetTriggerNil() { + o.Trigger.Set(nil) +} + +// UnsetTrigger ensures that no value is present for Trigger, not even an explicit nil +func (o *MentalModelResponse) UnsetTrigger() { + o.Trigger.Unset() } // GetLastRefreshedAt returns the LastRefreshedAt field value if set, zero value otherwise (both if not set or set to explicit null). @@ -409,16 +458,20 @@ func (o MentalModelResponse) ToMap() (map[string]interface{}, error) { toSerialize["id"] = o.Id toSerialize["bank_id"] = o.BankId toSerialize["name"] = o.Name - toSerialize["source_query"] = o.SourceQuery - toSerialize["content"] = o.Content + if o.SourceQuery.IsSet() { + toSerialize["source_query"] = o.SourceQuery.Get() + } + if o.Content.IsSet() { + toSerialize["content"] = o.Content.Get() + } if !IsNil(o.Tags) { toSerialize["tags"] = o.Tags } - if !IsNil(o.MaxTokens) { - toSerialize["max_tokens"] = o.MaxTokens + if o.MaxTokens.IsSet() { + toSerialize["max_tokens"] = o.MaxTokens.Get() } - if !IsNil(o.Trigger) { - toSerialize["trigger"] = o.Trigger + if o.Trigger.IsSet() { + toSerialize["trigger"] = o.Trigger.Get() } if o.LastRefreshedAt.IsSet() { toSerialize["last_refreshed_at"] = o.LastRefreshedAt.Get() @@ -440,8 +493,6 @@ func (o *MentalModelResponse) UnmarshalJSON(data []byte) (err error) { "id", "bank_id", "name", - "source_query", - "content", } allProperties := make(map[string]interface{}) diff --git a/hindsight-clients/python/hindsight_client_api/api/mental_models_api.py b/hindsight-clients/python/hindsight_client_api/api/mental_models_api.py index e262510c..82ad1a06 100644 --- a/hindsight-clients/python/hindsight_client_api/api/mental_models_api.py +++ b/hindsight-clients/python/hindsight_client_api/api/mental_models_api.py @@ -648,6 +648,7 @@ class MentalModelsApi: self, bank_id: StrictStr, mental_model_id: StrictStr, + detail: Annotated[Optional[StrictStr], Field(description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)")] = None, authorization: Optional[StrictStr] = None, _request_timeout: Union[ None, @@ -670,6 +671,8 @@ class MentalModelsApi: :type bank_id: str :param mental_model_id: (required) :type mental_model_id: str + :param detail: Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + :type detail: str :param authorization: :type authorization: str :param _request_timeout: timeout setting for this request. If one @@ -697,6 +700,7 @@ class MentalModelsApi: _param = self._get_mental_model_serialize( bank_id=bank_id, mental_model_id=mental_model_id, + detail=detail, authorization=authorization, _request_auth=_request_auth, _content_type=_content_type, @@ -724,6 +728,7 @@ class MentalModelsApi: self, bank_id: StrictStr, mental_model_id: StrictStr, + detail: Annotated[Optional[StrictStr], Field(description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)")] = None, authorization: Optional[StrictStr] = None, _request_timeout: Union[ None, @@ -746,6 +751,8 @@ class MentalModelsApi: :type bank_id: str :param mental_model_id: (required) :type mental_model_id: str + :param detail: Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + :type detail: str :param authorization: :type authorization: str :param _request_timeout: timeout setting for this request. If one @@ -773,6 +780,7 @@ class MentalModelsApi: _param = self._get_mental_model_serialize( bank_id=bank_id, mental_model_id=mental_model_id, + detail=detail, authorization=authorization, _request_auth=_request_auth, _content_type=_content_type, @@ -800,6 +808,7 @@ class MentalModelsApi: self, bank_id: StrictStr, mental_model_id: StrictStr, + detail: Annotated[Optional[StrictStr], Field(description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)")] = None, authorization: Optional[StrictStr] = None, _request_timeout: Union[ None, @@ -822,6 +831,8 @@ class MentalModelsApi: :type bank_id: str :param mental_model_id: (required) :type mental_model_id: str + :param detail: Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + :type detail: str :param authorization: :type authorization: str :param _request_timeout: timeout setting for this request. If one @@ -849,6 +860,7 @@ class MentalModelsApi: _param = self._get_mental_model_serialize( bank_id=bank_id, mental_model_id=mental_model_id, + detail=detail, authorization=authorization, _request_auth=_request_auth, _content_type=_content_type, @@ -871,6 +883,7 @@ class MentalModelsApi: self, bank_id, mental_model_id, + detail, authorization, _request_auth, _content_type, @@ -898,6 +911,10 @@ class MentalModelsApi: if mental_model_id is not None: _path_params['mental_model_id'] = mental_model_id # process the query parameters + if detail is not None: + + _query_params.append(('detail', detail)) + # process the header parameters if authorization is not None: _header_params['authorization'] = authorization @@ -1235,6 +1252,7 @@ class MentalModelsApi: bank_id: StrictStr, tags: Annotated[Optional[List[StrictStr]], Field(description="Filter by tags")] = None, tags_match: Annotated[Optional[StrictStr], Field(description="How to match tags")] = None, + detail: Annotated[Optional[StrictStr], Field(description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)")] = None, limit: Optional[Annotated[int, Field(le=1000, strict=True, ge=1)]] = None, offset: Optional[Annotated[int, Field(strict=True, ge=0)]] = None, authorization: Optional[StrictStr] = None, @@ -1261,6 +1279,8 @@ class MentalModelsApi: :type tags: List[str] :param tags_match: How to match tags :type tags_match: str + :param detail: Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + :type detail: str :param limit: :type limit: int :param offset: @@ -1293,6 +1313,7 @@ class MentalModelsApi: bank_id=bank_id, tags=tags, tags_match=tags_match, + detail=detail, limit=limit, offset=offset, authorization=authorization, @@ -1323,6 +1344,7 @@ class MentalModelsApi: bank_id: StrictStr, tags: Annotated[Optional[List[StrictStr]], Field(description="Filter by tags")] = None, tags_match: Annotated[Optional[StrictStr], Field(description="How to match tags")] = None, + detail: Annotated[Optional[StrictStr], Field(description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)")] = None, limit: Optional[Annotated[int, Field(le=1000, strict=True, ge=1)]] = None, offset: Optional[Annotated[int, Field(strict=True, ge=0)]] = None, authorization: Optional[StrictStr] = None, @@ -1349,6 +1371,8 @@ class MentalModelsApi: :type tags: List[str] :param tags_match: How to match tags :type tags_match: str + :param detail: Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + :type detail: str :param limit: :type limit: int :param offset: @@ -1381,6 +1405,7 @@ class MentalModelsApi: bank_id=bank_id, tags=tags, tags_match=tags_match, + detail=detail, limit=limit, offset=offset, authorization=authorization, @@ -1411,6 +1436,7 @@ class MentalModelsApi: bank_id: StrictStr, tags: Annotated[Optional[List[StrictStr]], Field(description="Filter by tags")] = None, tags_match: Annotated[Optional[StrictStr], Field(description="How to match tags")] = None, + detail: Annotated[Optional[StrictStr], Field(description="Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)")] = None, limit: Optional[Annotated[int, Field(le=1000, strict=True, ge=1)]] = None, offset: Optional[Annotated[int, Field(strict=True, ge=0)]] = None, authorization: Optional[StrictStr] = None, @@ -1437,6 +1463,8 @@ class MentalModelsApi: :type tags: List[str] :param tags_match: How to match tags :type tags_match: str + :param detail: Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + :type detail: str :param limit: :type limit: int :param offset: @@ -1469,6 +1497,7 @@ class MentalModelsApi: bank_id=bank_id, tags=tags, tags_match=tags_match, + detail=detail, limit=limit, offset=offset, authorization=authorization, @@ -1494,6 +1523,7 @@ class MentalModelsApi: bank_id, tags, tags_match, + detail, limit, offset, authorization, @@ -1530,6 +1560,10 @@ class MentalModelsApi: _query_params.append(('tags_match', tags_match)) + if detail is not None: + + _query_params.append(('detail', detail)) + if limit is not None: _query_params.append(('limit', limit)) diff --git a/hindsight-clients/python/hindsight_client_api/models/mental_model_response.py b/hindsight-clients/python/hindsight_client_api/models/mental_model_response.py index 8352f168..e1445ad3 100644 --- a/hindsight-clients/python/hindsight_client_api/models/mental_model_response.py +++ b/hindsight-clients/python/hindsight_client_api/models/mental_model_response.py @@ -17,7 +17,7 @@ import pprint import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr +from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List, Optional from hindsight_client_api.models.mental_model_trigger_output import MentalModelTriggerOutput from typing import Optional, Set @@ -30,10 +30,10 @@ class MentalModelResponse(BaseModel): id: StrictStr bank_id: StrictStr name: StrictStr - source_query: StrictStr - content: StrictStr = Field(description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)") + source_query: Optional[StrictStr] = None + content: Optional[StrictStr] = None tags: Optional[List[StrictStr]] = None - max_tokens: Optional[StrictInt] = 2048 + max_tokens: Optional[StrictInt] = None trigger: Optional[MentalModelTriggerOutput] = None last_refreshed_at: Optional[StrictStr] = None created_at: Optional[StrictStr] = None @@ -82,6 +82,26 @@ class MentalModelResponse(BaseModel): # override the default output from pydantic by calling `to_dict()` of trigger if self.trigger: _dict['trigger'] = self.trigger.to_dict() + # set to None if source_query (nullable) is None + # and model_fields_set contains the field + if self.source_query is None and "source_query" in self.model_fields_set: + _dict['source_query'] = None + + # set to None if content (nullable) is None + # and model_fields_set contains the field + if self.content is None and "content" in self.model_fields_set: + _dict['content'] = None + + # set to None if max_tokens (nullable) is None + # and model_fields_set contains the field + if self.max_tokens is None and "max_tokens" in self.model_fields_set: + _dict['max_tokens'] = None + + # set to None if trigger (nullable) is None + # and model_fields_set contains the field + if self.trigger is None and "trigger" in self.model_fields_set: + _dict['trigger'] = None + # set to None if last_refreshed_at (nullable) is None # and model_fields_set contains the field if self.last_refreshed_at is None and "last_refreshed_at" in self.model_fields_set: @@ -115,7 +135,7 @@ class MentalModelResponse(BaseModel): "source_query": obj.get("source_query"), "content": obj.get("content"), "tags": obj.get("tags"), - "max_tokens": obj.get("max_tokens") if obj.get("max_tokens") is not None else 2048, + "max_tokens": obj.get("max_tokens"), "trigger": MentalModelTriggerOutput.from_dict(obj["trigger"]) if obj.get("trigger") is not None else None, "last_refreshed_at": obj.get("last_refreshed_at"), "created_at": obj.get("created_at"), diff --git a/hindsight-clients/typescript/generated/types.gen.ts b/hindsight-clients/typescript/generated/types.gen.ts index b6929f66..e2138dbb 100644 --- a/hindsight-clients/typescript/generated/types.gen.ts +++ b/hindsight-clients/typescript/generated/types.gen.ts @@ -1435,13 +1435,13 @@ export type MentalModelResponse = { /** * Source Query */ - source_query: string; + source_query?: string | null; /** * Content * * The mental model content as well-formatted markdown (auto-generated from reflect endpoint) */ - content: string; + content?: string | null; /** * Tags */ @@ -1449,8 +1449,8 @@ export type MentalModelResponse = { /** * Max Tokens */ - max_tokens?: number; - trigger?: MentalModelTriggerOutput; + max_tokens?: number | null; + trigger?: MentalModelTriggerOutput | null; /** * Last Refreshed At */ @@ -3331,6 +3331,12 @@ export type ListMentalModelsData = { * How to match tags */ tags_match?: "any" | "all" | "exact"; + /** + * Detail + * + * Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + */ + detail?: "metadata" | "content" | "full"; /** * Limit */ @@ -3458,7 +3464,14 @@ export type GetMentalModelData = { */ mental_model_id: string; }; - query?: never; + query?: { + /** + * Detail + * + * Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response) + */ + detail?: "metadata" | "content" | "full"; + }; url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}"; }; diff --git a/hindsight-docs/docs/developer/api/mental-models.mdx b/hindsight-docs/docs/developer/api/mental-models.mdx index 389496d2..9dd5cc8a 100644 --- a/hindsight-docs/docs/developer/api/mental-models.mdx +++ b/hindsight-docs/docs/developer/api/mental-models.mdx @@ -186,19 +186,52 @@ Enable automatic refresh for mental models that need to stay current. Disable it +### Detail Levels + +Both **List** and **Get** endpoints accept an optional `detail` query parameter that controls how much data is returned. This is useful for reducing response size, especially in agent boot flows or MCP clients where context budget is limited. + +| Level | Fields Returned | Use Case | +|-------|----------------|----------| +| `metadata` | `id`, `bank_id`, `name`, `tags`, `last_refreshed_at`, `created_at` | Inventory — "what models exist?" | +| `content` | All metadata fields + `source_query`, `content`, `max_tokens`, `trigger` | Agent boot — "what do the models say?" | +| `full` (default) | All fields including `reflect_response` | Deep inspection — "what evidence backs this model?" | + +```bash +# List only names and tags (smallest response) +curl "$BASE_URL/v1/default/banks/$BANK_ID/mental-models?detail=metadata" + +# List with content but without provenance chains +curl "$BASE_URL/v1/default/banks/$BANK_ID/mental-models?detail=content" + +# Get full detail (default behavior) +curl "$BASE_URL/v1/default/banks/$BANK_ID/mental-models/$MODEL_ID?detail=full" +``` + +The `detail` parameter is also available in the MCP tools: + +```json +{"bank_id": "my-bank", "detail": "metadata"} +``` + +:::tip +Use `detail=content` for agent orientation flows. It includes everything the agent needs to understand the models without the heavyweight `reflect_response` provenance chains, which can exceed 200KB for banks with many models. +::: + ### Response Fields -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | Unique mental model ID | -| `bank_id` | string | Memory bank ID | -| `name` | string | Human-readable name | -| `source_query` | string | The query used to generate content | -| `content` | string | The generated mental model text | -| `tags` | list | Tags for filtering | -| `last_refreshed_at` | string | When the mental model was last updated | -| `created_at` | string | When the mental model was created | -| `reflect_response` | object | Full reflect response including `based_on` facts | +| Field | Type | Detail Level | Description | +|-------|------|-------------|-------------| +| `id` | string | metadata | Unique mental model ID | +| `bank_id` | string | metadata | Memory bank ID | +| `name` | string | metadata | Human-readable name | +| `tags` | list | metadata | Tags for filtering | +| `last_refreshed_at` | string | metadata | When the mental model was last updated | +| `created_at` | string | metadata | When the mental model was created | +| `source_query` | string | content | The query used to generate content | +| `content` | string | content | The generated mental model text | +| `max_tokens` | int | content | Maximum tokens for the mental model content | +| `trigger` | object | content | Trigger settings (see [Automatic Refresh](#automatic-refresh)) | +| `reflect_response` | object | full | Full reflect response including `based_on` provenance facts | --- diff --git a/hindsight-docs/static/openapi.json b/hindsight-docs/static/openapi.json index 46801c5c..37e41154 100644 --- a/hindsight-docs/static/openapi.json +++ b/hindsight-docs/static/openapi.json @@ -971,6 +971,23 @@ }, "description": "How to match tags" }, + { + "name": "detail", + "in": "query", + "required": false, + "schema": { + "enum": [ + "metadata", + "content", + "full" + ], + "type": "string", + "description": "Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)", + "default": "full", + "title": "Detail" + }, + "description": "Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)" + }, { "name": "limit", "in": "query", @@ -1129,6 +1146,23 @@ "title": "Mental Model Id" } }, + { + "name": "detail", + "in": "query", + "required": false, + "schema": { + "enum": [ + "metadata", + "content", + "full" + ], + "type": "string", + "description": "Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)", + "default": "full", + "title": "Detail" + }, + "description": "Detail level: 'metadata' (names/tags only), 'content' (adds content/config), 'full' (includes reflect_response)" + }, { "name": "authorization", "in": "header", @@ -6706,11 +6740,25 @@ "title": "Name" }, "source_query": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Source Query" }, "content": { - "type": "string", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], "title": "Content", "description": "The mental model content as well-formatted markdown (auto-generated from reflect endpoint)" }, @@ -6723,13 +6771,25 @@ "default": [] }, "max_tokens": { - "type": "integer", - "title": "Max Tokens", - "default": 2048 + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Tokens" }, "trigger": { - "$ref": "#/components/schemas/MentalModelTrigger-Output", - "default": {} + "anyOf": [ + { + "$ref": "#/components/schemas/MentalModelTrigger-Output" + }, + { + "type": "null" + } + ] }, "last_refreshed_at": { "anyOf": [ @@ -6771,9 +6831,7 @@ "required": [ "id", "bank_id", - "name", - "source_query", - "content" + "name" ], "title": "MentalModelResponse", "description": "Response model for a mental model (stored reflect response)."