Foresight (Foresight v0.1.0)

Copy Markdown View Source

Semantic memory: retain what happened, recall what is relevant, reflect to answer.

This module is the public API. Every function takes a Foresight.Context naming the tenant, isolation mode and bank, so no call can touch memory without saying whose memory it is — there is no ambient state and no "current bank".

ctx = %Foresight.Context{tenant_id: "default", mode: :mode_a, bank: "notes"}

Foresight.retain(ctx, %{"items" => [%{"content" => "Maria leads the payments migration."}]})
Foresight.reflect(ctx, %{"query" => "who owns payments?"})

The three verbs

retain/3 is not an insert. It extracts discrete dated facts from what you give it, embeds them, and links entities into a graph. One paragraph usually becomes several independently retrievable facts, and it costs an LLM call.

recall/3 retrieves and ranks. It does not interpret. Use it when your own code decides what the results mean.

reflect/3 answers. A model searches memory itself, in a loop, choosing what to look for next from what it has already found. Use it when the answer has to be assembled from several places.

The gap between the last two is large: on the project benchmark, reflect scores 0.833 against a fixed reader's 0.467 over identical memories (McNemar p = 0.001).

Return values

Every function returns {:ok, result} or {:error, %Foresight.Error{}}. The error struct carries a :reason atom you can match on, so a missing capability is distinguishable from a bad request without parsing strings:

case Foresight.reflect(ctx, %{"query" => q}) do
  {:ok, answer} -> answer
  {:error, %Foresight.Error{reason: :unavailable}} -> :degraded
  {:error, %Foresight.Error{reason: :invalid_request, detail: d}} -> {:bad, d}
end

Guides

Start with Overview, then Getting started. How recall works explains the ranking and is worth reading before tuning anything.

Summary

Functions

Merge related facts into durable observations.

Retrieve memories relevant to a query, ranked.

Answer a question from memory, searching iteratively.

Store something that happened, extracting facts from it.

Functions

add_bank_background(ctx, bank_id, content, opts \\ [])

