Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ed5d169a7 | ||
|
|
3629b350af | ||
|
|
7788927966 | ||
|
|
5005669497 | ||
|
|
f404f31ae2 | ||
|
|
7e191ecd9f | ||
|
|
1232c79652 | ||
|
|
b6dedd5e4b | ||
|
|
11d7a9a5a5 | ||
|
|
e5186129fb | ||
|
|
f828ae46d9 | ||
|
|
00280e65bf | ||
|
|
226b0b5fba |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user