8 Commits

Author SHA1 Message Date
Nicolò Boschi f5b3f76a8d fix(reflect): say when the structured-output extraction failed (#4230) (#4248)
Reflect with a `response_schema` runs a second LLM call that reshapes the prose
answer into the caller's schema. When that call errored or returned something
unparseable, the bare `except` swallowed it and the caller got 200 with
`structured_output: null` — indistinguishable from an answer that genuinely held
nothing matching the schema. The machine-readable half, which is the reason a
caller supplied a schema at all, failed invisibly: no retry signal, no alert.

Returning 200 with the text answer is still right; the missing piece was saying
the structured half did not happen. `StructuredOutputResult` now carries an
`error`, and reflect surfaces it as a nullable `structured_output_error` on the
response. Present => the extraction broke (retryable); absent with a null
`structured_output` => nothing to extract.

The mental-model refresh path uses the same helper and now records the reason in
its `structured_output_failed` failure detail instead of only "extraction
failed".
2026-09-09 12:59:00 +02:00
Sanderhoff-alt 82859af02b docs(reflect): clarify tag-scoped directive behavior (#3038)
Explain how tags, tags_match, tag_groups, and directive isolation
interact across REST, MCP, versioned docs, and agent skills.

Clarify the different defaults used by reflect and directive listing,
then regenerate OpenAPI and supported client artifacts.
2026-08-12 07:41:47 +02:00
Nicolò Boschi 4278f0989d feat(reflect,mental-models): surface structured output in the control plane (#3113)
* feat(reflect,mental-models): surface structured output in control plane

Reflect's response_schema -> structured_output was already implemented and
tested in the engine but never exposed in the UI. Surface it in the reflect
(think) view, and extend the same structured-output extraction to mental
models via a per-model response_schema stored in the trigger config.

- engine: refresh_mental_model reads trigger.response_schema, forwards it to
  the internal reflect call, and persists the parsed structured_output onto
  the stored reflect_response payload; fix stale 'not yet supported' docstrings
- api: add response_schema to MentalModelTrigger
- control-plane: reflect route + api.ts forward response_schema; think-view
  gets a JSON-schema input and renders structured_output; create/update mental
  model dialogs get a schema editor; detail modal renders structured_output
- tests: mental model structured-output plumbing (schema forwarded + persisted)
- regenerate OpenAPI spec + client SDKs; add i18n keys for all locales

* feat(control-plane): show configured response_schema in mental model config tab

Adds a read-only JSON card for the mental model's trigger.response_schema in
the detail modal's Configuration tab (mirrors the tag_groups card), plus the
regenerated go openapi.yaml.

* style: ruff format test_mental_model_structured_output

* fix(control-plane): don't route the JSON schema example through next-intl

The response_schema placeholder was a t() message whose value is literal JSON.
next-intl parses messages as ICU, so the '{' in the example was read as an
argument placeholder, the parse failed, and the field rendered the raw message
key instead of the example. Inline the JSON example directly on the placeholder
prop (i18n:check skips JSON-shaped placeholders) and drop the now-unused
*Placeholder message keys. Caught by running the control plane.

* feat(structured-output): validate response_schema + add a no-code schema builder

Validation (both reflect and mental models): a schema that is valid JSON but
not a usable object-with-properties silently produced empty structured_output
or blew up inside the LLM extraction call later. Now:
- engine: validate_response_schema() enforces the usable-shape contract
  (object schema, non-empty properties, well-formed required); wired as Pydantic
  field_validators on ReflectRequest.response_schema and
  MentalModelTrigger.response_schema (invalid -> HTTP 422).
- control-plane: the reflect and mental-model forms validate the schema shape on
  submit (not just JSON.parse) and surface the specific error.

No-code schema builder: a 'Build schema' button on both the reflect view and the
mental-model dialogs opens a dialog with Visual and Code modes. Visual mode edits
a flat field list (name, type, array item-type, description, required); Code mode
edits raw JSON. The two stay in sync and Apply is gated on a usable schema. Shared
frontend lib (response-schema.ts) mirrors the backend contract.

tests: test_response_schema_validation.py (16 cases: validator + model integration).

* refactor(control-plane): schema only via the builder, show set/unset status

Removes the inline response_schema JSON textarea from the reflect view and the
mental-model dialogs. Editing now happens exclusively in the schema builder; the
page shows only whether a schema is set (field count + names, with Edit/Remove)
or a Build schema button when none. Extracts the shared ResponseSchemaField
component used identically by reflect and both mental-model dialogs.

* fix(mental-models): derive structured_output from final content, not reflect's answer

In delta mode reflect only sees facts created since the last refresh, so its
answer (and any structured_output it derived) reflects just the delta — while the
stored content is the delta-merged document. Persisting the reflect-derived value
made structured_output inconsistent with the markdown.

Now the mental-model refresh no longer passes response_schema to reflect; instead
it extracts structured_output from the FINAL stored content (correct for both full
and delta), and carries the previous value forward untouched when a delta refresh
preserves content (no new facts). Adds a delta test asserting extraction runs
against the merged document, not reflect's partial answer.

* fix(schema-builder): allow switching an empty schema from Code back to Visual

An empty schema serialises to properties:{}, which schemaToFields mapped to an
empty array — and the Code->Visual guard treated 'empty' the same as 'not
representable', blocking the switch. schemaToFields now returns [] (representable)
for a missing/empty properties map and null only for genuinely unrepresentable
schemas; the switch seeds a blank field when empty.

* docs(reflect): document structured output (response_schema) + schema builder

Adds a Structured Output section to the reflect docs: how response_schema returns
both text and a structured_output projection of the same answer, the schema rules,
mental-model structured output (extracted from the final/merged document), and the
no-code Build schema editor. Regenerates the docs-skill mirror.

* feat(schema-builder): recursive visual editor for nested objects & arrays

The visual editor was flat — object/array fields had no way to define their inner
shape. Reworks the field model into a recursive tree (each field has a node; an
object node nests fields, an array node nests an item node) so you can build
nested objects and arrays-of-objects entirely in the visual editor. Code<->Visual
round-trips losslessly; schemas using features the editor can't represent (enum,
oneOf, $ref, tuple items, …) stay in code mode rather than being silently
flattened.

* fix(structured-output): recursive model for nested schemas + fail refresh loudly

Two problems surfaced by nested schemas on Gemini:

1. _generate_structured_output mapped object/array properties to bare dict/list,
   which serialize with additionalProperties — rejected by the Gemini API. So any
   schema with a nested object/array silently failed extraction. Now it builds a
   proper recursive Pydantic model (nested objects -> nested models, arrays ->
   typed lists), matching how retain's structured output already works on Gemini.

2. On extraction failure the mental-model refresh silently persisted content with
   no structured_output, clobbering the previously-stored value. Now, when a
   response_schema is configured and extraction yields nothing, the refresh raises
   MentalModelRefreshError — prior content and structured_output are preserved and
   the refresh can be retried.

Verified live on Gemini: a nested {location:object, people:array} schema now
extracts (structured_output present) instead of failing on additionalProperties.
Adds a fail-loud regression test.

* fix(schema-builder): readable error text in dark mode

text-destructive resolves to a dark red (#C0183A) in dark mode, which is
low-contrast on the dark dialog background. Use the codebase's standard
readable pattern (text-red-600 dark:text-red-400) for the builder's validation
error and the invalid-schema notice.

* fix(cli): set response_schema on MentalModelTriggerInput literals

Adding response_schema to MentalModelTrigger regenerated the Rust
MentalModelTriggerInput struct with a new field; the hand-written CLI struct
literals must initialize it (E0063). Sets response_schema: None in the three
construction sites (create/update mental model, knowledge-base pin).

* docs(api): document mental-model response_schema; fix stale reflect text-empty claim; test schema lib

- api/mental-models: document the trigger.response_schema flag + a Structured
  Output section (extraction from final content, fail-loud, validation).
- api/reflect: correct the stale claim that text is empty with response_schema —
  reflect returns both text and structured_output.
- control-plane: vitest unit tests for the response-schema lib (validation +
  recursive fields<->schema round-trip).
- regenerate docs-skill mirror.

* chore: regenerate bank-template-schema for MentalModelTrigger.response_schema

The bank template schema embeds MentalModelTrigger; adding response_schema to
the trigger changed the generated schema. Regenerated so verify-generated-files
passes.
2026-08-03 16:51:38 +02:00
Nicolò Boschi 6a460d2c9a feat(reflect): add apply_all_directives to bypass directive tag scoping (#3031) (#3046)
* feat(reflect): add apply_all_directives to bypass directive tag scoping (#3031)

Directives are tag-scoped like memories: a reflect with no tags loads only
untagged directives, and tagged directives apply only when the request's tags
match. This is deliberate (isolation_mode), but it means an operator's
tag-organized directives silently never reach an untagged reflect — 45% of
standing rules in the deployment reported in #3031.

Add an opt-in `apply_all_directives` flag on the reflect request (default
false, preserving current behavior). When true, every active directive is
loaded regardless of tags, ignoring tag scope. Wired through the HTTP API,
both MCP reflect variants, and the engine.

Also correct the docs, which claimed directives are "always" enforced without
mentioning tag scoping.

Regenerated OpenAPI, clients (Go/Python/TS/Rust), and the docs skill mirror;
updated the control-plane reflect proxy + api.ts types.

* chore(cli): record apply_all_directives CLI-coverage exemption

The reflect field is intentionally not exposed as a CLI flag (available via
the REST API, SDKs, and control plane). Record the exemption so cli-coverage-check
passes, matching the existing tag_groups entry.
2026-07-29 15:39:50 +02:00
Nicolò Boschi 2a32273226 feat: increase customization for reflect, retain and consolidation (#419) 2026-02-23 20:35:32 +01:00
Nicolò Boschi 90e370ef35 fix: misc fixes for observations and mental models (#209)
* fix: misc fixes for observations and mental models

* feat: improve graph retrieval for observations

- Update LinkExpansionRetriever to traverse through source_memory_ids
  for observation entity connections (avoiding data duplication)
- Remove entity link copy from world facts to observations in consolidator
- Add tests for link expansion graph retrieval
- Add directives_applied field to ReflectResult
- Include user's other changes (CLI, docs, client updates)

* fix: CI test failures

- Add mental_model_id parameter to create_mental_model function
- Fix ToolCallTrace not including reason field from ToolCall
- Improve test_link_expansion_observation_graph_retrieval to wait for consolidation with retry

* chore: reduce link expansion log verbosity

* Revert "chore: reduce link expansion log verbosity"

This reverts commit 3ce759391cead1012157785fa78fef16ef9bfe3b.

* feat: add semantic/temporal/entity links as fallback in graph retrieval

- Add fallback query for semantic, temporal, and entity links from memory_links
- Check both directions (outgoing and incoming links)
- Weight fallback results at 0.5x to prioritize entity links via unit_entities
- Fixes graph retrieval returning 0 when data has cross-cluster temporal connections

* fix: enable observations fixture for link expansion test

- Add enable_observations fixture to ensure observations are created
- Increase wait time from 10 to 30 seconds for CI reliability
2026-01-27 15:37:57 +01:00
Nicolò Boschi 5b52a84fff chore: internal renames (#204)
This commit renames the terminology across the entire codebase:
- "mental models" (fact_type='mental_model' in memory_units) → "observations"
- "reflections" table (stored reflect responses) → "mental_models"

Changes include:
- Database migration to rename tables, indexes, and constraints
- API endpoints: /reflections → /mental-models, /mental-models → /observations
- Config: ENABLE_MENTAL_MODELS → ENABLE_OBSERVATIONS
- Response models and Pydantic classes
- Reflect agent tools and prompts
- Control plane UI and routes
- Documentation and examples
- Regenerated OpenAPI spec and client SDKs (Python, TypeScript)
- Rust CLI: reflection commands → mental-model commands
- LiteLLM: updated fact_types documentation
2026-01-27 09:53:28 +01:00
Nicolò Boschi 522b71aab8 doc: mental models (#199)
* doc: mental models

* doc: mental models
2026-01-26 14:27:08 +01:00