Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 4ed5d169a7 fix: add spacing between Fact Types label and pills, rename to Exclude all mental models 2026-03-19 16:41:44 +01:00
Nicolò Boschi 3629b350af feat: pill-style toggle buttons for fact type filter (blue/emerald/amber per type) 2026-03-19 16:33:12 +01:00
Nicolò Boschi 7788927966 refactor: shared FactTypeFilter component, tabbed mental model dialogs use General tab, clean up labels 2026-03-19 16:20:03 +01:00
Nicolò Boschi 5005669497 feat: tabbed mental model dialogs (Basic / Options tabs) 2026-03-19 16:08:57 +01:00
Nicolò Boschi f404f31ae2 fix: add missing trigger fields to local MentalModel interface in mental-models-view 2026-03-19 14:16:56 +01:00
Nicolò Boschi 7e191ecd9f fix: add missing trigger fields to MentalModel type in control plane api.ts 2026-03-19 14:04:23 +01:00
Nicolò Boschi 1232c79652 feat: expose fact_types, exclude_mental_models, exclude_mental_model_ids in control plane UI 2026-03-19 13:51:16 +01:00
Nicolò Boschi b6dedd5e4b chore: merge main, fix lint formatting and update skills openapi.json 2026-03-19 13:00:05 +01:00
Nicolò Boschi 11d7a9a5a5 Merge remote-tracking branch 'origin/main' into reflect 2026-03-19 12:59:24 +01:00
Nicolò Boschi e5186129fb fix: filter hallucinated tool calls before trace to prevent disabled tools appearing in results 2026-03-19 12:41:53 +01:00
Nicolò Boschi f828ae46d9 fix: add missing ReflectRequest fields in Rust CLI struct initializers 2026-03-19 12:26:33 +01:00
Nicolò Boschi 00280e65bf fix: guard against disabled-tool hallucination and regenerate clients
- Add enabled_tools guard in reflect agent: if an LLM calls a tool that
  was excluded (e.g. recall when fact_types=["observation"]), return an
  error result instead of executing it
- Regenerate OpenAPI spec and all SDK clients (Go, Python, TypeScript)
  to include new fact_types / exclude_mental_models fields
2026-03-19 12:08:43 +01:00
Nicolò Boschi 226b0b5fba feat: add fact_types and mental model exclusion filters to reflect and mental models
Adds three new filtering options to both the reflect endpoint and mental model creation/refresh:

- `fact_types`: restrict which fact types (world, experience, observation) are retrieved.
  Disables irrelevant agent tools entirely (no wasted tokens).
- `exclude_mental_models`: skip the search_mental_models tool altogether.
- `exclude_mental_model_ids`: exclude specific mental models by ID (merged with the
  existing self-exclusion logic during mental model refresh).

For mental models, options are persisted in the existing `trigger` JSONB column so they
are automatically applied on every refresh. The `UpdateMentalModelRequest` already
proxies `trigger`, so no extra endpoint changes are needed.