@spec add_bank_background(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

audit_log_stats(ctx, params \\ %{}, opts \\ [])

@spec audit_log_stats(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

bank_profile(ctx, bank_id, opts \\ [])

bank_stats(ctx, bank_id, opts \\ [])

cancel_operation(ctx, operation_id, opts \\ [])

@spec cancel_operation(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

clear_bank_memories(ctx, bank_id, params \\ %{}, opts \\ [])

@spec clear_bank_memories(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

clear_memory_observations(ctx, bank_id, memory_id, opts \\ [])

@spec clear_memory_observations(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  keyword()
) ::
  Foresight.Backend.result()

clear_observations(ctx, bank_id, opts \\ [])

@spec clear_observations(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

consolidate(ctx, payload \\ %{}, opts \\ [])

@spec consolidate(Foresight.Context.t(), term(), keyword()) ::
  Foresight.Backend.result()

Merge related facts into durable observations.

Runs during retain/3; call it directly to consolidate a bank on demand. Observations are what search_observations reads during reflect/3 — the difference between assembling an answer from twenty fragments and drawing it from one settled statement.

create_directive(ctx, bank_id, attrs, opts \\ [])

@spec create_directive(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

create_mental_model(ctx, bank_id, attrs, opts \\ [])

@spec create_mental_model(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

create_webhook(ctx, bank_id, attrs, opts \\ [])

@spec create_webhook(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

delete_bank(ctx, bank_id, opts \\ [])

delete_directive(ctx, bank_id, directive_id, opts \\ [])

@spec delete_directive(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

delete_document(ctx, bank_id, document_id, opts \\ [])

@spec delete_document(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

delete_mental_model(ctx, bank_id, mental_model_id, opts \\ [])

@spec delete_mental_model(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

delete_webhook(ctx, bank_id, webhook_id, opts \\ [])

@spec delete_webhook(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

entity_graph(ctx, params \\ %{}, opts \\ [])

@spec entity_graph(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

export_bank_template(ctx, bank_id, opts \\ [])

@spec export_bank_template(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

get_bank_config(ctx, bank_id, opts \\ [])

@spec get_bank_config(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

get_chunk(ctx, chunk_id, opts \\ [])

get_directive(ctx, bank_id, directive_id, opts \\ [])

get_document(ctx, document_id, opts \\ [])

get_entity(ctx, entity_id, opts \\ [])

get_memory(ctx, memory_id, opts \\ [])

get_memory_history(ctx, memory_id, opts \\ [])

@spec get_memory_history(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

get_mental_model(ctx, bank_id, mental_model_id, params \\ %{}, opts \\ [])

@spec get_mental_model(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  map(),
  keyword()
) ::
  Foresight.Backend.result()

get_mental_model_history(ctx, bank_id, mental_model_id, opts \\ [])

@spec get_mental_model_history(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  keyword()
) ::
  Foresight.Backend.result()

get_operation(ctx, operation_id, opts \\ [])

@spec get_operation(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

import_bank_template(ctx, bank_id, manifest, opts \\ [])

@spec import_bank_template(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_audit_logs(ctx, params \\ %{}, opts \\ [])

@spec list_audit_logs(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_banks(ctx, opts \\ [])

@spec list_banks(
  Foresight.Context.t(),
  keyword()
) :: Foresight.Backend.result()

list_directives(ctx, bank_id, params \\ %{}, opts \\ [])

@spec list_directives(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_document_chunks(ctx, document_id, params \\ %{}, opts \\ [])

@spec list_document_chunks(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_documents(ctx, params \\ %{}, opts \\ [])

@spec list_documents(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_entities(ctx, params \\ %{}, opts \\ [])

@spec list_entities(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_memories(ctx, params \\ %{}, opts \\ [])

@spec list_memories(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_mental_models(ctx, bank_id, params \\ %{}, opts \\ [])

@spec list_mental_models(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_operations(ctx, params \\ %{}, opts \\ [])

@spec list_operations(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

list_tags(ctx, params \\ %{}, opts \\ [])

list_webhook_deliveries(ctx, bank_id, webhook_id, params \\ %{}, opts \\ [])

@spec list_webhook_deliveries(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  map(),
  keyword()
) ::
  Foresight.Backend.result()

list_webhooks(ctx, bank_id, opts \\ [])

@spec list_webhooks(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

memories_timeseries(ctx, bank_id, period \\ "7d", time_field \\ "created_at", opts \\ [])

@spec memories_timeseries(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  String.t(),
  keyword()
) ::
  Foresight.Backend.result()

memory_graph(ctx, params \\ %{}, opts \\ [])

@spec memory_graph(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

migrate_bank(ctx, payload, opts \\ [])

@spec migrate_bank(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

not_implemented(operation)

@spec not_implemented(String.t()) :: {:error, Foresight.Error.t()}

patch_bank(ctx, bank_id, attrs, opts \\ [])

probe_bank_migration(ctx, payload, opts \\ [])

@spec probe_bank_migration(Foresight.Context.t(), map(), keyword()) ::
  Foresight.Backend.result()

put_bank(ctx, bank_id, attrs, opts \\ [])

recall(ctx, query, opts \\ [])

Retrieve memories relevant to a query, ranked.

{:ok, %{"results" => results}} = Foresight.recall(ctx, %{"query" => "payments migration"})

Four searches run concurrently — semantic, keyword, graph and temporal — and are merged into one ranking. See How recall works for the merge, which matters more than most of the searches.

Useful keys: "max_tokens" to budget the result set, "types" to restrict to e.g. ["observation"], and "trace" => true to get a full explanation of the ranking including per-stage timings_ms.

recover_consolidation(ctx, bank_id, opts \\ [])

@spec recover_consolidation(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

reflect(ctx, query, opts \\ [])

Answer a question from memory, searching iteratively.

{:ok, answer} = Foresight.reflect(ctx, %{"query" => "why did the migration slip?"})

Requires an LLM. The model is given tools — recall, search_observations, search_mental_models, expand — and decides for itself what to look for next.

Check trace["mode"] when the results disappoint: "agentic" means the loop ran, "fixed" means the configured model cannot call tools and Foresight fell back to a single-pass pipeline. The fallback is silent by design, which makes it easy to mistake for the loop underperforming. See Reflect.

refresh_mental_model(ctx, bank_id, mental_model_id, opts \\ [])

@spec refresh_mental_model(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

reprocess_document(ctx, bank_id, document_id, opts \\ [])

@spec reprocess_document(Foresight.Context.t(), String.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

reset_bank_config(ctx, bank_id, opts \\ [])

@spec reset_bank_config(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

retain(ctx, payload, opts \\ [])

Store something that happened, extracting facts from it.

payload carries an "items" list; each item needs "content" and may carry "context", "type", "tags" and "metadata".

Foresight.retain(ctx, %{
  "items" => [
    %{"content" => "Deployed v2.3 on Tuesday; the migration took 40 minutes.",
      "context" => "ops log",
      "tags" => ["deploy"]}
  ]
})

Pass "async" => true to queue the work instead of running it inline — this requires Oban, and returns an operation id you can poll with get_operation/3. Without Oban you get {:error, %Foresight.Error{reason: :unavailable}} rather than a crash.

Extraction quality dominates answer quality downstream. If answers are poor, inspect what was actually stored with list_memories/3 before tuning retrieval.

retain_files(ctx, payload, opts \\ [])

@spec retain_files(Foresight.Context.t(), term(), keyword()) ::
  Foresight.Backend.result()

retry_operation(ctx, operation_id, opts \\ [])

@spec retry_operation(Foresight.Context.t(), String.t(), keyword()) ::
  Foresight.Backend.result()

update_bank_config(ctx, bank_id, updates, opts \\ [])

@spec update_bank_config(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

update_bank_disposition(ctx, bank_id, disposition, opts \\ [])

@spec update_bank_disposition(Foresight.Context.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

update_directive(ctx, bank_id, directive_id, attrs, opts \\ [])

@spec update_directive(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  map(),
  keyword()
) ::
  Foresight.Backend.result()

update_document(ctx, bank_id, document_id, attrs, opts \\ [])

@spec update_document(Foresight.Context.t(), String.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()

update_mental_model(ctx, bank_id, mental_model_id, attrs, opts \\ [])

@spec update_mental_model(
  Foresight.Context.t(),
  String.t(),
  String.t(),
  map(),
  keyword()
) ::
  Foresight.Backend.result()

update_webhook(ctx, bank_id, webhook_id, attrs, opts \\ [])

@spec update_webhook(Foresight.Context.t(), String.t(), String.t(), map(), keyword()) ::
  Foresight.Backend.result()