Also fixes the test fixture (`pg0_db_url` in conftest.py) to correctly resolve pg0://
URLs and run migrations before tests, which was causing all DB-dependent tests to fail
with "relation public.banks does not exist" when HINDSIGHT_API_DATABASE_URL=pg0://uuuu.
2026-03-19 11:51:45 +01:00
23 changed files with 1393 additions and 220 deletions
@@ -669,6 +669,25 @@ class ReflectRequest(BaseModel):
description="Compound tag filter using boolean groups. Groups in the list are AND-ed. "
"Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.",
)
fact_types: list[Literal["world", "experience", "observation"]] | None = Field(
default=None,
description="Filter which fact types are retrieved during reflect. None means all types (world, experience, observation).",
)
exclude_mental_models: bool = Field(
default=False,
description="If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
)
exclude_mental_model_ids: list[str] | None = Field(
default=None,
description="Exclude specific mental models by ID from the reflect loop.",
)
@field_validator("fact_types")
@classmethod
def validate_reflect_fact_types(cls, v: list[str] | None) -> list[str] | None:
if v is not None and len(v) == 0:
raise ValueError("fact_types must not be empty. Use null to include all fact types.")
return v
@model_validator(mode="after")
def validate_tags_exclusive(self) -> "ReflectRequest":
@@ -1435,6 +1454,25 @@ class MentalModelTrigger(BaseModel):
default=False,
description="If true, refresh this mental model after observations consolidation (real-time mode)",
)
fact_types: list[Literal["world", "experience", "observation"]] | None = Field(
default=None,
description="Filter which fact types are retrieved during reflect. None means all types (world, experience, observation).",
)
exclude_mental_models: bool = Field(
default=False,
description="If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
)
exclude_mental_model_ids: list[str] | None = Field(
default=None,
description="Exclude specific mental models by ID from the reflect loop.",
)
@field_validator("fact_types")
@classmethod
def validate_fact_types(cls, v: list[str] | None) -> list[str] | None:
if v is not None and len(v) == 0:
raise ValueError("fact_types must not be empty. Use null to include all fact types.")
return v
class MentalModelResponse(BaseModel):
@@ -2505,6 +2543,9 @@ def _register_routes(app: FastAPI):
tags=request.tags,
tags_match=request.tags_match,
tag_groups=request.tag_groups,
fact_types=request.fact_types,
exclude_mental_models=request.exclude_mental_models,
exclude_mental_model_ids=request.exclude_mental_model_ids,
)
# Build based_on (memories + mental_models + directives) if facts are requested
@@ -868,14 +868,23 @@ class MemoryEngine(MemoryEngineInterface):
tags = mental_model.get("tags")
tags_match = "all_strict" if tags else "any"
# Read reflect options from trigger (if stored)
trigger_data = mental_model.get("trigger") or {}
fact_types = trigger_data.get("fact_types")
exclude_mental_models = trigger_data.get("exclude_mental_models", False)
stored_exclude_ids: list[str] = trigger_data.get("exclude_mental_model_ids") or []
# Run reflect to generate new content, excluding the mental model being refreshed
# Always add self to excluded IDs to prevent circular reference
reflect_result = await self.reflect_async(
bank_id=bank_id,
query=source_query,
request_context=internal_context,
tags=tags,
tags_match=tags_match,
exclude_mental_model_ids=[mental_model_id],
fact_types=fact_types,
exclude_mental_models=exclude_mental_models,
exclude_mental_model_ids=list({*stored_exclude_ids, mental_model_id}),
)
generated_content = reflect_result.text or "No content generated"
@@ -5113,6 +5122,8 @@ class MemoryEngine(MemoryEngineInterface):
tags_match: TagsMatch = "any",
tag_groups: list[TagGroup] | None = None,
exclude_mental_model_ids: list[str] | None = None,
fact_types: list[str] | None = None,
exclude_mental_models: bool = False,
_skip_span: bool = False,
) -> ReflectResult:
"""
@@ -5233,6 +5244,11 @@ class MemoryEngine(MemoryEngineInterface):
pending_consolidation=pending_consolidation,
)
# Determine which tools to enable based on fact_types and exclude_mental_models
include_observations = fact_types is None or "observation" in fact_types
recall_fact_types = [ft for ft in (fact_types or ["world", "experience"]) if ft in ("world", "experience")]
include_recall = bool(recall_fact_types)
async def recall_fn(q: str, max_tokens: int = 4096, max_chunk_tokens: int = 1000) -> dict[str, Any]:
return await tool_recall(
self,
@@ -5244,6 +5260,7 @@ class MemoryEngine(MemoryEngineInterface):
tags_match=tags_match,
tag_groups=tag_groups,
max_chunk_tokens=max_chunk_tokens,
fact_types=recall_fact_types if fact_types is not None else None,
)
async def expand_fn(memory_ids: list[str], depth: str) -> dict[str, Any]:
@@ -5266,15 +5283,17 @@ class MemoryEngine(MemoryEngineInterface):
if directives:
logger.info(f"[REFLECT {reflect_id}] Loaded {len(directives)} directives")
# Check if the bank has any mental models
async with pool.acquire() as conn:
mental_model_count = await conn.fetchval(
f"SELECT COUNT(*) FROM {fq_table('mental_models')} WHERE bank_id = $1",
bank_id,
)
has_mental_models = mental_model_count > 0
if has_mental_models:
logger.info(f"[REFLECT {reflect_id}] Bank has {mental_model_count} mental models")
# Check if the bank has any mental models (skip check if all mental models are excluded)
has_mental_models = False
if not exclude_mental_models:
async with pool.acquire() as conn:
mental_model_count = await conn.fetchval(
f"SELECT COUNT(*) FROM {fq_table('mental_models')} WHERE bank_id = $1",
bank_id,
)
has_mental_models = mental_model_count > 0
if has_mental_models:
logger.info(f"[REFLECT {reflect_id}] Bank has {mental_model_count} mental models")
# Run the agent with parent span for reflect operation (skip if called from another operation)
if not _skip_span:
@@ -5299,6 +5318,8 @@ class MemoryEngine(MemoryEngineInterface):
response_schema=response_schema,
directives=directives,
has_mental_models=has_mental_models,
include_observations=include_observations,
include_recall=include_recall,
budget=effective_budget,
max_context_tokens=max_context_tokens,
)
@@ -6430,6 +6451,12 @@ class MemoryEngine(MemoryEngineInterface):
tags = mental_model.get("tags")
tags_match = "all_strict" if tags else "any"
# Read reflect options from trigger (if stored)
trigger_data = mental_model.get("trigger") or {}
fact_types = trigger_data.get("fact_types")
exclude_mental_models = trigger_data.get("exclude_mental_models", False)
stored_exclude_ids: list[str] = trigger_data.get("exclude_mental_model_ids") or []
# Run reflect with the source query, excluding the mental model being refreshed
# Skip creating a nested "hindsight.reflect" span since we already have "hindsight.mental_model_refresh"
reflect_result = await self.reflect_async(
@@ -6438,7 +6465,9 @@ class MemoryEngine(MemoryEngineInterface):
request_context=request_context,
tags=tags,
tags_match=tags_match,
exclude_mental_model_ids=[mental_model_id],
fact_types=fact_types,
exclude_mental_models=exclude_mental_models,
exclude_mental_model_ids=list({*stored_exclude_ids, mental_model_id}),
_skip_span=True,
)
@@ -316,6 +316,8 @@ async def run_reflect_agent(
response_schema: dict | None = None,
directives: list[dict[str, Any]] | None = None,
has_mental_models: bool = False,
include_observations: bool = True,
include_recall: bool = True,
budget: str | None = None,
max_context_tokens: int = 100_000,
) -> ReflectAgentResult:
@@ -355,7 +357,14 @@ async def run_reflect_agent(
directive_rules = _extract_directive_rules(directives) if directives else None
# Get tools for this agent (with directive compliance field if directives exist)
tools = get_reflect_tools(directive_rules=directive_rules)
tools = get_reflect_tools(
directive_rules=directive_rules,
include_mental_models=has_mental_models,
include_observations=include_observations,
include_recall=include_recall,
)
# Build set of enabled tool names to guard against LLM hallucinating disabled tool calls
enabled_tools: frozenset[str] = frozenset(t["function"]["name"] for t in tools if t.get("type") == "function")
# Build initial messages (directives are injected into system prompt at START and END)
system_prompt = build_system_prompt_for_tools(
@@ -538,19 +547,18 @@ async def run_reflect_agent(
llm_start = time.time()
# Determine tool_choice for this iteration.
# Force the full hierarchical retrieval path before allowing auto:
# With mental models:
# 0 → search_mental_models, 1 → search_observations, 2 → recall, 3+ → auto
# Without mental models:
# 0 → search_observations, 1 → recall, 2+ → auto
if iteration == 0 and has_mental_models:
iter_tool_choice: str | dict = {"type": "function", "function": {"name": "search_mental_models"}}
elif iteration == 0:
iter_tool_choice = {"type": "function", "function": {"name": "search_observations"}}
elif iteration == 1 and has_mental_models:
iter_tool_choice = {"type": "function", "function": {"name": "search_observations"}}
elif iteration == 1 or (iteration == 2 and has_mental_models):
iter_tool_choice = {"type": "function", "function": {"name": "recall"}}
# Force the full hierarchical retrieval path (only for enabled tools) before allowing auto.
# Build the forced sequence from the tools that are actually enabled.
forced_sequence = []
if has_mental_models:
forced_sequence.append("search_mental_models")
if include_observations:
forced_sequence.append("search_observations")
if include_recall:
forced_sequence.append("recall")
if iteration < len(forced_sequence):
iter_tool_choice: str | dict = {"type": "function", "function": {"name": forced_sequence[iteration]}}
else:
iter_tool_choice = "auto"
@@ -769,7 +777,17 @@ async def run_reflect_agent(
# Execute other tools in parallel (exclude done tool in all its format variants)
other_tools = [tc for tc in result.tool_calls if not _is_done_tool(tc.name)]
if other_tools:
# Add assistant message with tool calls
# Partition into enabled vs hallucinated (not in enabled_tools set)
allowed_tools = []
hallucinated_tools = []
for tc in other_tools:
norm = _normalize_tool_name(tc.name)
if enabled_tools is not None and norm not in enabled_tools and norm not in ("done", "expand"):
hallucinated_tools.append(tc)
else:
allowed_tools.append(tc)
# Build assistant message with all tool calls (LLM requires them for history)
messages.append(
{
"role": "assistant",
@@ -777,6 +795,23 @@ async def run_reflect_agent(
}
)
# Immediately reject hallucinated tool calls without adding to trace
for tc in hallucinated_tools:
messages.append(
{
"role": "tool",
"tool_call_id": tc.id,
"name": tc.name,
"content": json.dumps(
{
"error": f"Tool '{_normalize_tool_name(tc.name)}' is not available. Use only the tools provided to you."
}
),
}
)
other_tools = allowed_tools
# Execute tools in parallel
tool_tasks = [
_execute_tool_with_timing(
@@ -785,6 +820,7 @@ async def run_reflect_agent(
search_observations_fn,
recall_fn,
expand_fn,
enabled_tools=enabled_tools,
)
for tc in other_tools
]
@@ -974,6 +1010,7 @@ async def _execute_tool_with_timing(
search_observations_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
recall_fn: Callable[[str, int, int], Awaitable[dict[str, Any]]],
expand_fn: Callable[[list[str], str], Awaitable[dict[str, Any]]],
enabled_tools: frozenset[str] | None = None,
) -> tuple[dict[str, Any], int]:
"""Execute a tool call and return result with timing."""
from hindsight_api.tracing import get_tracer
@@ -1007,6 +1044,7 @@ async def _execute_tool_with_timing(
search_observations_fn,
recall_fn,
expand_fn,
enabled_tools=enabled_tools,
)
# Set success attributes
@@ -1046,11 +1084,16 @@ async def _execute_tool(
search_observations_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
recall_fn: Callable[[str, int, int], Awaitable[dict[str, Any]]],
expand_fn: Callable[[list[str], str], Awaitable[dict[str, Any]]],
enabled_tools: frozenset[str] | None = None,
) -> dict[str, Any]:
"""Execute a single tool by name."""
# Normalize tool name for various LLM output formats
tool_name = _normalize_tool_name(tool_name)
# Guard against LLMs hallucinating calls to tools that were not provided
if enabled_tools is not None and tool_name not in enabled_tools and tool_name not in ("done", "expand"):
return {"error": f"Tool '{tool_name}' is not available. Use only the tools provided to you."}
if tool_name == "search_mental_models":
query = args.get("query")
if not query:
@@ -200,6 +200,7 @@ async def tool_recall(
tag_groups: "list | None" = None,
connection_budget: int = 1,
max_chunk_tokens: int = 1000,
fact_types: list[str] | None = None,
) -> dict[str, Any]:
"""
Search memories using TEMPR retrieval.
@@ -217,15 +218,18 @@ async def tool_recall(
tags_match: How to match tags - "any" (OR), "all" (AND), or "exact"
connection_budget: Max DB connections for this recall (default 1 for internal ops)
max_chunk_tokens: Maximum tokens for raw source chunk text (default 1000, always included)
fact_types: Optional filter for fact types to retrieve. Defaults to ["experience", "world"].
Returns:
Dict with list of matching memories including raw chunk text
"""
# Only world/experience are valid for raw recall (observation is handled by search_observations)
recall_fact_type = [ft for ft in (fact_types or ["experience", "world"]) if ft in ("world", "experience")]
include_chunks = True
result = await memory_engine.recall_async(
bank_id=bank_id,
query=query,
fact_type=["experience", "world"],
fact_type=recall_fact_type,
max_tokens=max_tokens,
enable_trace=False,
request_context=request_context,
@@ -227,7 +227,12 @@ def _build_done_tool_with_directives(directive_rules: list[str]) -> dict:
}
def get_reflect_tools(directive_rules: list[str] | None = None) -> list[dict]:
def get_reflect_tools(
directive_rules: list[str] | None = None,
include_mental_models: bool = True,
include_observations: bool = True,
include_recall: bool = True,
) -> list[dict]:
"""
Get the list of tools for the reflect agent.
@@ -239,16 +244,23 @@ def get_reflect_tools(directive_rules: list[str] | None = None) -> list[dict]:
Args:
directive_rules: Optional list of directive rule strings. If provided,
the done() tool will require directive compliance confirmation.
include_mental_models: Whether to include the search_mental_models tool.
include_observations: Whether to include the search_observations tool.
include_recall: Whether to include the recall tool.
Returns:
List of tool definitions in OpenAI format
"""
tools = [
TOOL_SEARCH_MENTAL_MODELS,
TOOL_SEARCH_OBSERVATIONS,
TOOL_RECALL,
TOOL_EXPAND,
]
tools = []
if include_mental_models:
tools.append(TOOL_SEARCH_MENTAL_MODELS)
if include_observations:
tools.append(TOOL_SEARCH_OBSERVATIONS)
if include_recall:
tools.append(TOOL_RECALL)
tools.append(TOOL_EXPAND)
# Use directive-aware done tool if directives are present
if directive_rules:
+20 -6
View File
@@ -48,7 +48,8 @@ def pg0_db_url(db_url, tmp_path_factory, worker_id):
Session-scoped fixture that ensures pg0 is running, migrations are applied,
and returns the database URL.
If HINDSIGHT_API_DATABASE_URL is set, uses that directly (no pg0 management).
If HINDSIGHT_API_DATABASE_URL is a plain postgresql:// URL, uses it directly.
If HINDSIGHT_API_DATABASE_URL is a pg0:// URL, resolves it to a real URL first.
Otherwise, starts pg0 once for the entire test session.
Uses filelock to ensure only one pytest-xdist worker starts pg0.
@@ -58,10 +59,23 @@ def pg0_db_url(db_url, tmp_path_factory, worker_id):
Note: We don't stop pg0 at the end because pytest-xdist runs workers in separate
processes that share the same pg0 instance. pg0 will persist for the next test run.
"""
if db_url:
# Use provided database URL directly
from hindsight_api.pg0 import parse_pg0_url as _parse_pg0_url
# Determine pg0 instance name/port from db_url (if it's a pg0:// URL) or use defaults
if db_url and not _parse_pg0_url(db_url)[0]:
# Plain postgresql:// URL - use it directly but still run migrations
from hindsight_api.migrations import run_migrations
run_migrations(db_url)
return db_url
if db_url:
_, pg0_name, pg0_port = _parse_pg0_url(db_url)
pg0_instance_name = pg0_name or DEFAULT_PG0_INSTANCE_NAME
pg0_instance_port = pg0_port or DEFAULT_PG0_PORT
else:
pg0_instance_name = DEFAULT_PG0_INSTANCE_NAME
pg0_instance_port = DEFAULT_PG0_PORT
# Get shared temp dir for coordination between xdist workers
if worker_id == "master":
# Running without xdist (-n 0 or no -n flag)
@@ -71,8 +85,8 @@ def pg0_db_url(db_url, tmp_path_factory, worker_id):
root_tmp_dir = tmp_path_factory.getbasetemp().parent
# Use a lock file to ensure only one worker starts pg0
lock_file = root_tmp_dir / "pg0_setup.lock"
url_file = root_tmp_dir / "pg0_url.txt"
lock_file = root_tmp_dir / f"pg0_setup_{pg0_instance_name}.lock"
url_file = root_tmp_dir / f"pg0_url_{pg0_instance_name}.txt"
with filelock.FileLock(str(lock_file)):
if url_file.exists():
@@ -80,7 +94,7 @@ def pg0_db_url(db_url, tmp_path_factory, worker_id):
url = url_file.read_text().strip()
else:
# First worker - start pg0
pg0 = EmbeddedPostgres(name=DEFAULT_PG0_INSTANCE_NAME, port=DEFAULT_PG0_PORT)
pg0 = EmbeddedPostgres(name=pg0_instance_name, port=pg0_instance_port)
# Run ensure_running in a new event loop
loop = asyncio.new_event_loop()
@@ -485,3 +485,206 @@ class TestReflectUsesMentalModels:
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
class TestMentalModelReflectOptions:
"""Tests for fact_types and exclude_mental_models options stored in the trigger field."""
@pytest.mark.asyncio
async def test_trigger_stores_fact_types(self, memory: MemoryEngine, request_context):
"""Trigger field persists fact_types and returns them via get_mental_model."""
bank_id = f"test-mm-trigger-ft-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
mm = await memory.create_mental_model(
bank_id=bank_id,
name="Observations only",
source_query="Summarize observations",
content="content",
trigger={"refresh_after_consolidation": False, "fact_types": ["observation"]},
request_context=request_context,
)
fetched = await memory.get_mental_model(bank_id=bank_id, mental_model_id=mm["id"], request_context=request_context)
assert fetched["trigger"]["fact_types"] == ["observation"]
assert fetched["trigger"]["refresh_after_consolidation"] is False
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_trigger_stores_exclude_mental_models(self, memory: MemoryEngine, request_context):
"""Trigger field persists exclude_mental_models flag."""
bank_id = f"test-mm-trigger-em-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
mm = await memory.create_mental_model(
bank_id=bank_id,
name="No mental models",
source_query="Summarize raw facts",
content="content",
trigger={"refresh_after_consolidation": False, "exclude_mental_models": True},
request_context=request_context,
)
fetched = await memory.get_mental_model(bank_id=bank_id, mental_model_id=mm["id"], request_context=request_context)
assert fetched["trigger"]["exclude_mental_models"] is True
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_trigger_stores_exclude_mental_model_ids(self, memory: MemoryEngine, request_context):
"""Trigger field persists exclude_mental_model_ids list."""
bank_id = f"test-mm-trigger-eid-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
excluded_ids = ["mm-abc", "mm-xyz"]
mm = await memory.create_mental_model(
bank_id=bank_id,
name="Exclude some models",
source_query="Summarize",
content="content",
trigger={"refresh_after_consolidation": False, "exclude_mental_model_ids": excluded_ids},
request_context=request_context,
)
fetched = await memory.get_mental_model(bank_id=bank_id, mental_model_id=mm["id"], request_context=request_context)
assert fetched["trigger"]["exclude_mental_model_ids"] == excluded_ids
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_trigger_reflect_options(self, memory: MemoryEngine, request_context):
"""update_mental_model persists updated trigger reflect options."""
bank_id = f"test-mm-trigger-upd-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
mm = await memory.create_mental_model(
bank_id=bank_id,
name="Initially no filter",
source_query="Summarize",
content="content",
trigger={"refresh_after_consolidation": False},
request_context=request_context,
)
updated = await memory.update_mental_model(
bank_id=bank_id,
mental_model_id=mm["id"],
trigger={
"refresh_after_consolidation": True,
"fact_types": ["world", "experience"],
"exclude_mental_models": False,
"exclude_mental_model_ids": ["mm-skip"],
},
request_context=request_context,
)
assert updated["trigger"]["refresh_after_consolidation"] is True
assert updated["trigger"]["fact_types"] == ["world", "experience"]
assert updated["trigger"]["exclude_mental_models"] is False
assert updated["trigger"]["exclude_mental_model_ids"] == ["mm-skip"]
await memory.delete_bank(bank_id, request_context=request_context)
class TestReflectFactTypeFiltering:
"""Tests for fact_types and exclude_mental_models filtering in reflect_async."""
@pytest.mark.asyncio
async def test_exclude_mental_models_skips_search_mental_models_tool(
self, memory: MemoryEngine, request_context
):
"""When exclude_mental_models=True, search_mental_models is never called."""
bank_id = f"test-reflect-exmm-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
# Create a mental model so the bank has one
await memory.create_mental_model(
bank_id=bank_id,
name="Existing Model",
source_query="Q",
content="Some content about the team",
request_context=request_context,
)
result = await memory.reflect_async(
bank_id=bank_id,
query="Tell me about the team",
request_context=request_context,
exclude_mental_models=True,
)
tool_names = [tc.tool for tc in result.tool_trace]
assert "search_mental_models" not in tool_names, (
f"search_mental_models should be excluded but found in: {tool_names}"
)
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_exclude_observations_via_fact_types(self, memory: MemoryEngine, request_context):
"""When fact_types excludes observation, search_observations is never called."""
bank_id = f"test-reflect-exobs-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
result = await memory.reflect_async(
bank_id=bank_id,
query="Tell me something",
request_context=request_context,
fact_types=["world", "experience"],
)
tool_names = [tc.tool for tc in result.tool_trace]
assert "search_observations" not in tool_names, (
f"search_observations should be excluded but found in: {tool_names}"
)
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_observation_only_fact_types_skips_recall(self, memory: MemoryEngine, request_context):
"""When fact_types=['observation'], recall is never called."""
bank_id = f"test-reflect-obsonly-{uuid.uuid4().hex[:8]}"
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
result = await memory.reflect_async(
bank_id=bank_id,
query="Tell me something",
request_context=request_context,
fact_types=["observation"],
)
tool_names = [tc.tool for tc in result.tool_trace]
assert "recall" not in tool_names, f"recall should be excluded but found in: {tool_names}"
await memory.delete_bank(bank_id, request_context=request_context)
class TestReflectRequestValidation:
"""Tests for ReflectRequest and MentalModelTrigger validation via the HTTP API."""
@pytest.mark.asyncio
async def test_reflect_empty_fact_types_rejected(self, api_client, test_bank_id):
"""Passing fact_types=[] to reflect must return 422."""
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/reflect",
json={"query": "test", "fact_types": []},
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_create_mental_model_empty_fact_types_rejected(self, api_client, test_bank_id):
"""Passing fact_types=[] inside trigger must return 422."""
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/mental-models",
json={
"name": "Test",
"source_query": "Q",
"trigger": {"refresh_after_consolidation": False, "fact_types": []},
},
)
assert response.status_code == 422
+3
View File
@@ -363,6 +363,9 @@ impl App {
tags: None,
tags_match: TagsMatch::Any,
tag_groups: None,
fact_types: None,
exclude_mental_models: false,
exclude_mental_model_ids: None,
};
let result = client.reflect(&bank_id, &request, false)
+3
View File
@@ -366,6 +366,9 @@ pub fn reflect(
tags: if tags.is_empty() { None } else { Some(tags) },
tags_match: parse_tags_match(&tags_match),
tag_groups: None,
fact_types: None,
exclude_mental_models: false,
exclude_mental_model_ids: None,
};
let response = client.reflect(agent_id, &request, verbose);
+68
View File
@@ -4074,6 +4074,13 @@ components:
id: id
trigger:
refresh_after_consolidation: false
fact_types:
- world
- world
exclude_mental_model_ids:
- exclude_mental_model_ids
- exclude_mental_model_ids
exclude_mental_models: false
last_refreshed_at: last_refreshed_at
content: content
tags:
@@ -4089,6 +4096,13 @@ components:
id: id
trigger:
refresh_after_consolidation: false
fact_types:
- world
- world
exclude_mental_model_ids:
- exclude_mental_model_ids
- exclude_mental_model_ids
exclude_mental_models: false
last_refreshed_at: last_refreshed_at
content: content
tags:
@@ -4115,6 +4129,13 @@ components:
id: id
trigger:
refresh_after_consolidation: false
fact_types:
- world
- world
exclude_mental_model_ids:
- exclude_mental_model_ids
- exclude_mental_model_ids
exclude_mental_models: false
last_refreshed_at: last_refreshed_at
content: content
tags:
@@ -4169,6 +4190,13 @@ components:
description: Trigger settings for a mental model.
example:
refresh_after_consolidation: false
fact_types:
- world
- world
exclude_mental_model_ids:
- exclude_mental_model_ids
- exclude_mental_model_ids
exclude_mental_models: false
properties:
refresh_after_consolidation:
default: false
@@ -4176,6 +4204,26 @@ components:
\ (real-time mode)"
title: Refresh After Consolidation
type: boolean
fact_types:
items:
enum:
- world
- experience
- observation
type: string
nullable: true
type: array
exclude_mental_models:
default: false
description: "If true, exclude all mental models from the reflect loop (skip\
\ search_mental_models tool)."
title: Exclude Mental Models
type: boolean
exclude_mental_model_ids:
items:
type: string
nullable: true
type: array
title: MentalModelTrigger
OperationResponse:
description: Response model for a single async operation.
@@ -4684,6 +4732,26 @@ components:
$ref: '#/components/schemas/RecallRequest_tag_groups_inner'
nullable: true
type: array
fact_types:
items:
enum:
- world
- experience
- observation
type: string
nullable: true
type: array
exclude_mental_models:
default: false
description: "If true, exclude all mental models from the reflect loop (skip\
\ search_mental_models tool)."
title: Exclude Mental Models
type: boolean
exclude_mental_model_ids:
items:
type: string
nullable: true
type: array
required:
- query
title: ReflectRequest
@@ -21,6 +21,10 @@ var _ MappedNullable = &MentalModelTrigger{}
type MentalModelTrigger struct {
// If true, refresh this mental model after observations consolidation (real-time mode)
RefreshAfterConsolidation *bool `json:"refresh_after_consolidation,omitempty"`
FactTypes []string `json:"fact_types,omitempty"`
// If true, exclude all mental models from the reflect loop (skip search_mental_models tool).
ExcludeMentalModels *bool `json:"exclude_mental_models,omitempty"`
ExcludeMentalModelIds []string `json:"exclude_mental_model_ids,omitempty"`
}
// NewMentalModelTrigger instantiates a new MentalModelTrigger object
@@ -31,6 +35,8 @@ func NewMentalModelTrigger() *MentalModelTrigger {
this := MentalModelTrigger{}
var refreshAfterConsolidation bool = false
this.RefreshAfterConsolidation = &refreshAfterConsolidation
var excludeMentalModels bool = false
this.ExcludeMentalModels = &excludeMentalModels
return &this
}
@@ -41,6 +47,8 @@ func NewMentalModelTriggerWithDefaults() *MentalModelTrigger {
this := MentalModelTrigger{}
var refreshAfterConsolidation bool = false
this.RefreshAfterConsolidation = &refreshAfterConsolidation
var excludeMentalModels bool = false
this.ExcludeMentalModels = &excludeMentalModels
return &this
}
@@ -76,6 +84,104 @@ func (o *MentalModelTrigger) SetRefreshAfterConsolidation(v bool) {
o.RefreshAfterConsolidation = &v
}
// GetFactTypes returns the FactTypes field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *MentalModelTrigger) GetFactTypes() []string {
if o == nil {
var ret []string
return ret
}
return o.FactTypes
}
// GetFactTypesOk returns a tuple with the FactTypes 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 *MentalModelTrigger) GetFactTypesOk() ([]string, bool) {
if o == nil || IsNil(o.FactTypes) {
return nil, false
}
return o.FactTypes, true
}
// HasFactTypes returns a boolean if a field has been set.
func (o *MentalModelTrigger) HasFactTypes() bool {
if o != nil && !IsNil(o.FactTypes) {
return true
}
return false
}
// SetFactTypes gets a reference to the given []string and assigns it to the FactTypes field.
func (o *MentalModelTrigger) SetFactTypes(v []string) {
o.FactTypes = v
}
// GetExcludeMentalModels returns the ExcludeMentalModels field value if set, zero value otherwise.
func (o *MentalModelTrigger) GetExcludeMentalModels() bool {
if o == nil || IsNil(o.ExcludeMentalModels) {
var ret bool
return ret
}
return *o.ExcludeMentalModels
}
// GetExcludeMentalModelsOk returns a tuple with the ExcludeMentalModels field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *MentalModelTrigger) GetExcludeMentalModelsOk() (*bool, bool) {
if o == nil || IsNil(o.ExcludeMentalModels) {
return nil, false
}
return o.ExcludeMentalModels, true
}
// HasExcludeMentalModels returns a boolean if a field has been set.
func (o *MentalModelTrigger) HasExcludeMentalModels() bool {
if o != nil && !IsNil(o.ExcludeMentalModels) {
return true
}
return false
}
// SetExcludeMentalModels gets a reference to the given bool and assigns it to the ExcludeMentalModels field.
func (o *MentalModelTrigger) SetExcludeMentalModels(v bool) {
o.ExcludeMentalModels = &v
}
// GetExcludeMentalModelIds returns the ExcludeMentalModelIds field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *MentalModelTrigger) GetExcludeMentalModelIds() []string {
if o == nil {
var ret []string
return ret
}
return o.ExcludeMentalModelIds
}
// GetExcludeMentalModelIdsOk returns a tuple with the ExcludeMentalModelIds 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 *MentalModelTrigger) GetExcludeMentalModelIdsOk() ([]string, bool) {
if o == nil || IsNil(o.ExcludeMentalModelIds) {
return nil, false
}
return o.ExcludeMentalModelIds, true
}
// HasExcludeMentalModelIds returns a boolean if a field has been set.
func (o *MentalModelTrigger) HasExcludeMentalModelIds() bool {
if o != nil && !IsNil(o.ExcludeMentalModelIds) {
return true
}
return false
}
// SetExcludeMentalModelIds gets a reference to the given []string and assigns it to the ExcludeMentalModelIds field.
func (o *MentalModelTrigger) SetExcludeMentalModelIds(v []string) {
o.ExcludeMentalModelIds = v
}
func (o MentalModelTrigger) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
@@ -89,6 +195,15 @@ func (o MentalModelTrigger) ToMap() (map[string]interface{}, error) {
if !IsNil(o.RefreshAfterConsolidation) {
toSerialize["refresh_after_consolidation"] = o.RefreshAfterConsolidation
}
if o.FactTypes != nil {
toSerialize["fact_types"] = o.FactTypes
}
if !IsNil(o.ExcludeMentalModels) {
toSerialize["exclude_mental_models"] = o.ExcludeMentalModels
}
if o.ExcludeMentalModelIds != nil {
toSerialize["exclude_mental_model_ids"] = o.ExcludeMentalModelIds
}
return toSerialize, nil
}
@@ -33,6 +33,10 @@ type ReflectRequest struct {
// How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
TagsMatch *string `json:"tags_match,omitempty"`
TagGroups []RecallRequestTagGroupsInner `json:"tag_groups,omitempty"`
FactTypes []string `json:"fact_types,omitempty"`
// If true, exclude all mental models from the reflect loop (skip search_mental_models tool).
ExcludeMentalModels *bool `json:"exclude_mental_models,omitempty"`
ExcludeMentalModelIds []string `json:"exclude_mental_model_ids,omitempty"`
}
type _ReflectRequest ReflectRequest
@@ -48,6 +52,8 @@ func NewReflectRequest(query string) *ReflectRequest {
this.MaxTokens = &maxTokens
var tagsMatch string = "any"
this.TagsMatch = &tagsMatch
var excludeMentalModels bool = false
this.ExcludeMentalModels = &excludeMentalModels
return &this
}
@@ -60,6 +66,8 @@ func NewReflectRequestWithDefaults() *ReflectRequest {
this.MaxTokens = &maxTokens
var tagsMatch string = "any"
this.TagsMatch = &tagsMatch
var excludeMentalModels bool = false
this.ExcludeMentalModels = &excludeMentalModels
return &this
}
@@ -356,6 +364,104 @@ func (o *ReflectRequest) SetTagGroups(v []RecallRequestTagGroupsInner) {
o.TagGroups = v
}
// GetFactTypes returns the FactTypes field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *ReflectRequest) GetFactTypes() []string {
if o == nil {
var ret []string
return ret
}
return o.FactTypes
}
// GetFactTypesOk returns a tuple with the FactTypes 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 *ReflectRequest) GetFactTypesOk() ([]string, bool) {
if o == nil || IsNil(o.FactTypes) {
return nil, false
}
return o.FactTypes, true
}
// HasFactTypes returns a boolean if a field has been set.
func (o *ReflectRequest) HasFactTypes() bool {
if o != nil && !IsNil(o.FactTypes) {
return true
}
return false
}
// SetFactTypes gets a reference to the given []string and assigns it to the FactTypes field.
func (o *ReflectRequest) SetFactTypes(v []string) {
o.FactTypes = v
}
// GetExcludeMentalModels returns the ExcludeMentalModels field value if set, zero value otherwise.
func (o *ReflectRequest) GetExcludeMentalModels() bool {
if o == nil || IsNil(o.ExcludeMentalModels) {
var ret bool
return ret
}
return *o.ExcludeMentalModels
}
// GetExcludeMentalModelsOk returns a tuple with the ExcludeMentalModels field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *ReflectRequest) GetExcludeMentalModelsOk() (*bool, bool) {
if o == nil || IsNil(o.ExcludeMentalModels) {
return nil, false
}
return o.ExcludeMentalModels, true
}
// HasExcludeMentalModels returns a boolean if a field has been set.
func (o *ReflectRequest) HasExcludeMentalModels() bool {
if o != nil && !IsNil(o.ExcludeMentalModels) {
return true
}
return false
}
// SetExcludeMentalModels gets a reference to the given bool and assigns it to the ExcludeMentalModels field.
func (o *ReflectRequest) SetExcludeMentalModels(v bool) {
o.ExcludeMentalModels = &v
}
// GetExcludeMentalModelIds returns the ExcludeMentalModelIds field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *ReflectRequest) GetExcludeMentalModelIds() []string {
if o == nil {
var ret []string
return ret
}
return o.ExcludeMentalModelIds
}
// GetExcludeMentalModelIdsOk returns a tuple with the ExcludeMentalModelIds 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 *ReflectRequest) GetExcludeMentalModelIdsOk() ([]string, bool) {
if o == nil || IsNil(o.ExcludeMentalModelIds) {
return nil, false
}
return o.ExcludeMentalModelIds, true
}
// HasExcludeMentalModelIds returns a boolean if a field has been set.
func (o *ReflectRequest) HasExcludeMentalModelIds() bool {
if o != nil && !IsNil(o.ExcludeMentalModelIds) {
return true
}
return false
}
// SetExcludeMentalModelIds gets a reference to the given []string and assigns it to the ExcludeMentalModelIds field.
func (o *ReflectRequest) SetExcludeMentalModelIds(v []string) {
o.ExcludeMentalModelIds = v
}
func (o ReflectRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
@@ -391,6 +497,15 @@ func (o ReflectRequest) ToMap() (map[string]interface{}, error) {
if o.TagGroups != nil {
toSerialize["tag_groups"] = o.TagGroups
}
if o.FactTypes != nil {
toSerialize["fact_types"] = o.FactTypes
}
if !IsNil(o.ExcludeMentalModels) {
toSerialize["exclude_mental_models"] = o.ExcludeMentalModels
}
if o.ExcludeMentalModelIds != nil {
toSerialize["exclude_mental_model_ids"] = o.ExcludeMentalModelIds
}
return toSerialize, nil
}
@@ -17,7 +17,7 @@ import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictBool
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
@@ -27,7 +27,21 @@ class MentalModelTrigger(BaseModel):
Trigger settings for a mental model.
""" # noqa: E501
refresh_after_consolidation: Optional[StrictBool] = Field(default=False, description="If true, refresh this mental model after observations consolidation (real-time mode)")
__properties: ClassVar[List[str]] = ["refresh_after_consolidation"]
fact_types: Optional[List[StrictStr]] = None
exclude_mental_models: Optional[StrictBool] = Field(default=False, description="If true, exclude all mental models from the reflect loop (skip search_mental_models tool).")
exclude_mental_model_ids: Optional[List[StrictStr]] = None
__properties: ClassVar[List[str]] = ["refresh_after_consolidation", "fact_types", "exclude_mental_models", "exclude_mental_model_ids"]
@field_validator('fact_types')
def fact_types_validate_enum(cls, value):
"""Validates the enum"""
if value is None:
return value
for i in value:
if i not in set(['world', 'experience', 'observation']):
raise ValueError("each list item must be one of ('world', 'experience', 'observation')")
return value
model_config = ConfigDict(
populate_by_name=True,
@@ -68,6 +82,16 @@ class MentalModelTrigger(BaseModel):
exclude=excluded_fields,
exclude_none=True,
)
# set to None if fact_types (nullable) is None
# and model_fields_set contains the field
if self.fact_types is None and "fact_types" in self.model_fields_set:
_dict['fact_types'] = None
# set to None if exclude_mental_model_ids (nullable) is None
# and model_fields_set contains the field
if self.exclude_mental_model_ids is None and "exclude_mental_model_ids" in self.model_fields_set:
_dict['exclude_mental_model_ids'] = None
return _dict
@classmethod
@@ -80,7 +104,10 @@ class MentalModelTrigger(BaseModel):
return cls.model_validate(obj)
_obj = cls.model_validate({
"refresh_after_consolidation": obj.get("refresh_after_consolidation") if obj.get("refresh_after_consolidation") is not None else False
"refresh_after_consolidation": obj.get("refresh_after_consolidation") if obj.get("refresh_after_consolidation") is not None else False,
"fact_types": obj.get("fact_types"),
"exclude_mental_models": obj.get("exclude_mental_models") if obj.get("exclude_mental_models") is not None else False,
"exclude_mental_model_ids": obj.get("exclude_mental_model_ids")
})
return _obj
@@ -17,7 +17,7 @@ import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, field_validator
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.budget import Budget
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
@@ -38,7 +38,10 @@ class ReflectRequest(BaseModel):
tags: Optional[List[StrictStr]] = None
tags_match: Optional[StrictStr] = Field(default='any', description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).")
tag_groups: Optional[List[RecallRequestTagGroupsInner]] = None
__properties: ClassVar[List[str]] = ["query", "budget", "context", "max_tokens", "include", "response_schema", "tags", "tags_match", "tag_groups"]
fact_types: Optional[List[StrictStr]] = None
exclude_mental_models: Optional[StrictBool] = Field(default=False, description="If true, exclude all mental models from the reflect loop (skip search_mental_models tool).")
exclude_mental_model_ids: Optional[List[StrictStr]] = None
__properties: ClassVar[List[str]] = ["query", "budget", "context", "max_tokens", "include", "response_schema", "tags", "tags_match", "tag_groups", "fact_types", "exclude_mental_models", "exclude_mental_model_ids"]
@field_validator('tags_match')
def tags_match_validate_enum(cls, value):
@@ -50,6 +53,17 @@ class ReflectRequest(BaseModel):
raise ValueError("must be one of enum values ('any', 'all', 'any_strict', 'all_strict')")
return value
@field_validator('fact_types')
def fact_types_validate_enum(cls, value):
"""Validates the enum"""
if value is None:
return value
for i in value:
if i not in set(['world', 'experience', 'observation']):
raise ValueError("each list item must be one of ('world', 'experience', 'observation')")
return value
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
@@ -119,6 +133,16 @@ class ReflectRequest(BaseModel):
if self.tag_groups is None and "tag_groups" in self.model_fields_set:
_dict['tag_groups'] = None
# set to None if fact_types (nullable) is None
# and model_fields_set contains the field
if self.fact_types is None and "fact_types" in self.model_fields_set:
_dict['fact_types'] = None
# set to None if exclude_mental_model_ids (nullable) is None
# and model_fields_set contains the field
if self.exclude_mental_model_ids is None and "exclude_mental_model_ids" in self.model_fields_set:
_dict['exclude_mental_model_ids'] = None
return _dict
@classmethod
@@ -139,7 +163,10 @@ class ReflectRequest(BaseModel):
"response_schema": obj.get("response_schema"),
"tags": obj.get("tags"),
"tags_match": obj.get("tags_match") if obj.get("tags_match") is not None else 'any',
"tag_groups": [RecallRequestTagGroupsInner.from_dict(_item) for _item in obj["tag_groups"]] if obj.get("tag_groups") is not None else None
"tag_groups": [RecallRequestTagGroupsInner.from_dict(_item) for _item in obj["tag_groups"]] if obj.get("tag_groups") is not None else None,
"fact_types": obj.get("fact_types"),
"exclude_mental_models": obj.get("exclude_mental_models") if obj.get("exclude_mental_models") is not None else False,
"exclude_mental_model_ids": obj.get("exclude_mental_model_ids")
})
return _obj
@@ -1331,6 +1331,24 @@ export type MentalModelTrigger = {
* If true, refresh this mental model after observations consolidation (real-time mode)
*/
refresh_after_consolidation?: boolean;
/**
* Fact Types
*
* Filter which fact types are retrieved during reflect. None means all types (world, experience, observation).
*/
fact_types?: Array<"world" | "experience" | "observation"> | null;
/**
* Exclude Mental Models
*
* If true, exclude all mental models from the reflect loop (skip search_mental_models tool).
*/
exclude_mental_models?: boolean;
/**
* Exclude Mental Model Ids
*
* Exclude specific mental models by ID from the reflect loop.
*/
exclude_mental_model_ids?: Array<string> | null;
};
/**
@@ -1825,6 +1843,24 @@ export type ReflectRequest = {
tag_groups?: Array<
TagGroupLeaf | TagGroupAnd | TagGroupOr | TagGroupNot
> | null;
/**
* Fact Types
*
* Filter which fact types are retrieved during reflect. None means all types (world, experience, observation).
*/
fact_types?: Array<"world" | "experience" | "observation"> | null;
/**
* Exclude Mental Models
*
* If true, exclude all mental models from the reflect loop (skip search_mental_models tool).
*/
exclude_mental_models?: boolean;
/**
* Exclude Mental Model Ids
*
* Exclude specific mental models by ID from the reflect loop.
*/
exclude_mental_model_ids?: Array<string> | null;
};
/**
@@ -14,6 +14,9 @@ export async function POST(request: NextRequest) {
tags,
tags_match,
max_tokens,
fact_types,
exclude_mental_models,
exclude_mental_model_ids,
} = body;
const requestBody: any = {
@@ -22,6 +25,9 @@ export async function POST(request: NextRequest) {
tags,
tags_match,
max_tokens: max_tokens || undefined,
fact_types: fact_types || undefined,
exclude_mental_models: exclude_mental_models || undefined,
exclude_mental_model_ids: exclude_mental_model_ids || undefined,
};
// Add include options if specified
@@ -0,0 +1,113 @@
"use client";
import { cn } from "@/lib/utils";
export type FactType = "world" | "experience" | "observation";
export const ALL_FACT_TYPES: FactType[] = ["world", "experience", "observation"];
const FACT_TYPE_CONFIG: Record<
FactType,
{ label: string; active: string; inactive: string; dot: string }
> = {
world: {
label: "World",
active: "bg-blue-500/15 text-blue-700 border-blue-400 dark:text-blue-300 dark:border-blue-500",
inactive:
"border-border text-muted-foreground hover:border-blue-300 hover:text-blue-600 dark:hover:text-blue-400",
dot: "bg-blue-500",
},
experience: {
label: "Experience",
active:
"bg-emerald-500/15 text-emerald-700 border-emerald-400 dark:text-emerald-300 dark:border-emerald-500",
inactive:
"border-border text-muted-foreground hover:border-emerald-300 hover:text-emerald-600 dark:hover:text-emerald-400",
dot: "bg-emerald-500",
},
observation: {
label: "Observation",
active:
"bg-amber-500/15 text-amber-700 border-amber-400 dark:text-amber-300 dark:border-amber-500",
inactive:
"border-border text-muted-foreground hover:border-amber-300 hover:text-amber-600 dark:hover:text-amber-400",
dot: "bg-amber-500",
},
};
function FactTypePill({
ft,
active,
onToggle,
}: {
ft: FactType;
active: boolean;
onToggle: () => void;
}) {
const cfg = FACT_TYPE_CONFIG[ft];
return (
<button
type="button"
onClick={onToggle}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-all",
active ? cfg.active : cfg.inactive
)}
>
<span
className={cn("h-1.5 w-1.5 rounded-full", active ? cfg.dot : "bg-muted-foreground/50")}
/>
{cfg.label}
</button>
);
}
/**
* Inline pill-toggle fact-type filter for filter bars.
* An empty selection means "all types included".
*/
export function FactTypeFilter({
value,
onChange,
label = "Fact types:",
}: {
value: FactType[];
onChange: (next: FactType[]) => void;
label?: string;
}) {
const toggle = (ft: FactType) =>
onChange(value.includes(ft) ? value.filter((f) => f !== ft) : [...value, ft]);
return (
<div className="flex items-center gap-2">
{label && <span className="text-sm font-medium text-muted-foreground">{label}</span>}
<div className="flex gap-1.5">
{ALL_FACT_TYPES.map((ft) => (
<FactTypePill key={ft} ft={ft} active={value.includes(ft)} onToggle={() => toggle(ft)} />
))}
</div>
</div>
);
}
/**
* Pill-toggle group for use inside forms/dialogs.
*/
export function FactTypeCheckboxGroup({
value,
onChange,
}: {
value: FactType[];
onChange: (next: FactType[]) => void;
}) {
const toggle = (ft: FactType) =>
onChange(value.includes(ft) ? value.filter((f) => f !== ft) : [...value, ft]);
return (
<div className="flex flex-wrap gap-1.5">
{ALL_FACT_TYPES.map((ft) => (
<FactTypePill key={ft} ft={ft} active={value.includes(ft)} onToggle={() => toggle(ft)} />
))}
</div>
);
}
@@ -8,6 +8,8 @@ import { useBank } from "@/lib/bank-context";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FactType, FactTypeCheckboxGroup } from "@/components/fact-type-filter";
import { toast } from "sonner";
import { Card, CardContent } from "@/components/ui/card";
import {
@@ -86,6 +88,9 @@ interface MentalModel {
max_tokens: number;
trigger: {
refresh_after_consolidation: boolean;
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
};
last_refreshed_at: string;
created_at: string;
@@ -593,6 +598,9 @@ function CreateMentalModelDialog({
maxTokens: "2048",
tags: "",
autoRefresh: false,
factTypes: [] as Array<"world" | "experience" | "observation">,
excludeMentalModels: false,
excludeMentalModelIds: "",
});
const handleCreate = async () => {
@@ -608,13 +616,23 @@ function CreateMentalModelDialog({
const maxTokens = parseInt(form.maxTokens) || 2048;
// Submit mental model creation - content will be generated in background
const excludeIds = form.excludeMentalModelIds
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
await client.createMentalModel(currentBank, {
id: form.id.trim() || undefined,
name: form.name.trim(),
source_query: form.sourceQuery.trim(),
tags: tags.length > 0 ? tags : undefined,
max_tokens: maxTokens,
trigger: { refresh_after_consolidation: form.autoRefresh },
trigger: {
refresh_after_consolidation: form.autoRefresh,
fact_types: form.factTypes.length > 0 ? form.factTypes : undefined,
exclude_mental_models: form.excludeMentalModels || undefined,
exclude_mental_model_ids: excludeIds.length > 0 ? excludeIds : undefined,
},
});
setForm({
@@ -624,6 +642,9 @@ function CreateMentalModelDialog({
maxTokens: "2048",
tags: "",
autoRefresh: false,
factTypes: [],
excludeMentalModels: false,
excludeMentalModelIds: "",
});
onCreated();
} catch (error) {
@@ -645,6 +666,9 @@ function CreateMentalModelDialog({
maxTokens: "2048",
tags: "",
autoRefresh: false,
factTypes: [],
excludeMentalModels: false,
excludeMentalModelIds: "",
});
onClose();
}
@@ -659,80 +683,111 @@ function CreateMentalModelDialog({
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
ID <span className="text-muted-foreground font-normal">(optional)</span>
</label>
<Input
value={form.id}
onChange={(e) => setForm({ ...form, id: e.target.value })}
placeholder="e.g., team-communication"
/>
<p className="text-xs text-muted-foreground">
Custom ID for the mental model. If not provided, a UUID will be generated.
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Name *</label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="e.g., Team Communication Preferences"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Source Query *</label>
<Input
value={form.sourceQuery}
onChange={(e) => setForm({ ...form, sourceQuery: e.target.value })}
placeholder="e.g., How does the team prefer to communicate?"
/>
<p className="text-xs text-muted-foreground">
This query will be run to generate the initial content, and re-run when you refresh.
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Max Tokens</label>
<Input
type="number"
value={form.maxTokens}
onChange={(e) => setForm({ ...form, maxTokens: e.target.value })}
placeholder="2048"
min="256"
max="8192"
/>
<p className="text-xs text-muted-foreground">
Maximum tokens for the generated response (256-8192).
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Tags <span className="text-muted-foreground font-normal">(optional)</span>
</label>
<Input
value={form.tags}
onChange={(e) => setForm({ ...form, tags: e.target.value })}
placeholder="e.g., project-x, team-alpha (comma-separated)"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="auto-refresh"
checked={form.autoRefresh}
onCheckedChange={(checked) => setForm({ ...form, autoRefresh: checked === true })}
/>
<label
htmlFor="auto-refresh"
className="text-sm font-medium text-foreground cursor-pointer"
>
Auto-refresh after consolidation
</label>
</div>
<p className="text-xs text-muted-foreground -mt-2 ml-6">
Automatically refresh this mental model when memories are consolidated.
</p>
</div>
<Tabs defaultValue="general" className="py-2">
<TabsList className="w-full">
<TabsTrigger value="general" className="flex-1">
General
</TabsTrigger>
<TabsTrigger value="options" className="flex-1">
Options
</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4 pt-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">ID</label>
<Input
value={form.id}
onChange={(e) => setForm({ ...form, id: e.target.value })}
placeholder="e.g., team-communication"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Name *</label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="e.g., Team Communication Preferences"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Source Query *</label>
<Input
value={form.sourceQuery}
onChange={(e) => setForm({ ...form, sourceQuery: e.target.value })}
placeholder="e.g., How does the team prefer to communicate?"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Max Tokens</label>
<Input
type="number"
value={form.maxTokens}
onChange={(e) => setForm({ ...form, maxTokens: e.target.value })}
placeholder="2048"
min="256"
max="8192"
/>
</div>
</TabsContent>
<TabsContent value="options" className="space-y-4 pt-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Tags</label>
<Input
value={form.tags}
onChange={(e) => setForm({ ...form, tags: e.target.value })}
placeholder="e.g., project-x, team-alpha (comma-separated)"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="auto-refresh"
checked={form.autoRefresh}
onCheckedChange={(checked) => setForm({ ...form, autoRefresh: checked === true })}
/>
<label
htmlFor="auto-refresh"
className="text-sm font-medium text-foreground cursor-pointer"
>
Auto-refresh after consolidation
</label>
</div>
<div className="space-y-3">
<label className="text-sm font-medium text-foreground">Fact Types</label>
<FactTypeCheckboxGroup
value={form.factTypes}
onChange={(v) => setForm({ ...form, factTypes: v as FactType[] })}
/>
<p className="text-xs text-muted-foreground">Leave empty to include all types.</p>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="exclude-mental-models"
checked={form.excludeMentalModels}
onCheckedChange={(checked) =>
setForm({ ...form, excludeMentalModels: checked === true })
}
/>
<label
htmlFor="exclude-mental-models"
className="text-sm font-medium text-foreground cursor-pointer"
>
Exclude all mental models
</label>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Exclude Mental Model IDs
</label>
<Input
value={form.excludeMentalModelIds}
onChange={(e) => setForm({ ...form, excludeMentalModelIds: e.target.value })}
placeholder="e.g., model-a, model-b (comma-separated)"
/>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={creating}>
@@ -776,6 +831,12 @@ function UpdateMentalModelDialog({
maxTokens: String(mentalModel.max_tokens || 2048),
tags: mentalModel.tags.join(", "),
autoRefresh: mentalModel.trigger?.refresh_after_consolidation || false,
factTypes:
(mentalModel.trigger?.fact_types as
| Array<"world" | "experience" | "observation">
| undefined) || [],
excludeMentalModels: mentalModel.trigger?.exclude_mental_models || false,
excludeMentalModelIds: (mentalModel.trigger?.exclude_mental_model_ids || []).join(", "),
});
// Reset form when mental model changes or dialog opens
@@ -787,6 +848,12 @@ function UpdateMentalModelDialog({
maxTokens: String(mentalModel.max_tokens || 2048),
tags: mentalModel.tags.join(", "),
autoRefresh: mentalModel.trigger?.refresh_after_consolidation || false,
factTypes:
(mentalModel.trigger?.fact_types as
| Array<"world" | "experience" | "observation">
| undefined) || [],
excludeMentalModels: mentalModel.trigger?.exclude_mental_models || false,
excludeMentalModelIds: (mentalModel.trigger?.exclude_mental_model_ids || []).join(", "),
});
}
}, [open, mentalModel]);
@@ -803,12 +870,22 @@ function UpdateMentalModelDialog({
const maxTokens = parseInt(form.maxTokens) || 2048;
const excludeIds = form.excludeMentalModelIds
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
const updated = await client.updateMentalModel(currentBank, mentalModel.id, {
name: form.name.trim(),
source_query: form.sourceQuery.trim(),
tags: tags.length > 0 ? tags : undefined,
max_tokens: maxTokens,
trigger: { refresh_after_consolidation: form.autoRefresh },
trigger: {
refresh_after_consolidation: form.autoRefresh,
fact_types: form.factTypes.length > 0 ? form.factTypes : undefined,
exclude_mental_models: form.excludeMentalModels || undefined,
exclude_mental_model_ids: excludeIds.length > 0 ? excludeIds : undefined,
},
});
onUpdated(updated);
@@ -830,72 +907,107 @@ function UpdateMentalModelDialog({
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">ID</label>
<Input value={mentalModel.id} disabled className="bg-muted" />
<p className="text-xs text-muted-foreground">ID cannot be changed after creation.</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Name *</label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="e.g., Team Communication Preferences"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Source Query *</label>
<Input
value={form.sourceQuery}
onChange={(e) => setForm({ ...form, sourceQuery: e.target.value })}
placeholder="e.g., How does the team prefer to communicate?"
/>
<p className="text-xs text-muted-foreground">
This query will be run to generate the initial content, and re-run when you refresh.
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Max Tokens</label>
<Input
type="number"
value={form.maxTokens}
onChange={(e) => setForm({ ...form, maxTokens: e.target.value })}
placeholder="2048"
min="256"
max="8192"
/>
<p className="text-xs text-muted-foreground">
Maximum tokens for the generated response (256-8192).
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Tags <span className="text-muted-foreground font-normal">(optional)</span>
</label>
<Input
value={form.tags}
onChange={(e) => setForm({ ...form, tags: e.target.value })}
placeholder="e.g., project-x, team-alpha (comma-separated)"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="update-auto-refresh"
checked={form.autoRefresh}
onCheckedChange={(checked) => setForm({ ...form, autoRefresh: checked === true })}
/>
<label
htmlFor="update-auto-refresh"
className="text-sm font-medium text-foreground cursor-pointer"
>
Auto-refresh after consolidation
</label>
</div>
<p className="text-xs text-muted-foreground -mt-2 ml-6">
Automatically refresh this mental model when memories are consolidated.
</p>
</div>
<Tabs defaultValue="general" className="py-2">
<TabsList className="w-full">
<TabsTrigger value="general" className="flex-1">
General
</TabsTrigger>
<TabsTrigger value="options" className="flex-1">
Options
</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4 pt-4">
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">ID</label>
<Input value={mentalModel.id} disabled className="bg-muted" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Name *</label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="e.g., Team Communication Preferences"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Source Query *</label>
<Input
value={form.sourceQuery}
onChange={(e) => setForm({ ...form, sourceQuery: e.target.value })}
placeholder="e.g., How does the team prefer to communicate?"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Max Tokens</label>
<Input
type="number"
value={form.maxTokens}
onChange={(e) => setForm({ ...form, maxTokens: e.target.value })}
placeholder="2048"
min="256"
max="8192"
/>
</div>
</TabsContent>
<TabsContent value="options" className="space-y-4 pt-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Tags</label>
<Input
value={form.tags}
onChange={(e) => setForm({ ...form, tags: e.target.value })}
placeholder="e.g., project-x, team-alpha (comma-separated)"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="update-auto-refresh"
checked={form.autoRefresh}
onCheckedChange={(checked) => setForm({ ...form, autoRefresh: checked === true })}
/>
<label
htmlFor="update-auto-refresh"
className="text-sm font-medium text-foreground cursor-pointer"
>
Auto-refresh after consolidation
</label>
</div>
<div className="space-y-3">
<label className="text-sm font-medium text-foreground">Fact Types</label>
<FactTypeCheckboxGroup
value={form.factTypes}
onChange={(v) => setForm({ ...form, factTypes: v as FactType[] })}
/>
<p className="text-xs text-muted-foreground">Leave empty to include all types.</p>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="update-exclude-mental-models"
checked={form.excludeMentalModels}
onCheckedChange={(checked) =>
setForm({ ...form, excludeMentalModels: checked === true })
}
/>
<label
htmlFor="update-exclude-mental-models"
className="text-sm font-medium text-foreground cursor-pointer"
>
Exclude all mental models
</label>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Exclude Mental Model IDs
</label>
<Input
value={form.excludeMentalModelIds}
onChange={(e) => setForm({ ...form, excludeMentalModelIds: e.target.value })}
placeholder="e.g., model-a, model-b (comma-separated)"
/>
</div>
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={updating}>
@@ -14,6 +14,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { FactType, FactTypeFilter } from "@/components/fact-type-filter";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -33,7 +34,6 @@ import JsonView from "react18-json-view";
import "react18-json-view/src/style.css";
import { MemoryDetailPanel } from "./memory-detail-panel";
type FactType = "world" | "experience" | "observation";
type Budget = "low" | "mid" | "high";
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
type ViewMode = "results" | "trace" | "json";
@@ -157,10 +157,6 @@ export function SearchDebugView() {
}
};
const toggleFactType = (ft: FactType) => {
setFactTypes((prev) => (prev.includes(ft) ? prev.filter((t) => t !== ft) : [...prev, ft]));
};
if (!currentBank) {
return (
<Card className="border-dashed">
@@ -197,28 +193,7 @@ export function SearchDebugView() {
{/* Filters */}
<div className="flex flex-wrap items-center gap-6 mt-4 pt-4 border-t">
{/* Fact Types */}
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-muted-foreground">Types:</span>
<div className="flex gap-3">
{(["world", "experience"] as FactType[]).map((ft) => (
<label key={ft} className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={factTypes.includes(ft)}
onCheckedChange={() => toggleFactType(ft)}
/>
<span className="text-sm capitalize">{ft}</span>
</label>
))}
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={factTypes.includes("observation")}
onCheckedChange={() => toggleFactType("observation")}
/>
<span className="text-sm">Observations</span>
</label>
</div>
</div>
<FactTypeFilter value={factTypes} onChange={setFactTypes} label="Types:" />
<div className="h-6 w-px bg-border" />
@@ -13,6 +13,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { FactType, FactTypeFilter } from "@/components/fact-type-filter";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Sparkles,
@@ -51,6 +52,9 @@ export function ThinkView() {
const [loading, setLoading] = useState(false);
const [tags, setTags] = useState("");
const [tagsMatch, setTagsMatch] = useState<TagsMatch>("any");
const [factTypes, setFactTypes] = useState<FactType[]>([]);
const [excludeMentalModels, setExcludeMentalModels] = useState(false);
const [excludeMentalModelIds, setExcludeMentalModelIds] = useState("");
const [feedback, setFeedback] = useState("");
const [feedbackSubmitting, setFeedbackSubmitting] = useState(false);
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
@@ -151,6 +155,11 @@ export function ThinkView() {
.map((t) => t.trim())
.filter((t) => t.length > 0);
const excludeIds = excludeMentalModelIds
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
const data: any = await client.reflect({
bank_id: currentBank,
query,
@@ -159,6 +168,9 @@ export function ThinkView() {
include_facts: includeFacts,
include_tool_calls: includeToolCalls,
...(parsedTags.length > 0 && { tags: parsedTags, tags_match: tagsMatch }),
...(factTypes.length > 0 && { fact_types: factTypes }),
...(excludeMentalModels && { exclude_mental_models: true }),
...(excludeIds.length > 0 && { exclude_mental_model_ids: excludeIds }),
});
setResult(data);
} catch (error) {
@@ -275,6 +287,29 @@ export function ThinkView() {
</SelectContent>
</Select>
</div>
{/* Fact Types & Mental Model Filters */}
<div className="flex flex-wrap items-center gap-6 mt-4 pt-4 border-t">
<FactTypeFilter value={factTypes} onChange={setFactTypes} />
<div className="h-6 w-px bg-border" />
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={excludeMentalModels}
onCheckedChange={(c) => setExcludeMentalModels(c as boolean)}
/>
<span className="text-sm">Exclude mental models</span>
</label>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Exclude IDs:</span>
<Input
type="text"
value={excludeMentalModelIds}
onChange={(e) => setExcludeMentalModelIds(e.target.value)}
placeholder="model-a, model-b"
className="h-8 w-48"
/>
</div>
</div>
</CardContent>
</Card>
+33 -5
View File
@@ -47,7 +47,12 @@ export interface MentalModel {
content: string;
tags: string[];
max_tokens: number;
trigger: { refresh_after_consolidation: boolean };
trigger: {
refresh_after_consolidation: boolean;
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
};
last_refreshed_at: string;
created_at: string;
reflect_response?: any;
@@ -183,6 +188,9 @@ export class ControlPlaneClient {
include_tool_calls?: boolean;
tags?: string[];
tags_match?: "any" | "all" | "any_strict" | "all_strict";
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
}) {
return this.fetchApi("/api/reflect", {
method: "POST",
@@ -757,7 +765,12 @@ export class ControlPlaneClient {
content: string;
tags: string[];
max_tokens: number;
trigger: { refresh_after_consolidation: boolean };
trigger: {
refresh_after_consolidation: boolean;
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
};
last_refreshed_at: string;
created_at: string;
reflect_response?: {
@@ -780,7 +793,12 @@ export class ControlPlaneClient {
source_query: string;
tags?: string[];
max_tokens?: number;
trigger?: { refresh_after_consolidation: boolean };
trigger?: {
refresh_after_consolidation: boolean;
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
};
}
) {
return this.fetchApi<{
@@ -809,7 +827,12 @@ export class ControlPlaneClient {
source_query?: string;
max_tokens?: number;
tags?: string[];
trigger?: { refresh_after_consolidation: boolean };
trigger?: {
refresh_after_consolidation: boolean;
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
};
}
) {
return this.fetchApi<{
@@ -820,7 +843,12 @@ export class ControlPlaneClient {
content: string;
tags: string[];
max_tokens: number;
trigger: { refresh_after_consolidation: boolean };
trigger: {
refresh_after_consolidation: boolean;
fact_types?: Array<"world" | "experience" | "observation">;
exclude_mental_models?: boolean;
exclude_mental_model_ids?: string[];
};
last_refreshed_at: string;
created_at: string;
reflect_response?: {
+82
View File
@@ -6299,6 +6299,47 @@
"title": "Refresh After Consolidation",
"description": "If true, refresh this mental model after observations consolidation (real-time mode)",
"default": false
},
"fact_types": {
"anyOf": [
{
"items": {
"type": "string",
"enum": [
"world",
"experience",
"observation"
]
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Fact Types",
"description": "Filter which fact types are retrieved during reflect. None means all types (world, experience, observation)."
},
"exclude_mental_models": {
"type": "boolean",
"title": "Exclude Mental Models",
"description": "If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
"default": false
},
"exclude_mental_model_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Exclude Mental Model Ids",
"description": "Exclude specific mental models by ID from the reflect loop."
}
},
"type": "object",
@@ -7299,6 +7340,47 @@
],
"title": "Tag Groups",
"description": "Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}."
},
"fact_types": {
"anyOf": [
{
"items": {
"type": "string",
"enum": [
"world",
"experience",
"observation"
]
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Fact Types",
"description": "Filter which fact types are retrieved during reflect. None means all types (world, experience, observation)."
},
"exclude_mental_models": {
"type": "boolean",
"title": "Exclude Mental Models",
"description": "If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
"default": false
},
"exclude_mental_model_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Exclude Mental Model Ids",
"description": "Exclude specific mental models by ID from the reflect loop."
}
},
"type": "object",
@@ -6299,6 +6299,47 @@
"title": "Refresh After Consolidation",
"description": "If true, refresh this mental model after observations consolidation (real-time mode)",
"default": false
},
"fact_types": {
"anyOf": [
{
"items": {
"type": "string",
"enum": [
"world",
"experience",
"observation"
]
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Fact Types",
"description": "Filter which fact types are retrieved during reflect. None means all types (world, experience, observation)."
},
"exclude_mental_models": {
"type": "boolean",
"title": "Exclude Mental Models",
"description": "If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
"default": false
},
"exclude_mental_model_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Exclude Mental Model Ids",
"description": "Exclude specific mental models by ID from the reflect loop."
}
},
"type": "object",
@@ -7299,6 +7340,47 @@
],
"title": "Tag Groups",
"description": "Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}."
},
"fact_types": {
"anyOf": [
{
"items": {
"type": "string",
"enum": [
"world",
"experience",
"observation"
]
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Fact Types",
"description": "Filter which fact types are retrieved during reflect. None means all types (world, experience, observation)."
},
"exclude_mental_models": {
"type": "boolean",
"title": "Exclude Mental Models",
"description": "If true, exclude all mental models from the reflect loop (skip search_mental_models tool).",
"default": false
},
"exclude_mental_model_ids": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Exclude Mental Model Ids",
"description": "Exclude specific mental models by ID from the reflect loop."
}
},
"type": "object",