Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0022d427d3 | ||
|
|
1f9bad0858 | ||
|
|
138bf02f29 | ||
|
|
df178aae8a | ||
|
|
a43026b8f4 | ||
|
|
5c425e276e |
@@ -11,11 +11,6 @@
|
||||
"name": "hindsight-memory",
|
||||
"description": "Automatic long-term memory for Claude Code via Hindsight",
|
||||
"source": "./hindsight-integrations/claude-code"
|
||||
},
|
||||
{
|
||||
"name": "hindsight-zcode",
|
||||
"description": "No-MCP long-term memory for ZCode via Hindsight hooks",
|
||||
"source": "./hindsight-integrations/zcode"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ jobs:
|
||||
integrations-github-copilot: ${{ steps.filter.outputs.integrations-github-copilot }}
|
||||
integrations-continue: ${{ steps.filter.outputs.integrations-continue }}
|
||||
integrations-cursor-cli: ${{ steps.filter.outputs.integrations-cursor-cli }}
|
||||
integrations-zcode: ${{ steps.filter.outputs.integrations-zcode }}
|
||||
integrations-crewai: ${{ steps.filter.outputs.integrations-crewai }}
|
||||
integrations-litellm: ${{ steps.filter.outputs.integrations-litellm }}
|
||||
integrations-pydantic-ai: ${{ steps.filter.outputs.integrations-pydantic-ai }}
|
||||
@@ -181,8 +180,6 @@ jobs:
|
||||
- 'hindsight-integrations/cursor/**'
|
||||
integrations-zed:
|
||||
- 'hindsight-integrations/zed/**'
|
||||
integrations-zcode:
|
||||
- 'hindsight-integrations/zcode/**'
|
||||
integrations-n8n:
|
||||
- 'hindsight-integrations/n8n/**'
|
||||
integrations-zapier:
|
||||
@@ -705,43 +702,6 @@ jobs:
|
||||
working-directory: ./hindsight-integrations/cursor-cli
|
||||
run: uv run pytest tests -v
|
||||
|
||||
test-zcode-integration:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
needs.detect-changes.outputs.integrations-zcode == 'true' ||
|
||||
needs.detect-changes.outputs.ci == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build zcode integration
|
||||
working-directory: ./hindsight-integrations/zcode
|
||||
run: uv build
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-integrations/zcode
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./hindsight-integrations/zcode
|
||||
run: uv run pytest tests -v
|
||||
|
||||
build-ai-sdk-integration:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
@@ -4949,7 +4909,6 @@ jobs:
|
||||
- test-github-copilot-integration
|
||||
- test-codex-integration
|
||||
- test-cursor-cli-integration
|
||||
- test-zcode-integration
|
||||
- build-ai-sdk-integration
|
||||
- test-ai-sdk-integration-deno
|
||||
- test-opencode-integration
|
||||
|
||||
@@ -56,6 +56,7 @@ BACKUP_TABLES = [
|
||||
"observation_history",
|
||||
"mental_models",
|
||||
"mental_model_history",
|
||||
"knowledge_pages",
|
||||
"directives",
|
||||
"async_operations",
|
||||
"webhooks",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Add managed flag to knowledge_pages.
|
||||
|
||||
The knowledge base is managed by clients (CRUD over folders/pages). ``managed``
|
||||
lets a client tag a node as system-owned vs. hand-authored; it carries no
|
||||
server-side behaviour.
|
||||
|
||||
Revision ID: a5b6c7d8e9f0
|
||||
Revises: a9b8c7d6e5f4
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "a5b6c7d8e9f0"
|
||||
down_revision: str | Sequence[str] | None = "a9b8c7d6e5f4"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}knowledge_pages ADD COLUMN IF NOT EXISTS managed BOOLEAN NOT NULL DEFAULT false")
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}knowledge_pages DROP COLUMN IF EXISTS managed")
|
||||
|
||||
|
||||
def _oracle_upgrade() -> None:
|
||||
op.execute("ALTER TABLE knowledge_pages ADD (managed NUMBER(1) DEFAULT 0 NOT NULL)")
|
||||
|
||||
|
||||
def _oracle_downgrade() -> None:
|
||||
op.execute("ALTER TABLE knowledge_pages DROP COLUMN managed")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade)
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Add knowledge_pages table (knowledge-base hierarchy).
|
||||
|
||||
The knowledge base organizes synthesized mental models into a navigable tree of
|
||||
**folders** and **pages**. A page references the mental model that holds its
|
||||
content (``mental_model_id``); a folder is a pure container (``mental_model_id``
|
||||
NULL). Hierarchy is a single self-referential ``parent_id`` so folders can nest
|
||||
arbitrarily. Content stays in ``mental_models`` — this table is metadata + tree
|
||||
structure only.
|
||||
|
||||
Revision ID: a9b8c7d6e5f4
|
||||
Revises: b57a7c9e0d13
|
||||
Create Date: 2026-06-25
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "a9b8c7d6e5f4"
|
||||
down_revision: str | Sequence[str] | None = "b57a7c9e0d13"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
"""Schema-qualifier for raw SQL on PG (multi-tenant search_path)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
# parent_id self-FK cascades so deleting a folder row removes its whole
|
||||
# subtree of rows in one shot. The mental_model FK is composite (matches the
|
||||
# mental_models (id, bank_id) PK) and cascades too, so deleting a page's
|
||||
# mental model removes the page row — folders skip the FK because a NULL
|
||||
# column in a composite FK is not enforced (MATCH SIMPLE).
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {schema}knowledge_pages (
|
||||
id VARCHAR(64) NOT NULL,
|
||||
bank_id TEXT NOT NULL,
|
||||
parent_id VARCHAR(64),
|
||||
kind VARCHAR(16) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
mental_model_id VARCHAR(64),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pk_knowledge_pages PRIMARY KEY (id),
|
||||
CONSTRAINT ck_knowledge_pages_kind CHECK (kind IN ('folder', 'page')),
|
||||
CONSTRAINT fk_kp_bank FOREIGN KEY (bank_id)
|
||||
REFERENCES {schema}banks(bank_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_parent FOREIGN KEY (parent_id)
|
||||
REFERENCES {schema}knowledge_pages(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_mm FOREIGN KEY (mental_model_id, bank_id)
|
||||
REFERENCES {schema}mental_models(id, bank_id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
f"CREATE INDEX IF NOT EXISTS idx_kp_bank_parent ON {schema}knowledge_pages (bank_id, parent_id, sort_order)"
|
||||
)
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"DROP INDEX IF EXISTS {schema}idx_kp_bank_parent")
|
||||
op.execute(f"DROP TABLE IF EXISTS {schema}knowledge_pages")
|
||||
|
||||
|
||||
def _oracle_upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS knowledge_pages (
|
||||
id VARCHAR2(64) NOT NULL,
|
||||
bank_id VARCHAR2(256) NOT NULL,
|
||||
parent_id VARCHAR2(64),
|
||||
kind VARCHAR2(16) NOT NULL,
|
||||
name CLOB NOT NULL,
|
||||
mental_model_id VARCHAR2(64),
|
||||
sort_order NUMBER DEFAULT 0 NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT pk_knowledge_pages PRIMARY KEY (id),
|
||||
CONSTRAINT ck_knowledge_pages_kind CHECK (kind IN ('folder', 'page')),
|
||||
CONSTRAINT fk_kp_bank FOREIGN KEY (bank_id)
|
||||
REFERENCES banks(bank_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_parent FOREIGN KEY (parent_id)
|
||||
REFERENCES knowledge_pages(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_kp_mm FOREIGN KEY (mental_model_id, bank_id)
|
||||
REFERENCES mental_models(id, bank_id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX idx_kp_bank_parent ON knowledge_pages (bank_id, parent_id, sort_order)")
|
||||
|
||||
|
||||
def _oracle_downgrade() -> None:
|
||||
op.execute("DROP TABLE knowledge_pages CASCADE CONSTRAINTS")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Unique page name per folder in knowledge_pages.
|
||||
|
||||
The folder curator can fire concurrently (folder-create trigger + the
|
||||
post-consolidation sweep), and an in-process lock can't serialize runs that
|
||||
execute in different threads/loops. A partial unique index on
|
||||
(bank_id, parent, lower(name)) for pages makes duplicate-named pages in the same
|
||||
folder impossible at the DB level — the second concurrent insert fails and the
|
||||
curator treats it as "already exists".
|
||||
|
||||
PostgreSQL only: the Oracle ``name`` column is a CLOB and cannot back a
|
||||
functional unique index; Oracle relies on the in-process serialization instead.
|
||||
|
||||
Revision ID: c3d4e5f6a7b8
|
||||
Revises: a5b6c7d8e9f0
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "c3d4e5f6a7b8"
|
||||
down_revision: str | Sequence[str] | None = "a5b6c7d8e9f0"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
# First drop any pre-existing duplicate pages (created by the racy curator
|
||||
# before this guard existed), keeping the earliest row of each duplicate set,
|
||||
# so the unique index can be built. Their backing mental models are left in
|
||||
# place (harmless orphans).
|
||||
op.execute(
|
||||
f"""
|
||||
DELETE FROM {schema}knowledge_pages a
|
||||
USING {schema}knowledge_pages b
|
||||
WHERE a.kind = 'page' AND b.kind = 'page'
|
||||
AND a.bank_id = b.bank_id
|
||||
AND COALESCE(a.parent_id, '') = COALESCE(b.parent_id, '')
|
||||
AND lower(a.name) = lower(b.name)
|
||||
AND a.ctid > b.ctid
|
||||
"""
|
||||
)
|
||||
# COALESCE(parent_id, '') so root-level pages (NULL parent) are also unique by
|
||||
# name — NULLs would otherwise compare distinct and allow duplicates.
|
||||
op.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_kp_folder_pagename "
|
||||
f"ON {schema}knowledge_pages (bank_id, COALESCE(parent_id, ''), lower(name)) "
|
||||
"WHERE kind = 'page'"
|
||||
)
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"DROP INDEX IF EXISTS {schema}uq_kp_folder_pagename")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade) # oracle slot intentionally absent (CLOB name)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade)
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Drop the search_vector column from the curation archive (invalidated_memory_units).
|
||||
|
||||
The archive is cold storage, never a recall surface, and carries no text-search
|
||||
index. Like ``embedding`` (dropped in d4f6a8c2e1b3), ``search_vector`` is a
|
||||
recall-surface column whose type follows the configured text-search backend, so
|
||||
it has no business living on the archive. Earlier curation code copied the live
|
||||
row's ``search_vector`` into ``invalidated_memory_units`` on invalidate; the
|
||||
engine now leaves it out on invalidate and recomputes it on revert, so the
|
||||
column is dead weight.
|
||||
|
||||
Dropping it removes a latent failure mode (#2503): under a non-native backend
|
||||
(pgroonga / pg_textsearch / pg_search / vchord) ``ensure_text_search_extension``
|
||||
reconciles ``memory_units.search_vector`` to ``text`` / ``bm25vector`` but never
|
||||
touched the archive, which the ``LIKE memory_units`` clone (c9a1b2d3e4f5) created
|
||||
as ``tsvector``. The type mismatch then broke the curation INSERT … SELECT
|
||||
round-trip:
|
||||
|
||||
column "search_vector" is of type tsvector but expression is of type text
|
||||
|
||||
With no column at all, there is nothing to mismatch. Unlike ``embedding`` (whose
|
||||
creation sites already omit it), the ``LIKE`` clone still adds ``search_vector``,
|
||||
so this migration does real work on both fresh and existing PostgreSQL databases.
|
||||
|
||||
DROP COLUMN is a metadata-only operation on both PostgreSQL and Oracle 23ai (no
|
||||
table rewrite), so it is cheap even across many tenant schemas. The downgrade
|
||||
re-adds an empty ``tsvector`` column (its original creation type).
|
||||
|
||||
Revision ID: e7c3a9f1b2d5
|
||||
Revises: b57a7c9e0d13
|
||||
Create Date: 2026-07-02
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
from hindsight_api.alembic._dialect import run_for_dialect
|
||||
|
||||
revision: str = "e7c3a9f1b2d5"
|
||||
down_revision: str | Sequence[str] | None = "b57a7c9e0d13"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _pg_schema_prefix() -> str:
|
||||
"""Schema-qualifier for raw SQL on PG (multi-tenant search_path)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def _pg_upgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}invalidated_memory_units DROP COLUMN IF EXISTS search_vector")
|
||||
|
||||
|
||||
def _pg_downgrade() -> None:
|
||||
schema = _pg_schema_prefix()
|
||||
# Re-add as the original tsvector creation type; comes back empty regardless.
|
||||
op.execute(f"ALTER TABLE {schema}invalidated_memory_units ADD COLUMN IF NOT EXISTS search_vector tsvector")
|
||||
|
||||
|
||||
def _oracle_upgrade() -> None:
|
||||
# Oracle has no `DROP COLUMN IF EXISTS`; swallow ORA-00904 (column does not
|
||||
# exist) so the migration is idempotent and safe on a schema whose baseline
|
||||
# may already omit the column.
|
||||
op.execute(
|
||||
"""
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'ALTER TABLE invalidated_memory_units DROP COLUMN search_vector';
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
IF SQLCODE != -904 THEN RAISE; END IF;
|
||||
END;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _oracle_downgrade() -> None:
|
||||
# Swallow ORA-01430 (column already exists) for idempotency. Oracle stores
|
||||
# search_vector as CLOB (see the Oracle baseline), so re-add it as CLOB.
|
||||
op.execute(
|
||||
"""
|
||||
BEGIN
|
||||
EXECUTE IMMEDIATE 'ALTER TABLE invalidated_memory_units ADD (search_vector CLOB)';
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
IF SQLCODE != -1430 THEN RAISE; END IF;
|
||||
END;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade)
|
||||
@@ -18,6 +18,7 @@ from typing import Any, Literal, TypeVar
|
||||
from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from hindsight_api.api import okf
|
||||
from hindsight_api.api.disconnect import ClientDisconnectCancellationMiddleware, get_scope_cancellation_token
|
||||
from hindsight_api.cancellation import OperationCancelledError
|
||||
from hindsight_api.engine.audit import (
|
||||
@@ -2110,6 +2111,150 @@ class MentalModelListResponse(BaseModel):
|
||||
items: list[MentalModelResponse]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# KNOWLEDGE BASE (folders + pages over mental models, projected to OKF)
|
||||
# =========================================================================
|
||||
|
||||
|
||||
class KnowledgeNode(BaseModel):
|
||||
"""A node in the knowledge-base tree — a folder or a page.
|
||||
|
||||
Pages carry ``description``/``tags`` from their backing mental model. The
|
||||
knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node
|
||||
as system-owned vs. hand-authored.
|
||||
"""
|
||||
|
||||
id: str
|
||||
kind: Literal["folder", "page"]
|
||||
name: str
|
||||
parent_id: str | None = None
|
||||
mental_model_id: str | None = Field(default=None, description="Backing mental model id (pages only).")
|
||||
managed: bool = Field(default=False, description="Client-set flag: true = system-owned, false = hand-authored.")
|
||||
description: str | None = Field(default=None, description="Page source query (OKF `description`).")
|
||||
tags: list[str] = FieldWithDefault(list)
|
||||
timestamp: str | None = Field(default=None, description="Last refresh (page) or last update (folder).")
|
||||
children: list["KnowledgeNode"] = FieldWithDefault(list)
|
||||
|
||||
|
||||
class KnowledgeTreeResponse(BaseModel):
|
||||
"""The knowledge base as a nested folder/page tree."""
|
||||
|
||||
roots: list[KnowledgeNode]
|
||||
|
||||
|
||||
class CreateFolderRequest(BaseModel):
|
||||
"""Create a folder under an optional parent folder."""
|
||||
|
||||
name: str
|
||||
parent_id: str | None = None
|
||||
|
||||
|
||||
class CreatePageRequest(BaseModel):
|
||||
"""Create a page (a mental model + tree node) under an optional parent folder."""
|
||||
|
||||
name: str
|
||||
source_query: str
|
||||
parent_id: str | None = None
|
||||
tags: list[str] | None = None
|
||||
max_tokens: int | None = None
|
||||
trigger: MentalModelTrigger | None = None
|
||||
|
||||
|
||||
class UpdateNodeRequest(BaseModel):
|
||||
"""Rename and/or move a node. Each field applies only when present."""
|
||||
|
||||
name: str | None = None
|
||||
parent_id: str | None = None
|
||||
|
||||
|
||||
class CreateKnowledgePageResponse(BaseModel):
|
||||
"""Result of creating a page: the node id, its mental model, and the refresh op."""
|
||||
|
||||
page_id: str
|
||||
mental_model_id: str
|
||||
operation_id: str | None = None
|
||||
|
||||
|
||||
class KnowledgePageResponse(BaseModel):
|
||||
"""A knowledge page rendered as an OKF document."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
type: str = Field(description="OKF document type — from a `type:<x>` tag, else 'knowledge-page'.")
|
||||
description: str | None = Field(default=None, description="The source query that rebuilds the page.")
|
||||
tags: list[str] = FieldWithDefault(list)
|
||||
timestamp: str | None = Field(default=None, description="Last refresh time (falls back to creation).")
|
||||
body: str | None = Field(default=None, description="The page's synthesized markdown body.")
|
||||
markdown: str = Field(description="The full OKF document: YAML frontmatter + markdown body.")
|
||||
|
||||
|
||||
class KnowledgePageGraphResponse(BaseModel):
|
||||
"""Constellation graph of knowledge pages linked by shared tags."""
|
||||
|
||||
nodes: list[dict[str, Any]]
|
||||
edges: list[dict[str, Any]]
|
||||
total_pages: int
|
||||
total_edges: int
|
||||
|
||||
|
||||
class KnowledgePageBundleFile(BaseModel):
|
||||
"""One file in a portable OKF bundle."""
|
||||
|
||||
path: str
|
||||
content: str
|
||||
|
||||
|
||||
class KnowledgePageBundleResponse(BaseModel):
|
||||
"""A portable OKF bundle — a flat set of markdown files (index + pages + logs)."""
|
||||
|
||||
files: list[KnowledgePageBundleFile]
|
||||
|
||||
|
||||
def _knowledge_node_model(node: dict[str, Any]) -> KnowledgeNode:
|
||||
"""Project an engine node dict into a (childless) KnowledgeNode."""
|
||||
is_page = node.get("kind") == "page"
|
||||
return KnowledgeNode(
|
||||
id=node["id"],
|
||||
kind=node["kind"],
|
||||
name=node["name"],
|
||||
parent_id=node.get("parent_id"),
|
||||
mental_model_id=node.get("mental_model_id"),
|
||||
managed=bool(node.get("managed")),
|
||||
description=node.get("source_query") if is_page else None,
|
||||
tags=list(node.get("tags") or []) if is_page else [],
|
||||
timestamp=(node.get("last_refreshed_at") if is_page else node.get("updated_at")),
|
||||
)
|
||||
|
||||
|
||||
def _build_knowledge_tree(nodes: list[dict[str, Any]]) -> list[KnowledgeNode]:
|
||||
"""Assemble the flat node list into a nested tree of roots."""
|
||||
models = {n["id"]: _knowledge_node_model(n) for n in nodes}
|
||||
roots: list[KnowledgeNode] = []
|
||||
for node in nodes:
|
||||
model = models[node["id"]]
|
||||
parent_id = node.get("parent_id")
|
||||
if parent_id and parent_id in models:
|
||||
models[parent_id].children.append(model)
|
||||
else:
|
||||
roots.append(model)
|
||||
return roots
|
||||
|
||||
|
||||
def _knowledge_page_response(node: dict[str, Any]) -> KnowledgePageResponse:
|
||||
"""Project a page node (with merged mental-model content) into an OKF document."""
|
||||
page = okf.page_type(node.get("tags"))
|
||||
return KnowledgePageResponse(
|
||||
id=node["id"],
|
||||
name=node["name"],
|
||||
type=page.type,
|
||||
description=node.get("source_query"),
|
||||
tags=page.display_tags,
|
||||
timestamp=node.get("last_refreshed_at") or node.get("created_at"),
|
||||
body=node.get("content"),
|
||||
markdown=okf.render_document(node),
|
||||
)
|
||||
|
||||
|
||||
class CreateMentalModelRequest(BaseModel):
|
||||
"""Request model for creating a mental model."""
|
||||
|
||||
@@ -4781,6 +4926,333 @@ def _register_routes(app: FastAPI):
|
||||
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# =========================================================================
|
||||
# KNOWLEDGE BASE ENDPOINTS (folders + pages, Open Knowledge Format)
|
||||
# =========================================================================
|
||||
# A hierarchy of folders and pages over mental models. Pages project to OKF
|
||||
# documents (markdown body + YAML frontmatter); see api/okf.py. The static
|
||||
# sub-paths (/tree, /folders, /pages, /graph, /export) are declared before
|
||||
# the /pages/{id} and /nodes/{id} path-parameter routes so they win.
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/tree",
|
||||
response_model=KnowledgeTreeResponse,
|
||||
summary="Get the knowledge-base tree",
|
||||
description="Return the knowledge base as a nested tree of folders and pages.",
|
||||
operation_id="get_knowledge_base_tree",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_knowledge_base_tree(
|
||||
bank_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Return the folder/page tree for a bank."""
|
||||
try:
|
||||
nodes = await app.state.memory.list_knowledge_nodes(bank_id=bank_id, request_context=request_context)
|
||||
return KnowledgeTreeResponse(roots=_build_knowledge_tree(nodes))
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/knowledge-base/tree: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/folders",
|
||||
response_model=KnowledgeNode,
|
||||
status_code=201,
|
||||
summary="Create a knowledge-base folder",
|
||||
description="Create a folder, optionally nested under a parent folder.",
|
||||
operation_id="create_knowledge_folder",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_create_knowledge_folder(
|
||||
bank_id: str,
|
||||
body: CreateFolderRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Create a folder node."""
|
||||
try:
|
||||
node = await app.state.memory.create_knowledge_folder(
|
||||
bank_id=bank_id,
|
||||
name=body.name,
|
||||
parent_id=body.parent_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
return _knowledge_node_model(node)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in POST /v1/default/banks/{bank_id}/knowledge-base/folders: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/pages",
|
||||
response_model=CreateKnowledgePageResponse,
|
||||
status_code=201,
|
||||
summary="Create a knowledge-base page",
|
||||
description="Create a page (a mental model + tree node). Content is generated asynchronously; "
|
||||
"use the returned operation_id to track completion.",
|
||||
operation_id="create_knowledge_page",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_create_knowledge_page(
|
||||
bank_id: str,
|
||||
body: CreatePageRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Create a page node (async content generation)."""
|
||||
try:
|
||||
node = await app.state.memory.create_knowledge_page(
|
||||
bank_id=bank_id,
|
||||
name=body.name,
|
||||
source_query=body.source_query,
|
||||
content="Generating content...",
|
||||
parent_id=body.parent_id,
|
||||
tags=body.tags if body.tags else None,
|
||||
max_tokens=body.max_tokens,
|
||||
trigger=body.trigger.model_dump() if body.trigger else None,
|
||||
request_context=request_context,
|
||||
)
|
||||
if node is None:
|
||||
raise HTTPException(status_code=409, detail=f"A page named '{body.name}' already exists in this folder")
|
||||
result = await app.state.memory.submit_async_refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=node["mental_model_id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
return CreateKnowledgePageResponse(
|
||||
page_id=node["id"],
|
||||
mental_model_id=node["mental_model_id"],
|
||||
operation_id=result["operation_id"],
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in POST /v1/default/banks/{bank_id}/knowledge-base/pages: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/graph",
|
||||
response_model=KnowledgePageGraphResponse,
|
||||
summary="Knowledge-base constellation graph",
|
||||
description="Return pages as nodes linked by shared tags, for the constellation view.",
|
||||
operation_id="get_knowledge_base_graph",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_knowledge_base_graph(
|
||||
bank_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Return the shared-tag constellation graph for a bank's pages."""
|
||||
try:
|
||||
nodes = await app.state.memory.list_knowledge_nodes(bank_id=bank_id, request_context=request_context)
|
||||
pages = [n for n in nodes if n.get("kind") == "page"]
|
||||
# Cluster the constellation by parent folder (the knowledge base's own
|
||||
# structure) rather than by the retired type: tag.
|
||||
folder_names = {n["id"]: n["name"] for n in nodes if n.get("kind") == "folder"}
|
||||
graph = okf.knowledge_graph(pages, cluster_for=lambda p: folder_names.get(p.get("parent_id"), "Ungrouped"))
|
||||
return KnowledgePageGraphResponse(
|
||||
nodes=graph.nodes,
|
||||
edges=graph.edges,
|
||||
total_pages=len(graph.nodes),
|
||||
total_edges=len(graph.edges),
|
||||
)
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/knowledge-base/graph: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/export",
|
||||
response_model=KnowledgePageBundleResponse,
|
||||
summary="Export the knowledge base as an OKF bundle",
|
||||
description="Return a portable OKF bundle: a nested index.md, one <id>.md per page, and history logs.",
|
||||
operation_id="export_knowledge_base",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_export_knowledge_base(
|
||||
bank_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Export a bank's knowledge base as a flat OKF markdown bundle."""
|
||||
try:
|
||||
nodes = await app.state.memory.list_knowledge_nodes(bank_id=bank_id, request_context=request_context)
|
||||
files = [KnowledgePageBundleFile(path=okf.INDEX_FILENAME, content=okf.render_index(nodes))]
|
||||
for node in nodes:
|
||||
if node.get("kind") != "page":
|
||||
continue
|
||||
page = await app.state.memory.get_knowledge_page(
|
||||
bank_id=bank_id, page_id=node["id"], request_context=request_context
|
||||
)
|
||||
if page is None:
|
||||
continue
|
||||
files.append(
|
||||
KnowledgePageBundleFile(path=okf.page_filename(node["id"]), content=okf.render_document(page))
|
||||
)
|
||||
if node.get("mental_model_id"):
|
||||
history = (
|
||||
await app.state.memory.get_mental_model_history(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=node["mental_model_id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
or []
|
||||
)
|
||||
if history:
|
||||
files.append(
|
||||
KnowledgePageBundleFile(
|
||||
path=okf.log_filename(node["id"]), content=okf.render_log(page, history)
|
||||
)
|
||||
)
|
||||
return KnowledgePageBundleResponse(files=files)
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/knowledge-base/export: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}",
|
||||
response_model=KnowledgePageResponse,
|
||||
summary="Get a knowledge-base page",
|
||||
description="Return a single page as an OKF document (frontmatter + markdown body).",
|
||||
operation_id="get_knowledge_page",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_get_knowledge_page(
|
||||
bank_id: str,
|
||||
page_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Get a single page as an OKF document."""
|
||||
try:
|
||||
node = await app.state.memory.get_knowledge_page(
|
||||
bank_id=bank_id, page_id=page_id, request_context=request_context
|
||||
)
|
||||
if node is None:
|
||||
raise HTTPException(status_code=404, detail=f"Knowledge page '{page_id}' not found")
|
||||
return _knowledge_page_response(node)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.patch(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}",
|
||||
response_model=KnowledgeNode,
|
||||
summary="Rename or move a knowledge-base node",
|
||||
description="Rename a node (set `name`) and/or move it under another folder (set `parent_id`, "
|
||||
"null for the root).",
|
||||
operation_id="update_knowledge_node",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_update_knowledge_node(
|
||||
bank_id: str,
|
||||
node_id: str,
|
||||
body: UpdateNodeRequest,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Rename and/or move a node."""
|
||||
try:
|
||||
updated: dict[str, Any] | None = None
|
||||
did_change = False
|
||||
if body.name is not None:
|
||||
did_change = True
|
||||
updated = await app.state.memory.rename_knowledge_node(
|
||||
bank_id=bank_id, node_id=node_id, name=body.name, request_context=request_context
|
||||
)
|
||||
# parent_id is applied only when present in the body, so passing null
|
||||
# moves the node to the root (distinct from "not provided").
|
||||
if "parent_id" in body.model_fields_set:
|
||||
did_change = True
|
||||
updated = await app.state.memory.move_knowledge_node(
|
||||
bank_id=bank_id, node_id=node_id, new_parent_id=body.parent_id, request_context=request_context
|
||||
)
|
||||
if not did_change:
|
||||
raise HTTPException(status_code=400, detail="Provide name and/or parent_id to update")
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=404, detail=f"Knowledge node '{node_id}' not found")
|
||||
return _knowledge_node_model(updated)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.delete(
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}",
|
||||
summary="Delete a knowledge-base node",
|
||||
description="Delete a folder or page and its whole subtree (pages' mental models are removed too).",
|
||||
operation_id="delete_knowledge_node",
|
||||
tags=["Knowledge Base"],
|
||||
)
|
||||
async def api_delete_knowledge_node(
|
||||
bank_id: str,
|
||||
node_id: str,
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
):
|
||||
"""Delete a node and its subtree."""
|
||||
try:
|
||||
deleted = await app.state.memory.delete_knowledge_node(
|
||||
bank_id=bank_id, node_id=node_id, request_context=request_context
|
||||
)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail=f"Knowledge node '{node_id}' not found")
|
||||
return {"status": "deleted"}
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# =========================================================================
|
||||
# DIRECTIVES ENDPOINTS
|
||||
# =========================================================================
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Open Knowledge Format (OKF) projection for knowledge pages.
|
||||
|
||||
Knowledge pages are a *read-only* OKF view over the existing mental models: each
|
||||
mental model is projected into an OKF document — a markdown body with YAML
|
||||
frontmatter (``type`` required; ``title``/``description``/``tags``/``timestamp``
|
||||
optional) — and pages are linked into a constellation graph via shared tags.
|
||||
|
||||
See the Open Knowledge Format spec:
|
||||
https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf
|
||||
|
||||
This module is intentionally pure: every function transforms the mental-model
|
||||
dicts returned by ``MemoryEngine.list_mental_models`` / ``get_mental_model`` and
|
||||
never touches the database. That keeps the OKF contract unit-testable without a
|
||||
DB or LLM and lets the HTTP layer stay a thin wrapper.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# OKF requires exactly one frontmatter field — ``type``. We default to this when
|
||||
# a page does not declare one via a ``type:<x>`` tag.
|
||||
DEFAULT_PAGE_TYPE = "knowledge-page"
|
||||
|
||||
# A page declares its OKF ``type`` through a tag of the form ``type:runbook``.
|
||||
# This keeps the projection schema-free (no new mental_models column): the type
|
||||
# is lifted from the existing tags array.
|
||||
TYPE_TAG_PREFIX = "type:"
|
||||
|
||||
INDEX_FILENAME = "index.md"
|
||||
|
||||
# Deterministic, colour-blind-friendly palette. Type → colour is stable across
|
||||
# requests so the constellation keeps the same colours between reloads.
|
||||
_PALETTE = (
|
||||
"#0074d9", # blue
|
||||
"#2ecc40", # green
|
||||
"#b10dc9", # purple
|
||||
"#ff851b", # orange
|
||||
"#39cccc", # teal
|
||||
"#f012be", # magenta
|
||||
"#3d9970", # olive
|
||||
"#ff4136", # red
|
||||
)
|
||||
|
||||
_EDGE_COLOR = "#9aa5b1"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageType:
|
||||
"""A page's OKF ``type`` and the tags that remain after the type tag is split off."""
|
||||
|
||||
type: str
|
||||
display_tags: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KnowledgeGraph:
|
||||
"""Cytoscape-style node/edge graph of knowledge pages linked by shared tags."""
|
||||
|
||||
nodes: list[dict[str, Any]] = field(default_factory=list)
|
||||
edges: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
def _color_for(key: str) -> str:
|
||||
"""Stable colour for a string key (FNV-ish hash into the fixed palette)."""
|
||||
h = 0
|
||||
for ch in key:
|
||||
h = (h * 31 + ord(ch)) & 0xFFFFFFFF
|
||||
return _PALETTE[h % len(_PALETTE)]
|
||||
|
||||
|
||||
def _scalar(value: Any) -> str:
|
||||
"""Emit a YAML-safe double-quoted scalar.
|
||||
|
||||
We always double-quote so arbitrary page names / source queries can't be
|
||||
misread as YAML special forms (``true``, ``2026-01-01``, ``- x``, etc.).
|
||||
"""
|
||||
text = str(value)
|
||||
escaped = text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def page_type(tags: list[str] | None) -> PageType:
|
||||
"""Split an OKF ``type`` out of the tag list.
|
||||
|
||||
The first ``type:<x>`` tag wins; all ``type:`` tags are removed from the
|
||||
returned ``display_tags`` so they don't pollute the constellation's
|
||||
shared-tag edges. Falls back to :data:`DEFAULT_PAGE_TYPE`.
|
||||
"""
|
||||
resolved = DEFAULT_PAGE_TYPE
|
||||
display: list[str] = []
|
||||
for tag in tags or []:
|
||||
if tag.startswith(TYPE_TAG_PREFIX):
|
||||
suffix = tag[len(TYPE_TAG_PREFIX) :].strip()
|
||||
if suffix and resolved == DEFAULT_PAGE_TYPE:
|
||||
resolved = suffix
|
||||
continue
|
||||
display.append(tag)
|
||||
return PageType(type=resolved, display_tags=display)
|
||||
|
||||
|
||||
def _timestamp(mm: dict[str, Any]) -> str | None:
|
||||
return mm.get("last_refreshed_at") or mm.get("created_at")
|
||||
|
||||
|
||||
def frontmatter(mm: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Build the ordered OKF frontmatter mapping for a mental model.
|
||||
|
||||
``None``/empty values are dropped by :func:`render_frontmatter`.
|
||||
"""
|
||||
pt = page_type(mm.get("tags"))
|
||||
return {
|
||||
"id": mm.get("id"),
|
||||
"type": pt.type,
|
||||
"title": mm.get("name"),
|
||||
"description": mm.get("source_query"),
|
||||
"tags": pt.display_tags,
|
||||
"timestamp": _timestamp(mm),
|
||||
}
|
||||
|
||||
|
||||
def render_frontmatter(fm: dict[str, Any]) -> str:
|
||||
"""Render a frontmatter mapping into a ``---`` fenced YAML block."""
|
||||
lines = ["---"]
|
||||
for key, value in fm.items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
if not value:
|
||||
continue
|
||||
lines.append(f"{key}:")
|
||||
lines.extend(f" - {_scalar(item)}" for item in value)
|
||||
else:
|
||||
lines.append(f"{key}: {_scalar(value)}")
|
||||
lines.append("---")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_document(mm: dict[str, Any]) -> str:
|
||||
"""Render a full OKF document: frontmatter block + markdown body."""
|
||||
body = (mm.get("content") or "").strip()
|
||||
return f"{render_frontmatter(frontmatter(mm))}\n\n{body}\n" if body else f"{render_frontmatter(frontmatter(mm))}\n"
|
||||
|
||||
|
||||
def page_filename(page_id: str) -> str:
|
||||
"""OKF bundle filename for a page id."""
|
||||
return f"{page_id}.md"
|
||||
|
||||
|
||||
def log_filename(page_id: str) -> str:
|
||||
"""OKF reserved per-page history filename."""
|
||||
return f"{page_id}.log.md"
|
||||
|
||||
|
||||
def render_index(nodes: list[dict[str, Any]]) -> str:
|
||||
"""Render the reserved ``index.md`` — nested OKF navigation over the tree.
|
||||
|
||||
``nodes`` is the flat folder/page list (each with ``id``, ``kind``, ``name``,
|
||||
``parent_id``); folders nest their children, pages link to their ``.md``.
|
||||
"""
|
||||
fm = render_frontmatter({"type": "index", "title": "Knowledge base"})
|
||||
lines = [fm, "", "# Knowledge base", ""]
|
||||
|
||||
children: dict[Any, list[dict[str, Any]]] = {}
|
||||
for node in nodes:
|
||||
children.setdefault(node.get("parent_id"), []).append(node)
|
||||
|
||||
def walk(parent: Any, depth: int) -> None:
|
||||
ordered = sorted(children.get(parent, []), key=lambda n: (n.get("sort_order", 0), n.get("name") or ""))
|
||||
for node in ordered:
|
||||
indent = " " * depth
|
||||
if node.get("kind") == "folder":
|
||||
lines.append(f"{indent}- **{node['name']}/**")
|
||||
walk(node["id"], depth + 1)
|
||||
else:
|
||||
description = node.get("source_query") or node.get("description")
|
||||
link = f"{indent}- [{node['name']}](./{page_filename(node['id'])})"
|
||||
lines.append(f"{link} — {description}" if description else link)
|
||||
|
||||
walk(None, 0)
|
||||
if len(lines) == 4:
|
||||
lines.append("_No knowledge pages yet._")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def render_log(mm: dict[str, Any], history: list[dict[str, Any]]) -> str:
|
||||
"""Render the reserved per-page ``log.md`` from refresh history.
|
||||
|
||||
Each history entry is ``{previous_content, previous_reflect_response,
|
||||
changed_at}`` (newest first), capturing the content *before* a refresh.
|
||||
"""
|
||||
name = mm.get("name") or mm.get("id")
|
||||
fm = render_frontmatter({"type": "log", "title": f"{name} — history"})
|
||||
lines = [fm, "", f"# {name} — history", ""]
|
||||
if not history:
|
||||
lines.append("_No refresh history._")
|
||||
return "\n".join(lines) + "\n"
|
||||
for entry in history:
|
||||
changed_at = entry.get("changed_at") or "unknown"
|
||||
previous = (entry.get("previous_content") or "").strip()
|
||||
lines.append(f"## {changed_at}")
|
||||
lines.append("")
|
||||
lines.append(previous if previous else "_(empty)_")
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def knowledge_graph(
|
||||
pages: list[dict[str, Any]],
|
||||
cluster_for: "Callable[[dict[str, Any]], str] | None" = None,
|
||||
) -> KnowledgeGraph:
|
||||
"""Derive the constellation graph: pages as nodes, shared tags as edges.
|
||||
|
||||
Two pages are linked when they share at least one (non-``type:``) tag; the
|
||||
edge weight is the number of shared tags. Each node's cluster (``type`` field
|
||||
+ colour) comes from ``cluster_for(page)`` — the knowledge base groups by
|
||||
parent folder; the default groups by OKF ``type``.
|
||||
"""
|
||||
nodes: list[dict[str, Any]] = []
|
||||
tag_sets: list[tuple[str, frozenset[str]]] = []
|
||||
for mm in pages:
|
||||
page_id = mm["id"]
|
||||
pt = page_type(mm.get("tags"))
|
||||
cluster = cluster_for(mm) if cluster_for else pt.type
|
||||
tag_sets.append((page_id, frozenset(pt.display_tags)))
|
||||
nodes.append(
|
||||
{
|
||||
"data": {
|
||||
"id": page_id,
|
||||
"label": mm.get("name") or page_id,
|
||||
"type": cluster,
|
||||
"tagCount": len(pt.display_tags),
|
||||
"color": _color_for(cluster),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
edges: list[dict[str, Any]] = []
|
||||
for i in range(len(tag_sets)):
|
||||
source_id, source_tags = tag_sets[i]
|
||||
if not source_tags:
|
||||
continue
|
||||
for j in range(i + 1, len(tag_sets)):
|
||||
target_id, target_tags = tag_sets[j]
|
||||
shared = source_tags & target_tags
|
||||
if not shared:
|
||||
continue
|
||||
edges.append(
|
||||
{
|
||||
"data": {
|
||||
"id": f"{source_id}--{target_id}",
|
||||
"source": source_id,
|
||||
"target": target_id,
|
||||
"sharedTags": sorted(shared),
|
||||
"weight": len(shared),
|
||||
"color": _EDGE_COLOR,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return KnowledgeGraph(nodes=nodes, edges=edges)
|
||||
@@ -9,33 +9,6 @@ from .ops import DataAccessOps, TagListingParts
|
||||
from .result import ResultRow
|
||||
|
||||
|
||||
def pg_search_vector_expr(
|
||||
config,
|
||||
*,
|
||||
text_col: str = "text",
|
||||
context_col: str = "context",
|
||||
signals_col: str = "text_signals",
|
||||
) -> str | None:
|
||||
"""SQL expression that builds ``search_vector`` for the configured PG text-search backend.
|
||||
|
||||
Single source of truth shared by the batch insert (over the ``input_data``
|
||||
CTE columns) and the curation revert recompute (over a ``memory_units`` row),
|
||||
so the two can never drift. Returns ``None`` for backends that leave
|
||||
``search_vector`` unpopulated — pgroonga / pg_textsearch / pg_search index the
|
||||
base text columns directly and keep only a dummy column, so there is nothing
|
||||
to build.
|
||||
|
||||
``text_search_extension_native_language`` is validated as a PG identifier in
|
||||
``HindsightConfig.validate()``, so embedding it as a SQL literal is safe.
|
||||
"""
|
||||
combined = f"COALESCE({text_col}, '') || ' ' || COALESCE({context_col}, '') || ' ' || COALESCE({signals_col}, '')"
|
||||
if config.text_search_extension == "vchord":
|
||||
return f"tokenize({combined}, 'llmlingua2')::bm25_catalog.bm25vector"
|
||||
if config.text_search_extension == "native":
|
||||
return f"to_tsvector('{config.text_search_extension_native_language}'::regconfig, {combined})"
|
||||
return None
|
||||
|
||||
|
||||
class PostgreSQLOps(DataAccessOps):
|
||||
"""PostgreSQL-specific data access operations using unnest and LATERAL."""
|
||||
|
||||
@@ -120,39 +93,101 @@ class PostgreSQLOps(DataAccessOps):
|
||||
config = get_config()
|
||||
table = self._get_mu_table()
|
||||
|
||||
# search_vector is populated inline for backends that store a real vector
|
||||
# (native tsvector, vchord bm25vector). pgroonga / pg_textsearch / pg_search
|
||||
# index the base text columns directly and keep only a dummy column, so the
|
||||
# expression is None and the column is left out of the insert entirely.
|
||||
# Same expression is reused by curation revert (see pg_search_vector_expr).
|
||||
sv_expr = pg_search_vector_expr(config)
|
||||
sv_insert_col = ", search_vector" if sv_expr else ""
|
||||
sv_select_val = f",\n {sv_expr}" if sv_expr else ""
|
||||
query = f"""
|
||||
WITH input_data AS (
|
||||
SELECT * FROM unnest(
|
||||
$2::text[], $3::vector[], $4::timestamptz[], $5::timestamptz[], $6::timestamptz[], $7::timestamptz[],
|
||||
$8::text[], $9::text[], $10::jsonb[], $11::text[], $12::text[], $13::jsonb[], $14::jsonb[], $15::text[]
|
||||
) AS t(text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags_json,
|
||||
observation_scopes_json, text_signals)
|
||||
)
|
||||
INSERT INTO {table} (bank_id, text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags,
|
||||
observation_scopes, text_signals{sv_insert_col})
|
||||
SELECT
|
||||
$1,
|
||||
text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id,
|
||||
COALESCE(
|
||||
(SELECT array_agg(elem) FROM jsonb_array_elements_text(tags_json) AS elem),
|
||||
'{{}}'::varchar[]
|
||||
),
|
||||
observation_scopes_json,
|
||||
text_signals{sv_select_val}
|
||||
FROM input_data
|
||||
RETURNING id
|
||||
"""
|
||||
if config.text_search_extension == "vchord":
|
||||
query = f"""
|
||||
WITH input_data AS (
|
||||
SELECT * FROM unnest(
|
||||
$2::text[], $3::vector[], $4::timestamptz[], $5::timestamptz[], $6::timestamptz[], $7::timestamptz[],
|
||||
$8::text[], $9::text[], $10::jsonb[], $11::text[], $12::text[], $13::jsonb[], $14::jsonb[], $15::text[]
|
||||
) AS t(text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags_json,
|
||||
observation_scopes_json, text_signals)
|
||||
)
|
||||
INSERT INTO {table} (bank_id, text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags,
|
||||
observation_scopes, text_signals, search_vector)
|
||||
SELECT
|
||||
$1,
|
||||
text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id,
|
||||
COALESCE(
|
||||
(SELECT array_agg(elem) FROM jsonb_array_elements_text(tags_json) AS elem),
|
||||
'{{}}'::varchar[]
|
||||
),
|
||||
observation_scopes_json,
|
||||
text_signals,
|
||||
tokenize(
|
||||
COALESCE(text, '') || ' ' || COALESCE(context, '') || ' ' || COALESCE(text_signals, ''),
|
||||
'llmlingua2'
|
||||
)::bm25_catalog.bm25vector
|
||||
FROM input_data
|
||||
RETURNING id
|
||||
"""
|
||||
elif config.text_search_extension == "native":
|
||||
# search_vector is a regular tsvector column populated here using the
|
||||
# configured native dictionary. It used to be GENERATED ALWAYS with
|
||||
# a hardcoded 'english', which prevented per-deployment language
|
||||
# configuration. text_search_extension_native_language is validated
|
||||
# in HindsightConfig.validate() as a PG identifier, so embedding it
|
||||
# as a SQL literal is safe.
|
||||
query = f"""
|
||||
WITH input_data AS (
|
||||
SELECT * FROM unnest(
|
||||
$2::text[], $3::vector[], $4::timestamptz[], $5::timestamptz[], $6::timestamptz[], $7::timestamptz[],
|
||||
$8::text[], $9::text[], $10::jsonb[], $11::text[], $12::text[], $13::jsonb[], $14::jsonb[], $15::text[]
|
||||
) AS t(text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags_json,
|
||||
observation_scopes_json, text_signals)
|
||||
)
|
||||
INSERT INTO {table} (bank_id, text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags,
|
||||
observation_scopes, text_signals, search_vector)
|
||||
SELECT
|
||||
$1,
|
||||
text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id,
|
||||
COALESCE(
|
||||
(SELECT array_agg(elem) FROM jsonb_array_elements_text(tags_json) AS elem),
|
||||
'{{}}'::varchar[]
|
||||
),
|
||||
observation_scopes_json,
|
||||
text_signals,
|
||||
to_tsvector(
|
||||
'{config.text_search_extension_native_language}'::regconfig,
|
||||
COALESCE(text, '') || ' ' || COALESCE(context, '') || ' ' || COALESCE(text_signals, '')
|
||||
)
|
||||
FROM input_data
|
||||
RETURNING id
|
||||
"""
|
||||
else:
|
||||
# pg_textsearch, pgroonga, and pg_search: search_vector is a dummy
|
||||
# TEXT column; the actual full-text index operates on the base text
|
||||
# columns directly, so we don't populate search_vector at insert time.
|
||||
query = f"""
|
||||
WITH input_data AS (
|
||||
SELECT * FROM unnest(
|
||||
$2::text[], $3::vector[], $4::timestamptz[], $5::timestamptz[], $6::timestamptz[], $7::timestamptz[],
|
||||
$8::text[], $9::text[], $10::jsonb[], $11::text[], $12::text[], $13::jsonb[], $14::jsonb[], $15::text[]
|
||||
) AS t(text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags_json,
|
||||
observation_scopes_json, text_signals)
|
||||
)
|
||||
INSERT INTO {table} (bank_id, text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id, tags,
|
||||
observation_scopes, text_signals)
|
||||
SELECT
|
||||
$1,
|
||||
text, embedding, event_date, occurred_start, occurred_end, mentioned_at,
|
||||
context, fact_type, metadata, chunk_id, document_id,
|
||||
COALESCE(
|
||||
(SELECT array_agg(elem) FROM jsonb_array_elements_text(tags_json) AS elem),
|
||||
'{{}}'::varchar[]
|
||||
),
|
||||
observation_scopes_json,
|
||||
text_signals
|
||||
FROM input_data
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
results = await conn.fetch(
|
||||
query,
|
||||
|
||||
@@ -6570,18 +6570,13 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
)
|
||||
|
||||
collist = await self._memory_unit_columns(conn)
|
||||
# The archive is cold storage, never a recall surface and carries no index,
|
||||
# so the schema gives it neither the `embedding` (dropped in d4f6a8c2e1b3)
|
||||
# nor the `search_vector` column (dropped in e7c3a9f1b2d5). Both are
|
||||
# recall-surface columns whose type/shape follows server
|
||||
# config, so the move in/out is over every memory_units column EXCEPT those
|
||||
# two; on revert each is recomputed from the unit's text/dates/entities below.
|
||||
# This makes a model switch (which re-dimensions memory_units) structurally
|
||||
# unable to trip a vector-dimension mismatch (#2209), and a text-search backend
|
||||
# switch unable to trip a search_vector type mismatch (#2503), on the
|
||||
# INSERT … SELECT round-trip.
|
||||
_archive_omitted = ('"embedding"', '"search_vector"')
|
||||
arch_cols = ", ".join(c for c in (s.strip() for s in collist.split(",")) if c not in _archive_omitted)
|
||||
# The archive is cold storage, never a recall surface, so the schema gives it
|
||||
# no `embedding` column at all (dropped in d4f6a8c2e1b3). The move in/out is
|
||||
# therefore over every memory_units column EXCEPT embedding; on revert the
|
||||
# embedding is recomputed from the unit's text/dates/entities below. This makes
|
||||
# a model switch (which re-dimensions memory_units) structurally unable to trip
|
||||
# a vector-dimension mismatch on the INSERT … SELECT round-trip (#2209).
|
||||
arch_cols = ", ".join(c for c in (s.strip() for s in collist.split(",")) if c != '"embedding"')
|
||||
|
||||
# --- Edit fields (live rows only): text / context / dates / fact_type / entities ---
|
||||
doing_edit = any(
|
||||
@@ -6700,29 +6695,14 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
arch_row = await conn.fetchrow(
|
||||
f"SELECT entity_ids FROM {arch} WHERE id = $1 AND bank_id = $2", str(memory_uuid), bank_id
|
||||
)
|
||||
# The archive keeps neither embedding nor search_vector (see arch_cols
|
||||
# above), so both default to NULL on the way back and are recomputed here:
|
||||
# the embedding below once entities are restored, the search_vector now
|
||||
# from the row's own text/context/text_signals.
|
||||
# The archive has no embedding column (see arch_cols above), so the live
|
||||
# row's embedding defaults to NULL on the way back and is recomputed below
|
||||
# once entities are restored.
|
||||
await conn.execute(
|
||||
f"INSERT INTO {mu} ({arch_cols}) SELECT {arch_cols} FROM {arch} WHERE id = $1 AND bank_id = $2",
|
||||
str(memory_uuid),
|
||||
bank_id,
|
||||
)
|
||||
# Rebuild search_vector using the *current* text-search backend, so the
|
||||
# reverted unit is keyword-searchable again (more correct than carrying a
|
||||
# verbatim copy that could be stale/wrong-type if the backend changed while
|
||||
# the fact sat archived). None = pgroonga/pg_textsearch/pg_search, which
|
||||
# index base columns directly and leave search_vector empty (#2503).
|
||||
from .db.ops_postgresql import pg_search_vector_expr
|
||||
|
||||
sv_expr = pg_search_vector_expr(get_config())
|
||||
if sv_expr is not None:
|
||||
await conn.execute(
|
||||
f"UPDATE {mu} SET search_vector = {sv_expr} WHERE id = $1 AND bank_id = $2",
|
||||
str(memory_uuid),
|
||||
bank_id,
|
||||
)
|
||||
# Re-consolidate from scratch; links are rebuilt by graph maintenance.
|
||||
await conn.execute(
|
||||
f"UPDATE {mu} SET consolidated_at = NULL, consolidation_failed_at = NULL, updated_at = now() "
|
||||
@@ -11139,6 +11119,335 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
|
||||
return result == "DELETE 1"
|
||||
|
||||
# =====================================================================
|
||||
# KNOWLEDGE BASE (folders + pages over mental models)
|
||||
# =====================================================================
|
||||
# The knowledge base is a tree of folders and pages stored in
|
||||
# ``knowledge_pages``. A page references the mental model holding its content
|
||||
# (``mental_model_id``); a folder is a container (``mental_model_id`` NULL).
|
||||
# Content lives in ``mental_models`` — this layer owns only tree structure.
|
||||
|
||||
# Default trigger for a knowledge page: a living document synthesized from the
|
||||
# bank's consolidated **observations** (not raw facts), refreshed incrementally
|
||||
# (delta) after each consolidation, and excluding other mental models so a page
|
||||
# never reflects on sibling pages. Applied when the client doesn't pass its own
|
||||
# ``trigger`` on create; a client can override any of these.
|
||||
KNOWLEDGE_PAGE_DEFAULT_TRIGGER = {
|
||||
"mode": "delta",
|
||||
"fact_types": ["observation"],
|
||||
"exclude_mental_models": True,
|
||||
"refresh_after_consolidation": True,
|
||||
}
|
||||
|
||||
# Knowledge pages default to a larger budget than a plain mental model (2048)
|
||||
# since they're meant to read as full documents. Applied when the client
|
||||
# doesn't pass ``max_tokens`` on create.
|
||||
KNOWLEDGE_PAGE_DEFAULT_MAX_TOKENS = 4096
|
||||
|
||||
@staticmethod
|
||||
def _row_to_knowledge_node(row) -> dict[str, Any]:
|
||||
"""Project a knowledge_pages row (optionally joined to its mental model)."""
|
||||
node: dict[str, Any] = {
|
||||
"id": row["id"],
|
||||
"bank_id": row["bank_id"],
|
||||
"parent_id": row["parent_id"],
|
||||
"kind": row["kind"],
|
||||
"name": row["name"],
|
||||
"mental_model_id": row["mental_model_id"],
|
||||
"sort_order": row["sort_order"],
|
||||
"managed": (row["managed"] if "managed" in row else False),
|
||||
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
|
||||
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
|
||||
}
|
||||
# Page rows are returned LEFT JOINed to mental_models so the OKF
|
||||
# projection (type/tags/description) needs no second round-trip.
|
||||
if "mm_tags" in row:
|
||||
node["tags"] = list(row["mm_tags"] or [])
|
||||
node["source_query"] = row["mm_source_query"]
|
||||
node["last_refreshed_at"] = row["mm_last_refreshed_at"].isoformat() if row["mm_last_refreshed_at"] else None
|
||||
return node
|
||||
|
||||
# Column list for plain (non-joined) knowledge_pages reads/RETURNING.
|
||||
_KP_COLUMNS = "id, bank_id, parent_id, kind, name, mental_model_id, sort_order, managed, created_at, updated_at"
|
||||
|
||||
_KP_PAGE_SELECT = (
|
||||
"kp.id, kp.bank_id, kp.parent_id, kp.kind, kp.name, kp.mental_model_id, "
|
||||
"kp.sort_order, kp.managed, kp.created_at, kp.updated_at, "
|
||||
"mm.tags AS mm_tags, mm.source_query AS mm_source_query, "
|
||||
"mm.last_refreshed_at AS mm_last_refreshed_at"
|
||||
)
|
||||
|
||||
def _kp_join(self) -> str:
|
||||
kp = fq_table("knowledge_pages")
|
||||
mm = fq_table("mental_models")
|
||||
return f"{kp} kp LEFT JOIN {mm} mm ON mm.id = kp.mental_model_id AND mm.bank_id = kp.bank_id"
|
||||
|
||||
async def _kp_assert_folder_parent(self, conn, bank_id: str, parent_id: str | None) -> None:
|
||||
"""A non-null parent must be an existing folder in this bank."""
|
||||
if parent_id is None:
|
||||
return
|
||||
row = await conn.fetchrow(
|
||||
f"SELECT kind FROM {fq_table('knowledge_pages')} WHERE bank_id = $1 AND id = $2",
|
||||
bank_id,
|
||||
parent_id,
|
||||
)
|
||||
if row is None:
|
||||
raise ValueError(f"Parent folder '{parent_id}' not found")
|
||||
if row["kind"] != "folder":
|
||||
raise ValueError(f"Parent '{parent_id}' is not a folder")
|
||||
|
||||
async def create_knowledge_folder(
|
||||
self,
|
||||
bank_id: str,
|
||||
name: str,
|
||||
*,
|
||||
parent_id: str | None = None,
|
||||
managed: bool = False,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a folder (a container node) in the knowledge base.
|
||||
|
||||
The knowledge base is managed by clients (CRUD over folders/pages);
|
||||
``managed`` lets a client tag a node as system-owned vs. hand-authored.
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
backend = await self._get_backend()
|
||||
folder_id = f"kf-{uuid.uuid4().hex}"
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
async with conn.transaction():
|
||||
await self._ensure_bank_exists(bank_id, request_context, conn=conn)
|
||||
await self._kp_assert_folder_parent(conn, bank_id, parent_id)
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
INSERT INTO {fq_table("knowledge_pages")} (id, bank_id, parent_id, kind, name, managed)
|
||||
VALUES ($1, $2, $3, 'folder', $4, $5)
|
||||
RETURNING {self._KP_COLUMNS}
|
||||
""",
|
||||
folder_id,
|
||||
bank_id,
|
||||
parent_id,
|
||||
name,
|
||||
managed,
|
||||
)
|
||||
return self._row_to_knowledge_node(row)
|
||||
|
||||
async def create_knowledge_page(
|
||||
self,
|
||||
bank_id: str,
|
||||
name: str,
|
||||
source_query: str,
|
||||
content: str,
|
||||
*,
|
||||
parent_id: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
max_tokens: int | None = None,
|
||||
trigger: dict[str, Any] | None = None,
|
||||
mental_model_id: str | None = None,
|
||||
managed: bool = False,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any] | None:
|
||||
"""Create a page: a backing mental model plus the tree node that refs it.
|
||||
|
||||
``managed`` lets a client tag the page as system-owned vs. hand-authored.
|
||||
When ``trigger`` is omitted the page uses ``KNOWLEDGE_PAGE_DEFAULT_TRIGGER``
|
||||
(observation-only, delta, auto-refresh) so a knowledge page is a living
|
||||
document by default.
|
||||
|
||||
Returns ``None`` when a page with the same name already exists in the same
|
||||
folder (a uniqueness violation) — the caller should treat that as
|
||||
"already exists" (surfaced by the API as a 409).
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
# The mental model carries the content (and is created+validated by the
|
||||
# existing path, including lazy bank creation); the node only refs it.
|
||||
mm = await self.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name=name,
|
||||
source_query=source_query,
|
||||
content=content,
|
||||
mental_model_id=mental_model_id,
|
||||
tags=tags,
|
||||
max_tokens=max_tokens if max_tokens is not None else self.KNOWLEDGE_PAGE_DEFAULT_MAX_TOKENS,
|
||||
trigger=trigger if trigger is not None else dict(self.KNOWLEDGE_PAGE_DEFAULT_TRIGGER),
|
||||
request_context=request_context,
|
||||
)
|
||||
backend = await self._get_backend()
|
||||
page_id = f"kp-{uuid.uuid4().hex}"
|
||||
try:
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
async with conn.transaction():
|
||||
await self._kp_assert_folder_parent(conn, bank_id, parent_id)
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
INSERT INTO {fq_table("knowledge_pages")}
|
||||
(id, bank_id, parent_id, kind, name, mental_model_id, managed)
|
||||
VALUES ($1, $2, $3, 'page', $4, $5, $6)
|
||||
RETURNING {self._KP_COLUMNS}
|
||||
""",
|
||||
page_id,
|
||||
bank_id,
|
||||
parent_id,
|
||||
name,
|
||||
mm["id"],
|
||||
managed,
|
||||
)
|
||||
except asyncpg.UniqueViolationError:
|
||||
# Duplicate page name in this folder (uq_kp_folder_pagename). Roll back
|
||||
# by deleting the orphan mental model we just created, then signal the
|
||||
# caller that the page already exists.
|
||||
await self.delete_mental_model(bank_id, mm["id"], request_context=request_context)
|
||||
return None
|
||||
node = self._row_to_knowledge_node(row)
|
||||
# Surface the mental-model metadata so the caller can render OKF or
|
||||
# schedule a content refresh without a second fetch.
|
||||
node["tags"] = list(mm.get("tags") or [])
|
||||
node["source_query"] = mm.get("source_query")
|
||||
node["last_refreshed_at"] = mm.get("last_refreshed_at")
|
||||
return node
|
||||
|
||||
async def list_knowledge_nodes(self, bank_id: str, *, request_context: "RequestContext") -> list[dict[str, Any]]:
|
||||
"""Return every folder/page node in the bank (flat; caller builds the tree)."""
|
||||
await self._authenticate_tenant(request_context)
|
||||
backend = await self._get_backend()
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT {self._KP_PAGE_SELECT}
|
||||
FROM {self._kp_join()}
|
||||
WHERE kp.bank_id = $1
|
||||
ORDER BY kp.sort_order, kp.name
|
||||
""",
|
||||
bank_id,
|
||||
)
|
||||
return [self._row_to_knowledge_node(r) for r in rows]
|
||||
|
||||
async def get_knowledge_page(
|
||||
self, bank_id: str, page_id: str, *, request_context: "RequestContext"
|
||||
) -> dict[str, Any] | None:
|
||||
"""Return a page node merged with its mental model's content (for OKF)."""
|
||||
await self._authenticate_tenant(request_context)
|
||||
backend = await self._get_backend()
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT {self._KP_PAGE_SELECT}, mm.content AS mm_content
|
||||
FROM {self._kp_join()}
|
||||
WHERE kp.bank_id = $1 AND kp.id = $2 AND kp.kind = 'page'
|
||||
""",
|
||||
bank_id,
|
||||
page_id,
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
node = self._row_to_knowledge_node(row)
|
||||
node["content"] = row["mm_content"]
|
||||
return node
|
||||
|
||||
async def rename_knowledge_node(
|
||||
self, bank_id: str, node_id: str, name: str, *, request_context: "RequestContext"
|
||||
) -> dict[str, Any] | None:
|
||||
"""Rename a folder or page node."""
|
||||
await self._authenticate_tenant(request_context)
|
||||
backend = await self._get_backend()
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
UPDATE {fq_table("knowledge_pages")}
|
||||
SET name = $3, updated_at = now()
|
||||
WHERE bank_id = $1 AND id = $2
|
||||
RETURNING {self._KP_COLUMNS}
|
||||
""",
|
||||
bank_id,
|
||||
node_id,
|
||||
name,
|
||||
)
|
||||
return self._row_to_knowledge_node(row) if row else None
|
||||
|
||||
async def move_knowledge_node(
|
||||
self, bank_id: str, node_id: str, new_parent_id: str | None, *, request_context: "RequestContext"
|
||||
) -> dict[str, Any] | None:
|
||||
"""Re-parent a node, rejecting self-parenting and cycles."""
|
||||
await self._authenticate_tenant(request_context)
|
||||
if new_parent_id == node_id:
|
||||
raise ValueError("A node cannot be its own parent")
|
||||
backend = await self._get_backend()
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
async with conn.transaction():
|
||||
await self._kp_assert_folder_parent(conn, bank_id, new_parent_id)
|
||||
# Cycle guard: walk up from the new parent; if we reach node_id,
|
||||
# the move would create a loop. Done in Python so the check stays
|
||||
# dialect-agnostic (no recursive CTE).
|
||||
if new_parent_id is not None:
|
||||
parents = {
|
||||
r["id"]: r["parent_id"]
|
||||
for r in await conn.fetch(
|
||||
f"SELECT id, parent_id FROM {fq_table('knowledge_pages')} WHERE bank_id = $1",
|
||||
bank_id,
|
||||
)
|
||||
}
|
||||
cursor: str | None = new_parent_id
|
||||
while cursor is not None:
|
||||
if cursor == node_id:
|
||||
raise ValueError("Cannot move a node into its own subtree")
|
||||
cursor = parents.get(cursor)
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
UPDATE {fq_table("knowledge_pages")}
|
||||
SET parent_id = $3, updated_at = now()
|
||||
WHERE bank_id = $1 AND id = $2
|
||||
RETURNING {self._KP_COLUMNS}
|
||||
""",
|
||||
bank_id,
|
||||
node_id,
|
||||
new_parent_id,
|
||||
)
|
||||
return self._row_to_knowledge_node(row) if row else None
|
||||
|
||||
async def delete_knowledge_node(self, bank_id: str, node_id: str, *, request_context: "RequestContext") -> bool:
|
||||
"""Delete a node and its whole subtree, including each page's mental model.
|
||||
|
||||
Deleting the mental models cascades their page rows away (FK ON DELETE
|
||||
CASCADE); deleting the node then cascades any remaining descendant folder
|
||||
rows. The subtree is gathered in Python so the logic is dialect-agnostic.
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
backend = await self._get_backend()
|
||||
async with acquire_with_retry(backend) as conn:
|
||||
async with conn.transaction():
|
||||
all_rows = await conn.fetch(
|
||||
f"SELECT id, parent_id, mental_model_id FROM {fq_table('knowledge_pages')} WHERE bank_id = $1",
|
||||
bank_id,
|
||||
)
|
||||
by_parent: dict[str | None, list] = {}
|
||||
for r in all_rows:
|
||||
by_parent.setdefault(r["parent_id"], []).append(r)
|
||||
if not any(r["id"] == node_id for r in all_rows):
|
||||
return False
|
||||
# BFS the subtree rooted at node_id, collecting page mental models.
|
||||
stack = [node_id]
|
||||
mm_ids: list[str] = []
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
for child in by_parent.get(current, []):
|
||||
stack.append(child["id"])
|
||||
node_row = next((r for r in all_rows if r["id"] == current), None)
|
||||
if node_row and node_row["mental_model_id"]:
|
||||
mm_ids.append(node_row["mental_model_id"])
|
||||
# Delete each backing mental model individually (the subtree is
|
||||
# small) to keep the SQL dialect-neutral — no PG array casts.
|
||||
for mm_id in mm_ids:
|
||||
await conn.execute(
|
||||
f"DELETE FROM {fq_table('mental_models')} WHERE bank_id = $1 AND id = $2",
|
||||
bank_id,
|
||||
mm_id,
|
||||
)
|
||||
await conn.execute(
|
||||
f"DELETE FROM {fq_table('knowledge_pages')} WHERE bank_id = $1 AND id = $2",
|
||||
bank_id,
|
||||
node_id,
|
||||
)
|
||||
return True
|
||||
|
||||
async def compute_mental_model_is_stale(
|
||||
self,
|
||||
conn,
|
||||
@@ -12867,3 +13176,4 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
result_metadata={"mental_model_id": mental_model_id, "name": mental_model["name"]},
|
||||
dedupe_by_bank=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -53,53 +53,6 @@ __all__ = [
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Name of the single forced function tool used to carry structured output when
|
||||
# strict_schema is on. The Codex backend speaks the OpenAI Responses API, so a
|
||||
# forced function call gives us constrained decoding straight into the response
|
||||
# schema — no prompt-injected schema, no raw json.loads on free-form model text,
|
||||
# no invalid-\escape retry storm (issue #2504, same class as #1002 / #2339).
|
||||
_STRUCTURED_TOOL_NAME = "structured_response"
|
||||
|
||||
# Valid JSON string escape characters (the char that may follow a backslash).
|
||||
_VALID_JSON_ESCAPE_CHARS = set('"\\/bfnrtu')
|
||||
|
||||
|
||||
def _repair_invalid_json_escapes(text: str) -> str:
|
||||
"""Best-effort repair of invalid ``\\escape`` sequences in a JSON string.
|
||||
|
||||
Escape-heavy content (code, serial/CLI commands, Windows paths, regexes)
|
||||
makes weaker models emit backslashes that aren't valid JSON escapes (e.g.
|
||||
``\\d``, ``\\s``, ``C:\\Users``), so ``json.loads`` fails deterministically
|
||||
and every retry re-fails the same way (issue #2504). This doubles any
|
||||
backslash that isn't part of a valid escape so the payload parses. It is a
|
||||
lenient fallback only — the strict_schema forced-tool path is the real fix.
|
||||
"""
|
||||
result: list[str] = []
|
||||
i = 0
|
||||
n = len(text)
|
||||
while i < n:
|
||||
ch = text[i]
|
||||
if ch == "\\" and i + 1 < n:
|
||||
nxt = text[i + 1]
|
||||
if nxt in _VALID_JSON_ESCAPE_CHARS:
|
||||
# Preserve the valid escape (both chars) verbatim.
|
||||
result.append(ch)
|
||||
result.append(nxt)
|
||||
i += 2
|
||||
continue
|
||||
# Invalid escape: escape the lone backslash so JSON parses.
|
||||
result.append("\\\\")
|
||||
i += 1
|
||||
continue
|
||||
if ch == "\\" and i + 1 == n:
|
||||
# Trailing lone backslash — escape it.
|
||||
result.append("\\\\")
|
||||
i += 1
|
||||
continue
|
||||
result.append(ch)
|
||||
i += 1
|
||||
return "".join(result)
|
||||
|
||||
|
||||
class CodexLLM(LLMInterface):
|
||||
"""
|
||||
@@ -383,18 +336,7 @@ class CodexLLM(LLMInterface):
|
||||
strict_schema: bool = False,
|
||||
return_usage: bool = False,
|
||||
) -> Any:
|
||||
"""Make API call to Codex backend with SSE streaming.
|
||||
|
||||
Args:
|
||||
strict_schema: Route structured output through a single forced
|
||||
function tool (constrained decoding) instead of prompt-injecting
|
||||
the schema and parsing free-form text. The Codex backend speaks
|
||||
the OpenAI Responses API, so the forced function call emits the
|
||||
response schema directly as tool arguments — eliminating the
|
||||
invalid-``\\escape`` retry storm (issue #2504). When False, falls
|
||||
back to schema-in-prompt + JSON parse, now hardened with a lenient
|
||||
invalid-escape repair before giving up.
|
||||
"""
|
||||
"""Make API call to Codex backend with SSE streaming."""
|
||||
start_time = time.time()
|
||||
|
||||
# Proactively refresh the OAuth access_token if it's near expiry.
|
||||
@@ -419,22 +361,11 @@ class CodexLLM(LLMInterface):
|
||||
else:
|
||||
user_messages.append(msg)
|
||||
|
||||
# Structured output: prefer a single forced function tool (constrained
|
||||
# decoding) over text-injecting the schema and parsing the reply. The
|
||||
# forced tool guarantees schema-shaped JSON in the tool arguments,
|
||||
# eliminating the invalid-\escape retry storm (issue #2504). When
|
||||
# strict_schema is off we keep the schema-in-prompt + json.loads
|
||||
# fallback (now hardened with a lenient escape repair) for callers that
|
||||
# can't force tools.
|
||||
schema = None
|
||||
use_forced_tool = False
|
||||
# Add JSON schema instruction if response_format is provided
|
||||
if response_format is not None and hasattr(response_format, "model_json_schema"):
|
||||
schema = response_format.model_json_schema()
|
||||
if strict_schema:
|
||||
use_forced_tool = True
|
||||
else:
|
||||
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2, ensure_ascii=False)}"
|
||||
system_instruction += schema_msg
|
||||
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2, ensure_ascii=False)}"
|
||||
system_instruction += schema_msg
|
||||
|
||||
# gpt-5.2-codex only supports "detailed" reasoning summary
|
||||
reasoning_summary = "detailed" if "5.2" in self.model else self.reasoning_summary
|
||||
@@ -461,20 +392,6 @@ class CodexLLM(LLMInterface):
|
||||
"prompt_cache_key": str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
if use_forced_tool and schema is not None:
|
||||
# Single function tool whose parameters ARE the response schema;
|
||||
# force it via tool_choice so the backend does constrained decoding.
|
||||
payload["tools"] = [
|
||||
{
|
||||
"type": "function",
|
||||
"name": _STRUCTURED_TOOL_NAME,
|
||||
"description": "Return the structured response.",
|
||||
"parameters": schema,
|
||||
}
|
||||
]
|
||||
payload["tool_choice"] = {"type": "function", "name": _STRUCTURED_TOOL_NAME}
|
||||
payload["parallel_tool_calls"] = False
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.access_token}",
|
||||
"Content-Type": "application/json",
|
||||
@@ -495,15 +412,8 @@ class CodexLLM(LLMInterface):
|
||||
response = await self._client.post(url, json=payload, headers=headers, timeout=120.0)
|
||||
response.raise_for_status()
|
||||
|
||||
# Forced-tool path: read structured output from the function-call
|
||||
# arguments (already a JSON string in a dedicated channel) rather
|
||||
# than from free-form assistant text.
|
||||
if use_forced_tool:
|
||||
text_content, tool_calls = await self._parse_sse_tool_stream(response)
|
||||
content = text_content or ""
|
||||
else:
|
||||
tool_calls = []
|
||||
content = await self._parse_sse_stream(response)
|
||||
# Parse SSE stream
|
||||
content = await self._parse_sse_stream(response)
|
||||
|
||||
# Codex SSE carries no usage block; stash the same char/4 estimate
|
||||
# the success path traces so a later parse/validate failure records
|
||||
@@ -516,28 +426,7 @@ class CodexLLM(LLMInterface):
|
||||
)
|
||||
|
||||
# Handle structured output
|
||||
if use_forced_tool:
|
||||
tool_input = None
|
||||
for tc in tool_calls:
|
||||
if tc.name == _STRUCTURED_TOOL_NAME:
|
||||
tool_input = tc.arguments if isinstance(tc.arguments, dict) else None
|
||||
break
|
||||
if tool_input is None:
|
||||
# Model ignored the forced tool (rare — e.g. a gateway that
|
||||
# drops tool_choice). Retry so we don't hard-fail.
|
||||
logger.warning(
|
||||
f"Codex forced structured tool missing from response "
|
||||
f"(attempt {attempt + 1}/{max_retries + 1})"
|
||||
)
|
||||
if attempt < max_retries:
|
||||
backoff = min(initial_backoff * (2**attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
attempt += 1
|
||||
continue
|
||||
raise RuntimeError("Codex did not return the forced structured_response tool call")
|
||||
content = json.dumps(tool_input)
|
||||
result = tool_input if skip_validation else response_format.model_validate(tool_input)
|
||||
elif response_format is not None:
|
||||
if response_format is not None:
|
||||
# Models may wrap JSON in markdown
|
||||
clean_content = content
|
||||
if "```json" in content:
|
||||
@@ -548,20 +437,13 @@ class CodexLLM(LLMInterface):
|
||||
try:
|
||||
json_data = json.loads(clean_content)
|
||||
except json.JSONDecodeError as e:
|
||||
# Escape-heavy content deterministically re-fails every
|
||||
# retry (issue #2504). Try a lenient invalid-escape repair
|
||||
# before burning a retry / re-raising.
|
||||
try:
|
||||
json_data = json.loads(_repair_invalid_json_escapes(clean_content))
|
||||
logger.info("Codex JSON parsed after repairing invalid escape sequences")
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Codex JSON parse error (attempt {attempt + 1}/{max_retries + 1}): {e}")
|
||||
if attempt < max_retries:
|
||||
backoff = min(initial_backoff * (2**attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
attempt += 1
|
||||
continue
|
||||
raise
|
||||
logger.warning(f"Codex JSON parse error (attempt {attempt + 1}/{max_retries + 1}): {e}")
|
||||
if attempt < max_retries:
|
||||
backoff = min(initial_backoff * (2**attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
attempt += 1
|
||||
continue
|
||||
raise
|
||||
|
||||
if skip_validation:
|
||||
result = json_data
|
||||
@@ -990,13 +872,8 @@ class CodexLLM(LLMInterface):
|
||||
try:
|
||||
arguments = json.loads(arguments_str)
|
||||
except json.JSONDecodeError:
|
||||
# Escape-heavy content can emit invalid \escape
|
||||
# sequences (issue #2504); repair before giving up.
|
||||
try:
|
||||
arguments = json.loads(_repair_invalid_json_escapes(arguments_str))
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse tool arguments: {arguments_str}")
|
||||
arguments = {}
|
||||
logger.warning(f"Failed to parse tool arguments: {arguments_str}")
|
||||
arguments = {}
|
||||
|
||||
tool_calls.append(
|
||||
LLMToolCall(
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
"""
|
||||
Regression tests for Codex structured output (issue #2504).
|
||||
|
||||
Before the fix, ``CodexLLM.call(strict_schema=True)`` was a dead no-op: structured
|
||||
output always went through prompt-injected schema + raw ``json.loads`` on the
|
||||
model's free-form text. Escape-heavy content (code, serial/CLI commands, Windows
|
||||
paths, regexes) makes weaker models emit invalid ``\\escape`` sequences, so every
|
||||
parse attempt fails and retain/consolidation burn all retries and fail.
|
||||
|
||||
The fix:
|
||||
- ``strict_schema=True`` routes structured output through a single forced function
|
||||
tool (constrained decoding into the response schema).
|
||||
- The non-strict fallback now repairs invalid ``\\escape`` sequences before giving up.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from hindsight_api.engine.providers.codex_llm import (
|
||||
CodexLLM,
|
||||
_repair_invalid_json_escapes,
|
||||
)
|
||||
from hindsight_api.engine.response_models import LLMToolCall
|
||||
|
||||
|
||||
class _Fact(BaseModel):
|
||||
fact: str
|
||||
|
||||
|
||||
def build_llm() -> CodexLLM:
|
||||
with patch.object(CodexLLM, "_load_codex_auth", return_value=("token", "account")):
|
||||
return CodexLLM(
|
||||
provider="openai-codex",
|
||||
api_key="ignored",
|
||||
base_url="https://chatgpt.com/backend-api",
|
||||
model="gpt-5.4-mini",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _repair_invalid_json_escapes — pure unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_repair_fixes_invalid_escape_in_json():
|
||||
# `\d` and `\s` are not valid JSON escapes; raw json.loads fails.
|
||||
broken = r'{"fact": "regex \d+\s matches digits"}'
|
||||
import json
|
||||
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
json.loads(broken)
|
||||
repaired = _repair_invalid_json_escapes(broken)
|
||||
assert json.loads(repaired) == {"fact": r"regex \d+\s matches digits"}
|
||||
|
||||
|
||||
def test_repair_preserves_valid_escapes():
|
||||
import json
|
||||
|
||||
valid = r'{"fact": "line1\nline2\ttab \"quoted\" \\backslash é"}'
|
||||
# Already valid — repair must not corrupt it.
|
||||
assert json.loads(_repair_invalid_json_escapes(valid)) == json.loads(valid)
|
||||
|
||||
|
||||
def test_repair_handles_windows_paths():
|
||||
import json
|
||||
|
||||
# Uses path segments whose first char isn't a valid JSON escape letter
|
||||
# (b/f/n/r/t/u), where the repair is unambiguous.
|
||||
broken = r'{"path": "C:\Windows\System32\app.exe"}'
|
||||
assert json.loads(_repair_invalid_json_escapes(broken)) == {"path": r"C:\Windows\System32\app.exe"}
|
||||
|
||||
|
||||
def test_repair_handles_trailing_backslash():
|
||||
# A lone trailing backslash must be escaped, not dropped.
|
||||
assert _repair_invalid_json_escapes("abc\\") == "abc\\\\"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# strict_schema forced-tool path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strict_schema_uses_forced_function_tool():
|
||||
llm = build_llm()
|
||||
response = MagicMock()
|
||||
response.raise_for_status.return_value = None
|
||||
tool_call = LLMToolCall(id="call-1", name="structured_response", arguments={"fact": "the sky is blue"})
|
||||
|
||||
with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = response
|
||||
with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
|
||||
mock_parse.return_value = (None, [tool_call])
|
||||
result = await llm.call(
|
||||
messages=[{"role": "user", "content": "The sky is blue"}],
|
||||
response_format=_Fact,
|
||||
strict_schema=True,
|
||||
max_retries=0,
|
||||
)
|
||||
sent_payload = mock_post.call_args.kwargs["json"]
|
||||
|
||||
# Forced tool wired into the request payload.
|
||||
assert sent_payload["tool_choice"] == {"type": "function", "name": "structured_response"}
|
||||
assert len(sent_payload["tools"]) == 1
|
||||
assert sent_payload["tools"][0]["name"] == "structured_response"
|
||||
assert sent_payload["parallel_tool_calls"] is False
|
||||
# No prompt-injected schema in the instructions.
|
||||
assert "You must respond with valid JSON" not in sent_payload["instructions"]
|
||||
|
||||
assert isinstance(result, _Fact)
|
||||
assert result.fact == "the sky is blue"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strict_schema_skip_validation_returns_dict():
|
||||
llm = build_llm()
|
||||
response = MagicMock()
|
||||
response.raise_for_status.return_value = None
|
||||
tool_call = LLMToolCall(id="c", name="structured_response", arguments={"fact": "x"})
|
||||
|
||||
with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = response
|
||||
with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
|
||||
mock_parse.return_value = (None, [tool_call])
|
||||
result = await llm.call(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
response_format=_Fact,
|
||||
strict_schema=True,
|
||||
skip_validation=True,
|
||||
max_retries=0,
|
||||
)
|
||||
|
||||
assert result == {"fact": "x"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strict_schema_retries_when_forced_tool_missing():
|
||||
llm = build_llm()
|
||||
response = MagicMock()
|
||||
response.raise_for_status.return_value = None
|
||||
|
||||
with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = response
|
||||
# Model returns no tool call at all — should raise after retries exhausted.
|
||||
with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse:
|
||||
mock_parse.return_value = ("some prose", [])
|
||||
with pytest.raises(RuntimeError, match="structured_response"):
|
||||
await llm.call(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
response_format=_Fact,
|
||||
strict_schema=True,
|
||||
max_retries=0,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-strict fallback: escape repair keeps the retry storm from happening
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_strict_repairs_invalid_escapes_without_retrying():
|
||||
llm = build_llm()
|
||||
response = MagicMock()
|
||||
response.raise_for_status.return_value = None
|
||||
# Escape-heavy content the model would emit as invalid JSON.
|
||||
escape_heavy = r'{"fact": "run rig-control \d serial \s command"}'
|
||||
|
||||
with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post:
|
||||
mock_post.return_value = response
|
||||
with patch.object(llm, "_parse_sse_stream", new_callable=AsyncMock) as mock_parse:
|
||||
mock_parse.return_value = escape_heavy
|
||||
result = await llm.call(
|
||||
messages=[{"role": "user", "content": "coding transcript"}],
|
||||
response_format=_Fact,
|
||||
strict_schema=False,
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
# Parsed on the first attempt (no retry storm): the SSE stream was read once.
|
||||
assert mock_post.await_count == 1
|
||||
assert isinstance(result, _Fact)
|
||||
assert result.fact == r"run rig-control \d serial \s command"
|
||||
@@ -4,7 +4,8 @@ Unit tests that verify the abstraction interfaces work correctly
|
||||
without requiring a live database connection.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -747,85 +748,6 @@ class TestOracleOpsInsertFactsBatch:
|
||||
assert rows_data[0][13] == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PostgreSQL search_vector handling (insert). Since the curation archive drops
|
||||
# search_vector (#2503), the insert is the single place it is populated, and
|
||||
# pg_search_vector_expr is its one source of truth (shared with revert recompute).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPostgreSQLSearchVector:
|
||||
@staticmethod
|
||||
def _cfg(ext: str, lang: str = "english"):
|
||||
from types import SimpleNamespace
|
||||
|
||||
return SimpleNamespace(text_search_extension=ext, text_search_extension_native_language=lang)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ext,needle",
|
||||
[
|
||||
("native", "to_tsvector('english'::regconfig,"),
|
||||
("vchord", "::bm25_catalog.bm25vector"),
|
||||
],
|
||||
)
|
||||
def test_expr_builds_vector_for_vector_backends(self, ext, needle):
|
||||
from hindsight_api.engine.db.ops_postgresql import pg_search_vector_expr
|
||||
|
||||
expr = pg_search_vector_expr(self._cfg(ext))
|
||||
assert expr is not None and needle in expr
|
||||
# Always built from the same three carried columns.
|
||||
assert "COALESCE(text, '')" in expr and "COALESCE(text_signals, '')" in expr
|
||||
|
||||
@pytest.mark.parametrize("ext", ["pgroonga", "pg_textsearch", "pg_search"])
|
||||
def test_expr_none_for_base_column_backends(self, ext):
|
||||
from hindsight_api.engine.db.ops_postgresql import pg_search_vector_expr
|
||||
|
||||
# These index the base text columns directly; search_vector stays empty.
|
||||
assert pg_search_vector_expr(self._cfg(ext)) is None
|
||||
|
||||
def test_expr_accepts_custom_column_refs(self):
|
||||
from hindsight_api.engine.db.ops_postgresql import pg_search_vector_expr
|
||||
|
||||
expr = pg_search_vector_expr(self._cfg("native"), text_col="mu.text", context_col="mu.context")
|
||||
assert "COALESCE(mu.text, '')" in expr and "COALESCE(mu.context, '')" in expr
|
||||
|
||||
async def _insert_query(self, ext: str) -> str:
|
||||
from hindsight_api.engine.db.ops_postgresql import PostgreSQLOps
|
||||
|
||||
conn = AsyncMock(spec=DatabaseConnection)
|
||||
conn.fetch = AsyncMock(return_value=[{"id": "00000000-0000-0000-0000-000000000001"}])
|
||||
batch = dict(
|
||||
bank_id="b",
|
||||
fact_texts=["t"],
|
||||
embeddings=["[0.1]"],
|
||||
event_dates=[None],
|
||||
occurred_starts=[None],
|
||||
occurred_ends=[None],
|
||||
mentioned_ats=[None],
|
||||
contexts=["c"],
|
||||
fact_types=["world"],
|
||||
metadata_jsons=["{}"],
|
||||
chunk_ids=[None],
|
||||
document_ids=[None],
|
||||
tags_list=[""],
|
||||
observation_scopes_list=[None],
|
||||
text_signals_list=[None],
|
||||
)
|
||||
with patch("hindsight_api.config.get_config", return_value=self._cfg(ext)):
|
||||
await PostgreSQLOps().insert_facts_batch(conn=conn, **batch)
|
||||
return conn.fetch.call_args.args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("ext", ["native", "vchord"])
|
||||
async def test_insert_includes_search_vector_column(self, ext):
|
||||
assert "search_vector" in await self._insert_query(ext)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("ext", ["pgroonga", "pg_textsearch", "pg_search"])
|
||||
async def test_insert_omits_search_vector_column(self, ext):
|
||||
assert "search_vector" not in await self._insert_query(ext)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# normalize_schema tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""HTTP + engine integration tests for the knowledge base (folders + pages).
|
||||
|
||||
Pages are seeded directly via the engine (deterministic content, no LLM) so the
|
||||
tree, OKF projection, move/rename, and cascade-delete behaviour can be asserted
|
||||
without consolidation.
|
||||
"""
|
||||
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
import pytest_asyncio
|
||||
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine
|
||||
|
||||
|
||||
def _enc(bank_id: str) -> str:
|
||||
return urllib.parse.quote(bank_id, safe="")
|
||||
|
||||
|
||||
class _Seed:
|
||||
"""Holds the ids created by the seed fixture for assertions."""
|
||||
|
||||
def __init__(self, **ids):
|
||||
self.__dict__.update(ids)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def kb_bank(memory: MemoryEngine, request_context):
|
||||
"""A bank with folders, nested folders, and pages."""
|
||||
bank_id = f"test-kb-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
runbooks = await memory.create_knowledge_folder(bank_id, "Runbooks", request_context=request_context)
|
||||
policies = await memory.create_knowledge_folder(bank_id, "Policies", request_context=request_context)
|
||||
sub = await memory.create_knowledge_folder(
|
||||
bank_id, "Sub", parent_id=runbooks["id"], request_context=request_context
|
||||
)
|
||||
orders = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"Orders",
|
||||
"What are the order facts?",
|
||||
"# Orders\n\nOne row per order.",
|
||||
parent_id=runbooks["id"],
|
||||
tags=["type:runbook", "sales", "revenue"],
|
||||
request_context=request_context,
|
||||
)
|
||||
billing = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"Billing",
|
||||
"What is the billing policy?",
|
||||
"# Billing\n\nNet-30.",
|
||||
parent_id=policies["id"],
|
||||
tags=["type:policy", "revenue"],
|
||||
request_context=request_context,
|
||||
)
|
||||
loose = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"Loose",
|
||||
"A root page.",
|
||||
"# Loose\n\nNo folder, no tags.",
|
||||
tags=[],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
yield (
|
||||
bank_id,
|
||||
_Seed(
|
||||
runbooks=runbooks["id"],
|
||||
policies=policies["id"],
|
||||
sub=sub["id"],
|
||||
orders=orders["id"],
|
||||
billing=billing["id"],
|
||||
loose=loose["id"],
|
||||
orders_mm=orders["mental_model_id"],
|
||||
),
|
||||
)
|
||||
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestTree:
|
||||
async def test_nested_tree(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/tree")
|
||||
assert resp.status_code == 200, resp.text
|
||||
roots = {r["name"]: r for r in resp.json()["roots"]}
|
||||
assert set(roots) == {"Runbooks", "Policies", "Loose"}
|
||||
|
||||
runbooks = roots["Runbooks"]
|
||||
assert runbooks["kind"] == "folder"
|
||||
child_names = {c["name"] for c in runbooks["children"]}
|
||||
assert child_names == {"Sub", "Orders"}
|
||||
|
||||
orders = next(c for c in runbooks["children"] if c["name"] == "Orders")
|
||||
assert orders["kind"] == "page"
|
||||
# Human-created pages are pinned (not curator-managed).
|
||||
assert orders["managed"] is False
|
||||
assert "sales" in orders["tags"]
|
||||
assert roots["Loose"]["kind"] == "page"
|
||||
|
||||
|
||||
class TestPageDefaults:
|
||||
"""A knowledge page is a living document by default: observation-only, delta,
|
||||
auto-refreshing, with a larger token budget than a plain mental model."""
|
||||
|
||||
async def test_default_trigger_and_max_tokens(self, memory: MemoryEngine, request_context):
|
||||
bank_id = f"test-kb-def-{uuid.uuid4().hex[:8]}"
|
||||
page = await memory.create_knowledge_page(
|
||||
bank_id, "P", "What is P?", "seed", request_context=request_context
|
||||
)
|
||||
mm = await memory.get_mental_model(bank_id, page["mental_model_id"], request_context=request_context)
|
||||
assert mm["trigger"] == {
|
||||
"mode": "delta",
|
||||
"fact_types": ["observation"],
|
||||
"exclude_mental_models": True,
|
||||
"refresh_after_consolidation": True,
|
||||
}
|
||||
assert mm["max_tokens"] == 4096
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
async def test_client_trigger_and_max_tokens_override_defaults(self, memory: MemoryEngine, request_context):
|
||||
bank_id = f"test-kb-ovr-{uuid.uuid4().hex[:8]}"
|
||||
page = await memory.create_knowledge_page(
|
||||
bank_id,
|
||||
"P",
|
||||
"What is P?",
|
||||
"seed",
|
||||
trigger={"mode": "full", "refresh_after_consolidation": False},
|
||||
max_tokens=1024,
|
||||
request_context=request_context,
|
||||
)
|
||||
mm = await memory.get_mental_model(bank_id, page["mental_model_id"], request_context=request_context)
|
||||
assert mm["trigger"]["mode"] == "full"
|
||||
assert mm["trigger"].get("refresh_after_consolidation") is False
|
||||
assert mm["max_tokens"] == 1024
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
class TestGetPage:
|
||||
async def test_okf_document(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/pages/{ids.orders}")
|
||||
assert resp.status_code == 200, resp.text
|
||||
page = resp.json()
|
||||
assert page["type"] == "runbook"
|
||||
assert page["body"].startswith("# Orders")
|
||||
assert page["markdown"].startswith("---\n")
|
||||
assert 'type: "runbook"' in page["markdown"]
|
||||
|
||||
async def test_missing_page_404(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/pages/nope")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestCreate:
|
||||
async def test_create_folder(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.post(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/folders",
|
||||
json={"name": "Guides", "parent_id": None},
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
assert resp.json()["kind"] == "folder"
|
||||
assert resp.json()["name"] == "Guides"
|
||||
|
||||
async def test_create_folder_bad_parent(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
# parent that is a page, not a folder → 400
|
||||
resp = await api_client.post(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/folders",
|
||||
json={"name": "Nope", "parent_id": ids.orders},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
class TestGraphAndExport:
|
||||
async def test_graph_shared_tag_edge(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/graph")
|
||||
assert resp.status_code == 200, resp.text
|
||||
data = resp.json()
|
||||
assert data["total_pages"] == 3
|
||||
# orders & billing share "revenue"; loose has no tags
|
||||
assert data["total_edges"] == 1
|
||||
edge = data["edges"][0]["data"]
|
||||
assert {edge["source"], edge["target"]} == {ids.orders, ids.billing}
|
||||
assert edge["sharedTags"] == ["revenue"]
|
||||
|
||||
async def test_export_bundle_nested_index(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/export")
|
||||
assert resp.status_code == 200, resp.text
|
||||
files = {f["path"]: f["content"] for f in resp.json()["files"]}
|
||||
assert "index.md" in files
|
||||
assert f"{ids.orders}.md" in files
|
||||
# index reflects the folder hierarchy
|
||||
assert "**Runbooks/**" in files["index.md"]
|
||||
assert "One row per order." in files[f"{ids.orders}.md"]
|
||||
|
||||
|
||||
class TestMoveRenameDelete:
|
||||
async def test_rename(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
resp = await api_client.patch(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.policies}",
|
||||
json={"name": "Compliance"},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["name"] == "Compliance"
|
||||
|
||||
async def test_move_into_folder(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
# move the Loose root page under Policies
|
||||
resp = await api_client.patch(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.loose}",
|
||||
json={"parent_id": ids.policies},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json()["parent_id"] == ids.policies
|
||||
|
||||
async def test_move_cycle_rejected(self, api_client, kb_bank):
|
||||
bank_id, ids = kb_bank
|
||||
# moving Runbooks under its own descendant Sub must fail
|
||||
resp = await api_client.patch(
|
||||
f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.runbooks}",
|
||||
json={"parent_id": ids.sub},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
async def test_delete_folder_cascades(self, api_client, kb_bank, memory, request_context):
|
||||
bank_id, ids = kb_bank
|
||||
# deleting Runbooks removes Sub + Orders (and Orders' mental model)
|
||||
resp = await api_client.delete(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/nodes/{ids.runbooks}")
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
tree = (await api_client.get(f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/tree")).json()
|
||||
root_names = {r["name"] for r in tree["roots"]}
|
||||
assert "Runbooks" not in root_names
|
||||
# the backing mental model is gone too
|
||||
mm = await memory.get_mental_model(bank_id, ids.orders_mm, request_context=request_context)
|
||||
assert mm is None
|
||||
@@ -182,7 +182,7 @@ def test_batch_request_body_strict_follows_config(strict):
|
||||
from hindsight_api.engine.retain.fact_extraction import _build_request_body
|
||||
|
||||
llm_config = SimpleNamespace(model="gpt-4o-mini", provider="openai", _provider_impl=SimpleNamespace())
|
||||
config = SimpleNamespace(retain_max_completion_tokens=None, llm_strict_schema=strict, llm_temperature_retain=None)
|
||||
config = SimpleNamespace(retain_max_completion_tokens=None, llm_strict_schema=strict)
|
||||
# provider != "openai" service-tier branch skipped via _provider_impl without attr
|
||||
llm_config._provider_impl.openai_service_tier = None
|
||||
|
||||
|
||||
@@ -101,12 +101,11 @@ async def _archive_row(conn, mem_id: uuid.UUID) -> dict | None:
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
async def _archive_has_column(conn, column: str) -> bool:
|
||||
async def _archive_has_embedding_column(conn) -> bool:
|
||||
return bool(
|
||||
await conn.fetchval(
|
||||
"SELECT 1 FROM information_schema.columns "
|
||||
"WHERE table_name = 'invalidated_memory_units' AND column_name = $1",
|
||||
column,
|
||||
"WHERE table_name = 'invalidated_memory_units' AND column_name = 'embedding'"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -175,12 +174,9 @@ class TestInvalidate:
|
||||
arch = await _archive_row(conn, m1)
|
||||
assert arch is not None, "row must be in the archive"
|
||||
assert arch["invalidation_reason"] == "decommissioned"
|
||||
assert not await _archive_has_column(conn, "embedding"), (
|
||||
assert not await _archive_has_embedding_column(conn), (
|
||||
"archive is cold storage; the schema drops the embedding column (#2209)"
|
||||
)
|
||||
assert not await _archive_has_column(conn, "search_vector"), (
|
||||
"archive is cold storage with no index; the schema drops search_vector (#2503)"
|
||||
)
|
||||
assert await _link_count(conn, m1) == 0, "links cascade-pruned on move"
|
||||
assert str(obs_id) not in await _obs_ids(conn, bank_id), "derived observation removed"
|
||||
assert await _consolidated_at(conn, m2) is None, "surviving source reset for re-consolidation"
|
||||
@@ -220,10 +216,6 @@ class TestInvalidate:
|
||||
assert e1 in await _entity_ids_for(conn, m1), "entity associations restored on revert"
|
||||
reverted_emb = await conn.fetchval("SELECT embedding FROM memory_units WHERE id = $1", m1)
|
||||
assert reverted_emb is not None, "embedding recomputed on revert (archive keeps none)"
|
||||
# Native backend (test default) stores a real tsvector; it must be rebuilt on
|
||||
# revert so the reverted fact is keyword-searchable again (archive keeps none, #2503).
|
||||
reverted_sv = await conn.fetchval("SELECT search_vector FROM memory_units WHERE id = $1", m1)
|
||||
assert reverted_sv is not None, "search_vector recomputed on revert (archive keeps none)"
|
||||
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Pure unit tests for the OKF (Open Knowledge Format) serializer.
|
||||
|
||||
These exercise hindsight_api/api/okf.py with plain dicts — no DB, no LLM — so
|
||||
they pin the OKF contract (frontmatter projection, type-from-tag, shared-tag
|
||||
graph) deterministically and fast.
|
||||
"""
|
||||
|
||||
from hindsight_api.api import okf
|
||||
|
||||
|
||||
def _mm(**overrides):
|
||||
base = {
|
||||
"id": "orders",
|
||||
"name": "Orders",
|
||||
"source_query": "What are the order facts?",
|
||||
"content": "# Orders\n\nOne row per order.",
|
||||
"tags": ["type:runbook", "sales", "revenue"],
|
||||
"last_refreshed_at": "2026-01-02T00:00:00Z",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
class TestPageType:
|
||||
def test_lifts_type_from_tag_and_drops_it(self):
|
||||
pt = okf.page_type(["type:runbook", "sales", "revenue"])
|
||||
assert pt.type == "runbook"
|
||||
assert pt.display_tags == ["sales", "revenue"]
|
||||
|
||||
def test_defaults_when_no_type_tag(self):
|
||||
pt = okf.page_type(["sales"])
|
||||
assert pt.type == okf.DEFAULT_PAGE_TYPE
|
||||
assert pt.display_tags == ["sales"]
|
||||
|
||||
def test_handles_none_and_empty(self):
|
||||
assert okf.page_type(None).type == okf.DEFAULT_PAGE_TYPE
|
||||
assert okf.page_type(None).display_tags == []
|
||||
|
||||
def test_blank_type_suffix_falls_back(self):
|
||||
pt = okf.page_type(["type:", "sales"])
|
||||
assert pt.type == okf.DEFAULT_PAGE_TYPE
|
||||
# the (blank) type tag is still stripped from display tags
|
||||
assert pt.display_tags == ["sales"]
|
||||
|
||||
def test_first_type_tag_wins(self):
|
||||
pt = okf.page_type(["type:runbook", "type:guide"])
|
||||
assert pt.type == "runbook"
|
||||
assert pt.display_tags == []
|
||||
|
||||
|
||||
class TestFrontmatter:
|
||||
def test_projects_expected_fields(self):
|
||||
fm = okf.frontmatter(_mm())
|
||||
assert fm["id"] == "orders"
|
||||
assert fm["type"] == "runbook"
|
||||
assert fm["title"] == "Orders"
|
||||
assert fm["description"] == "What are the order facts?"
|
||||
assert fm["tags"] == ["sales", "revenue"]
|
||||
assert fm["timestamp"] == "2026-01-02T00:00:00Z"
|
||||
|
||||
def test_timestamp_falls_back_to_created_at(self):
|
||||
fm = okf.frontmatter(_mm(last_refreshed_at=None))
|
||||
assert fm["timestamp"] == "2026-01-01T00:00:00Z"
|
||||
|
||||
def test_render_omits_none_and_empty(self):
|
||||
rendered = okf.render_frontmatter({"type": "x", "title": None, "tags": []})
|
||||
assert "title" not in rendered
|
||||
assert "tags" not in rendered
|
||||
assert 'type: "x"' in rendered
|
||||
|
||||
def test_render_quotes_and_escapes(self):
|
||||
# A name that looks like a YAML bool / contains a quote must stay a string.
|
||||
rendered = okf.render_frontmatter({"title": 'true "x"'})
|
||||
assert 'title: "true \\"x\\""' in rendered
|
||||
|
||||
|
||||
class TestRenderDocument:
|
||||
def test_includes_frontmatter_and_body(self):
|
||||
doc = okf.render_document(_mm())
|
||||
assert doc.startswith("---\n")
|
||||
assert 'type: "runbook"' in doc
|
||||
assert "One row per order." in doc
|
||||
|
||||
def test_empty_body(self):
|
||||
doc = okf.render_document(_mm(content=""))
|
||||
assert doc.count("---") == 2
|
||||
assert doc.rstrip().endswith("---")
|
||||
|
||||
|
||||
class TestKnowledgeGraph:
|
||||
def test_edge_from_shared_tag(self):
|
||||
pages = [
|
||||
_mm(id="orders", tags=["type:runbook", "sales", "revenue"]),
|
||||
_mm(id="customers", tags=["sales", "crm"]),
|
||||
_mm(id="lonely", tags=[]),
|
||||
]
|
||||
graph = okf.knowledge_graph(pages)
|
||||
assert len(graph.nodes) == 3
|
||||
assert len(graph.edges) == 1
|
||||
edge = graph.edges[0]["data"]
|
||||
assert {edge["source"], edge["target"]} == {"orders", "customers"}
|
||||
assert edge["sharedTags"] == ["sales"]
|
||||
assert edge["weight"] == 1
|
||||
|
||||
def test_type_tag_does_not_create_edges(self):
|
||||
# Two pages sharing only a type: tag must NOT be linked.
|
||||
pages = [
|
||||
_mm(id="a", tags=["type:runbook"]),
|
||||
_mm(id="b", tags=["type:runbook"]),
|
||||
]
|
||||
graph = okf.knowledge_graph(pages)
|
||||
assert graph.edges == []
|
||||
|
||||
def test_node_carries_type_and_color(self):
|
||||
graph = okf.knowledge_graph([_mm(id="orders", tags=["type:runbook", "sales"])])
|
||||
node = graph.nodes[0]["data"]
|
||||
assert node["type"] == "runbook"
|
||||
assert node["label"] == "Orders"
|
||||
assert node["tagCount"] == 1
|
||||
assert node["color"].startswith("#")
|
||||
|
||||
def test_weight_counts_shared_tags(self):
|
||||
pages = [
|
||||
_mm(id="a", tags=["sales", "revenue", "x"]),
|
||||
_mm(id="b", tags=["sales", "revenue", "y"]),
|
||||
]
|
||||
graph = okf.knowledge_graph(pages)
|
||||
assert graph.edges[0]["data"]["weight"] == 2
|
||||
assert graph.edges[0]["data"]["sharedTags"] == ["revenue", "sales"]
|
||||
|
||||
|
||||
class TestReservedFiles:
|
||||
def test_index_links_each_page(self):
|
||||
index = okf.render_index([_mm(id="orders", name="Orders", source_query="q?")])
|
||||
assert "[Orders](./orders.md)" in index
|
||||
assert "q?" in index
|
||||
assert 'type: "index"' in index
|
||||
|
||||
def test_index_empty(self):
|
||||
assert "No knowledge pages yet" in okf.render_index([])
|
||||
|
||||
def test_index_nests_folders(self):
|
||||
nodes = [
|
||||
{"id": "f1", "kind": "folder", "name": "Runbooks", "parent_id": None},
|
||||
{"id": "p1", "kind": "page", "name": "Orders", "parent_id": "f1", "source_query": "q?"},
|
||||
{"id": "p2", "kind": "page", "name": "Loose", "parent_id": None},
|
||||
]
|
||||
idx = okf.render_index(nodes)
|
||||
assert "**Runbooks/**" in idx
|
||||
# the page nested in the folder is indented and links to its file
|
||||
assert " - [Orders](./p1.md) — q?" in idx
|
||||
assert "- [Loose](./p2.md)" in idx
|
||||
|
||||
def test_log_renders_history_newest_first(self):
|
||||
history = [
|
||||
{"previous_content": "v2", "changed_at": "2026-01-02T00:00:00Z"},
|
||||
{"previous_content": "v1", "changed_at": "2026-01-01T00:00:00Z"},
|
||||
]
|
||||
log = okf.render_log(_mm(), history)
|
||||
assert 'type: "log"' in log
|
||||
assert log.index("2026-01-02") < log.index("2026-01-01")
|
||||
assert "v2" in log and "v1" in log
|
||||
|
||||
def test_log_empty(self):
|
||||
assert "No refresh history" in okf.render_log(_mm(), [])
|
||||
@@ -1382,6 +1382,348 @@ paths:
|
||||
summary: Clear mental model content
|
||||
tags:
|
||||
- Mental Models
|
||||
/v1/default/banks/{bank_id}/knowledge-base/tree:
|
||||
get:
|
||||
description: Return the knowledge base as a nested tree of folders and pages.
|
||||
operationId: get_knowledge_base_tree
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeTreeResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Get the knowledge-base tree
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/folders:
|
||||
post:
|
||||
description: "Create a folder, optionally nested under a parent folder."
|
||||
operationId: create_knowledge_folder
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateFolderRequest'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Create a knowledge-base folder
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/pages:
|
||||
post:
|
||||
description: Create a page (a mental model + tree node). Content is generated
|
||||
asynchronously; use the returned operation_id to track completion.
|
||||
operationId: create_knowledge_page
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreatePageRequest'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateKnowledgePageResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Create a knowledge-base page
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/graph:
|
||||
get:
|
||||
description: "Return pages as nodes linked by shared tags, for the constellation\
|
||||
\ view."
|
||||
operationId: get_knowledge_base_graph
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgePageGraphResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Knowledge-base constellation graph
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/export:
|
||||
get:
|
||||
description: "Return a portable OKF bundle: a nested index.md, one <id>.md per\
|
||||
\ page, and history logs."
|
||||
operationId: export_knowledge_base
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgePageBundleResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Export the knowledge base as an OKF bundle
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}:
|
||||
get:
|
||||
description: Return a single page as an OKF document (frontmatter + markdown
|
||||
body).
|
||||
operationId: get_knowledge_page
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: path
|
||||
name: page_id
|
||||
required: true
|
||||
schema:
|
||||
title: Page Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgePageResponse'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Get a knowledge-base page
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}:
|
||||
delete:
|
||||
description: Delete a folder or page and its whole subtree (pages' mental models
|
||||
are removed too).
|
||||
operationId: delete_knowledge_node
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: path
|
||||
name: node_id
|
||||
required: true
|
||||
schema:
|
||||
title: Node Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Delete a knowledge-base node
|
||||
tags:
|
||||
- Knowledge Base
|
||||
patch:
|
||||
description: "Rename a node (set `name`) and/or move it under another folder\
|
||||
\ (set `parent_id`, null for the root)."
|
||||
operationId: update_knowledge_node
|
||||
parameters:
|
||||
- explode: false
|
||||
in: path
|
||||
name: bank_id
|
||||
required: true
|
||||
schema:
|
||||
title: Bank Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: path
|
||||
name: node_id
|
||||
required: true
|
||||
schema:
|
||||
title: Node Id
|
||||
type: string
|
||||
style: simple
|
||||
- explode: false
|
||||
in: header
|
||||
name: authorization
|
||||
required: false
|
||||
schema:
|
||||
nullable: true
|
||||
type: string
|
||||
style: simple
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateNodeRequest'
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
description: Successful Response
|
||||
"422":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HTTPValidationError'
|
||||
description: Validation Error
|
||||
summary: Rename or move a knowledge-base node
|
||||
tags:
|
||||
- Knowledge Base
|
||||
/v1/default/banks/{bank_id}/directives:
|
||||
get:
|
||||
description: List hard rules that are injected into prompts.
|
||||
@@ -5095,6 +5437,42 @@ components:
|
||||
- content
|
||||
- name
|
||||
title: CreateDirectiveRequest
|
||||
CreateFolderRequest:
|
||||
description: Create a folder under an optional parent folder.
|
||||
example:
|
||||
parent_id: parent_id
|
||||
name: name
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
title: CreateFolderRequest
|
||||
CreateKnowledgePageResponse:
|
||||
description: "Result of creating a page: the node id, its mental model, and\
|
||||
\ the refresh op."
|
||||
example:
|
||||
page_id: page_id
|
||||
operation_id: operation_id
|
||||
mental_model_id: mental_model_id
|
||||
properties:
|
||||
page_id:
|
||||
title: Page Id
|
||||
type: string
|
||||
mental_model_id:
|
||||
title: Mental Model Id
|
||||
type: string
|
||||
operation_id:
|
||||
nullable: true
|
||||
type: string
|
||||
required:
|
||||
- mental_model_id
|
||||
- page_id
|
||||
title: CreateKnowledgePageResponse
|
||||
CreateMentalModelRequest:
|
||||
description: Request model for creating a mental model.
|
||||
example:
|
||||
@@ -5153,6 +5531,64 @@ components:
|
||||
required:
|
||||
- operation_id
|
||||
title: CreateMentalModelResponse
|
||||
CreatePageRequest:
|
||||
description: Create a page (a mental model + tree node) under an optional parent
|
||||
folder.
|
||||
example:
|
||||
source_query: source_query
|
||||
max_tokens: 0
|
||||
parent_id: parent_id
|
||||
name: name
|
||||
trigger:
|
||||
mode: full
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
fact_types:
|
||||
- world
|
||||
- world
|
||||
exclude_mental_model_ids:
|
||||
- exclude_mental_model_ids
|
||||
- exclude_mental_model_ids
|
||||
include_chunks: true
|
||||
tags_match: any
|
||||
exclude_mental_models: false
|
||||
recall_max_tokens: 6
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
properties:
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
source_query:
|
||||
title: Source Query
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
max_tokens:
|
||||
nullable: true
|
||||
type: integer
|
||||
trigger:
|
||||
$ref: '#/components/schemas/MentalModelTrigger-Input'
|
||||
required:
|
||||
- name
|
||||
- source_query
|
||||
title: CreatePageRequest
|
||||
CreateWebhookRequest:
|
||||
description: Request model for registering a webhook.
|
||||
example:
|
||||
@@ -5956,6 +6392,226 @@ components:
|
||||
source_facts:
|
||||
$ref: '#/components/schemas/SourceFactsIncludeOptions'
|
||||
title: IncludeOptions
|
||||
KnowledgeNode:
|
||||
description: |-
|
||||
A node in the knowledge-base tree — a folder or a page.
|
||||
|
||||
Pages carry ``description``/``tags`` from their backing mental model. The
|
||||
knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node
|
||||
as system-owned vs. hand-authored.
|
||||
example:
|
||||
children:
|
||||
- null
|
||||
- null
|
||||
kind: folder
|
||||
parent_id: parent_id
|
||||
managed: false
|
||||
name: name
|
||||
description: description
|
||||
id: id
|
||||
mental_model_id: mental_model_id
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
properties:
|
||||
id:
|
||||
title: Id
|
||||
type: string
|
||||
kind:
|
||||
enum:
|
||||
- folder
|
||||
- page
|
||||
title: Kind
|
||||
type: string
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
mental_model_id:
|
||||
nullable: true
|
||||
type: string
|
||||
managed:
|
||||
default: false
|
||||
description: "Client-set flag: true = system-owned, false = hand-authored."
|
||||
title: Managed
|
||||
type: boolean
|
||||
description:
|
||||
nullable: true
|
||||
type: string
|
||||
tags:
|
||||
default: []
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
timestamp:
|
||||
nullable: true
|
||||
type: string
|
||||
children:
|
||||
default: []
|
||||
items:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
type: array
|
||||
required:
|
||||
- id
|
||||
- kind
|
||||
- name
|
||||
title: KnowledgeNode
|
||||
KnowledgePageBundleFile:
|
||||
description: One file in a portable OKF bundle.
|
||||
example:
|
||||
path: path
|
||||
content: content
|
||||
properties:
|
||||
path:
|
||||
title: Path
|
||||
type: string
|
||||
content:
|
||||
title: Content
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- path
|
||||
title: KnowledgePageBundleFile
|
||||
KnowledgePageBundleResponse:
|
||||
description: A portable OKF bundle — a flat set of markdown files (index + pages
|
||||
+ logs).
|
||||
example:
|
||||
files:
|
||||
- path: path
|
||||
content: content
|
||||
- path: path
|
||||
content: content
|
||||
properties:
|
||||
files:
|
||||
items:
|
||||
$ref: '#/components/schemas/KnowledgePageBundleFile'
|
||||
type: array
|
||||
required:
|
||||
- files
|
||||
title: KnowledgePageBundleResponse
|
||||
KnowledgePageGraphResponse:
|
||||
description: Constellation graph of knowledge pages linked by shared tags.
|
||||
example:
|
||||
total_edges: 6
|
||||
nodes:
|
||||
- key: ""
|
||||
- key: ""
|
||||
edges:
|
||||
- key: ""
|
||||
- key: ""
|
||||
total_pages: 0
|
||||
properties:
|
||||
nodes:
|
||||
items:
|
||||
additionalProperties: {}
|
||||
type: array
|
||||
edges:
|
||||
items:
|
||||
additionalProperties: {}
|
||||
type: array
|
||||
total_pages:
|
||||
title: Total Pages
|
||||
type: integer
|
||||
total_edges:
|
||||
title: Total Edges
|
||||
type: integer
|
||||
required:
|
||||
- edges
|
||||
- nodes
|
||||
- total_edges
|
||||
- total_pages
|
||||
title: KnowledgePageGraphResponse
|
||||
KnowledgePageResponse:
|
||||
description: A knowledge page rendered as an OKF document.
|
||||
example:
|
||||
name: name
|
||||
markdown: markdown
|
||||
description: description
|
||||
id: id
|
||||
type: type
|
||||
body: body
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
properties:
|
||||
id:
|
||||
title: Id
|
||||
type: string
|
||||
name:
|
||||
title: Name
|
||||
type: string
|
||||
type:
|
||||
description: "OKF document type — from a `type:<x>` tag, else 'knowledge-page'."
|
||||
title: Type
|
||||
type: string
|
||||
description:
|
||||
nullable: true
|
||||
type: string
|
||||
tags:
|
||||
default: []
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
timestamp:
|
||||
nullable: true
|
||||
type: string
|
||||
body:
|
||||
nullable: true
|
||||
type: string
|
||||
markdown:
|
||||
description: "The full OKF document: YAML frontmatter + markdown body."
|
||||
title: Markdown
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- markdown
|
||||
- name
|
||||
- type
|
||||
title: KnowledgePageResponse
|
||||
KnowledgeTreeResponse:
|
||||
description: The knowledge base as a nested folder/page tree.
|
||||
example:
|
||||
roots:
|
||||
- children:
|
||||
- null
|
||||
- null
|
||||
kind: folder
|
||||
parent_id: parent_id
|
||||
managed: false
|
||||
name: name
|
||||
description: description
|
||||
id: id
|
||||
mental_model_id: mental_model_id
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
- children:
|
||||
- null
|
||||
- null
|
||||
kind: folder
|
||||
parent_id: parent_id
|
||||
managed: false
|
||||
name: name
|
||||
description: description
|
||||
id: id
|
||||
mental_model_id: mental_model_id
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
timestamp: timestamp
|
||||
properties:
|
||||
roots:
|
||||
items:
|
||||
$ref: '#/components/schemas/KnowledgeNode'
|
||||
type: array
|
||||
required:
|
||||
- roots
|
||||
title: KnowledgeTreeResponse
|
||||
LLMRequestEntry:
|
||||
description: "A single LLM request trace row, as returned by the read API."
|
||||
example:
|
||||
@@ -6741,6 +7397,29 @@ components:
|
||||
title: MentalModelResponse
|
||||
MentalModelTrigger-Input:
|
||||
description: Trigger settings for a mental model.
|
||||
example:
|
||||
mode: full
|
||||
refresh_after_consolidation: false
|
||||
recall_chunks_max_tokens: 1
|
||||
tag_groups:
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
- match: any_strict
|
||||
tags:
|
||||
- tags
|
||||
- tags
|
||||
fact_types:
|
||||
- world
|
||||
- world
|
||||
exclude_mental_model_ids:
|
||||
- exclude_mental_model_ids
|
||||
- exclude_mental_model_ids
|
||||
include_chunks: true
|
||||
tags_match: any
|
||||
exclude_mental_models: false
|
||||
recall_max_tokens: 6
|
||||
properties:
|
||||
mode:
|
||||
default: full
|
||||
@@ -8114,6 +8793,19 @@ components:
|
||||
trigger:
|
||||
$ref: '#/components/schemas/MentalModelTrigger-Input'
|
||||
title: UpdateMentalModelRequest
|
||||
UpdateNodeRequest:
|
||||
description: Rename and/or move a node. Each field applies only when present.
|
||||
example:
|
||||
parent_id: parent_id
|
||||
name: name
|
||||
properties:
|
||||
name:
|
||||
nullable: true
|
||||
type: string
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: string
|
||||
title: UpdateNodeRequest
|
||||
UpdateWebhookRequest:
|
||||
description: Request model for updating a webhook. Only provided fields are
|
||||
updated.
|
||||
|
||||
@@ -65,6 +65,8 @@ type APIClient struct {
|
||||
|
||||
FilesAPI *FilesAPIService
|
||||
|
||||
KnowledgeBaseAPI *KnowledgeBaseAPIService
|
||||
|
||||
LLMTracesAPI *LLMTracesAPIService
|
||||
|
||||
MemoryAPI *MemoryAPIService
|
||||
@@ -102,6 +104,7 @@ func NewAPIClient(cfg *Configuration) *APIClient {
|
||||
c.DocumentsAPI = (*DocumentsAPIService)(&c.common)
|
||||
c.EntitiesAPI = (*EntitiesAPIService)(&c.common)
|
||||
c.FilesAPI = (*FilesAPIService)(&c.common)
|
||||
c.KnowledgeBaseAPI = (*KnowledgeBaseAPIService)(&c.common)
|
||||
c.LLMTracesAPI = (*LLMTracesAPIService)(&c.common)
|
||||
c.MemoryAPI = (*MemoryAPIService)(&c.common)
|
||||
c.MentalModelsAPI = (*MentalModelsAPIService)(&c.common)
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the CreateFolderRequest type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &CreateFolderRequest{}
|
||||
|
||||
// CreateFolderRequest Create a folder under an optional parent folder.
|
||||
type CreateFolderRequest struct {
|
||||
Name string `json:"name"`
|
||||
ParentId NullableString `json:"parent_id,omitempty"`
|
||||
}
|
||||
|
||||
type _CreateFolderRequest CreateFolderRequest
|
||||
|
||||
// NewCreateFolderRequest instantiates a new CreateFolderRequest object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewCreateFolderRequest(name string) *CreateFolderRequest {
|
||||
this := CreateFolderRequest{}
|
||||
this.Name = name
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewCreateFolderRequestWithDefaults instantiates a new CreateFolderRequest object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewCreateFolderRequestWithDefaults() *CreateFolderRequest {
|
||||
this := CreateFolderRequest{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetName returns the Name field value
|
||||
func (o *CreateFolderRequest) GetName() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Name
|
||||
}
|
||||
|
||||
// GetNameOk returns a tuple with the Name field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *CreateFolderRequest) GetNameOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Name, true
|
||||
}
|
||||
|
||||
// SetName sets field value
|
||||
func (o *CreateFolderRequest) SetName(v string) {
|
||||
o.Name = v
|
||||
}
|
||||
|
||||
// GetParentId returns the ParentId field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *CreateFolderRequest) GetParentId() string {
|
||||
if o == nil || IsNil(o.ParentId.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.ParentId.Get()
|
||||
}
|
||||
|
||||
// GetParentIdOk returns a tuple with the ParentId 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 *CreateFolderRequest) GetParentIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ParentId.Get(), o.ParentId.IsSet()
|
||||
}
|
||||
|
||||
// HasParentId returns a boolean if a field has been set.
|
||||
func (o *CreateFolderRequest) HasParentId() bool {
|
||||
if o != nil && o.ParentId.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetParentId gets a reference to the given NullableString and assigns it to the ParentId field.
|
||||
func (o *CreateFolderRequest) SetParentId(v string) {
|
||||
o.ParentId.Set(&v)
|
||||
}
|
||||
// SetParentIdNil sets the value for ParentId to be an explicit nil
|
||||
func (o *CreateFolderRequest) SetParentIdNil() {
|
||||
o.ParentId.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetParentId ensures that no value is present for ParentId, not even an explicit nil
|
||||
func (o *CreateFolderRequest) UnsetParentId() {
|
||||
o.ParentId.Unset()
|
||||
}
|
||||
|
||||
func (o CreateFolderRequest) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o CreateFolderRequest) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["name"] = o.Name
|
||||
if o.ParentId.IsSet() {
|
||||
toSerialize["parent_id"] = o.ParentId.Get()
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *CreateFolderRequest) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"name",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varCreateFolderRequest := _CreateFolderRequest{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varCreateFolderRequest)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = CreateFolderRequest(varCreateFolderRequest)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableCreateFolderRequest struct {
|
||||
value *CreateFolderRequest
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableCreateFolderRequest) Get() *CreateFolderRequest {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableCreateFolderRequest) Set(val *CreateFolderRequest) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableCreateFolderRequest) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableCreateFolderRequest) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableCreateFolderRequest(val *CreateFolderRequest) *NullableCreateFolderRequest {
|
||||
return &NullableCreateFolderRequest{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableCreateFolderRequest) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableCreateFolderRequest) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the CreateKnowledgePageResponse type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &CreateKnowledgePageResponse{}
|
||||
|
||||
// CreateKnowledgePageResponse Result of creating a page: the node id, its mental model, and the refresh op.
|
||||
type CreateKnowledgePageResponse struct {
|
||||
PageId string `json:"page_id"`
|
||||
MentalModelId string `json:"mental_model_id"`
|
||||
OperationId NullableString `json:"operation_id,omitempty"`
|
||||
}
|
||||
|
||||
type _CreateKnowledgePageResponse CreateKnowledgePageResponse
|
||||
|
||||
// NewCreateKnowledgePageResponse instantiates a new CreateKnowledgePageResponse object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewCreateKnowledgePageResponse(pageId string, mentalModelId string) *CreateKnowledgePageResponse {
|
||||
this := CreateKnowledgePageResponse{}
|
||||
this.PageId = pageId
|
||||
this.MentalModelId = mentalModelId
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewCreateKnowledgePageResponseWithDefaults instantiates a new CreateKnowledgePageResponse object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewCreateKnowledgePageResponseWithDefaults() *CreateKnowledgePageResponse {
|
||||
this := CreateKnowledgePageResponse{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetPageId returns the PageId field value
|
||||
func (o *CreateKnowledgePageResponse) GetPageId() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.PageId
|
||||
}
|
||||
|
||||
// GetPageIdOk returns a tuple with the PageId field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *CreateKnowledgePageResponse) GetPageIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.PageId, true
|
||||
}
|
||||
|
||||
// SetPageId sets field value
|
||||
func (o *CreateKnowledgePageResponse) SetPageId(v string) {
|
||||
o.PageId = v
|
||||
}
|
||||
|
||||
// GetMentalModelId returns the MentalModelId field value
|
||||
func (o *CreateKnowledgePageResponse) GetMentalModelId() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.MentalModelId
|
||||
}
|
||||
|
||||
// GetMentalModelIdOk returns a tuple with the MentalModelId field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *CreateKnowledgePageResponse) GetMentalModelIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.MentalModelId, true
|
||||
}
|
||||
|
||||
// SetMentalModelId sets field value
|
||||
func (o *CreateKnowledgePageResponse) SetMentalModelId(v string) {
|
||||
o.MentalModelId = v
|
||||
}
|
||||
|
||||
// GetOperationId returns the OperationId field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *CreateKnowledgePageResponse) GetOperationId() string {
|
||||
if o == nil || IsNil(o.OperationId.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.OperationId.Get()
|
||||
}
|
||||
|
||||
// GetOperationIdOk returns a tuple with the OperationId 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 *CreateKnowledgePageResponse) GetOperationIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.OperationId.Get(), o.OperationId.IsSet()
|
||||
}
|
||||
|
||||
// HasOperationId returns a boolean if a field has been set.
|
||||
func (o *CreateKnowledgePageResponse) HasOperationId() bool {
|
||||
if o != nil && o.OperationId.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetOperationId gets a reference to the given NullableString and assigns it to the OperationId field.
|
||||
func (o *CreateKnowledgePageResponse) SetOperationId(v string) {
|
||||
o.OperationId.Set(&v)
|
||||
}
|
||||
// SetOperationIdNil sets the value for OperationId to be an explicit nil
|
||||
func (o *CreateKnowledgePageResponse) SetOperationIdNil() {
|
||||
o.OperationId.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetOperationId ensures that no value is present for OperationId, not even an explicit nil
|
||||
func (o *CreateKnowledgePageResponse) UnsetOperationId() {
|
||||
o.OperationId.Unset()
|
||||
}
|
||||
|
||||
func (o CreateKnowledgePageResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o CreateKnowledgePageResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["page_id"] = o.PageId
|
||||
toSerialize["mental_model_id"] = o.MentalModelId
|
||||
if o.OperationId.IsSet() {
|
||||
toSerialize["operation_id"] = o.OperationId.Get()
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *CreateKnowledgePageResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"page_id",
|
||||
"mental_model_id",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varCreateKnowledgePageResponse := _CreateKnowledgePageResponse{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varCreateKnowledgePageResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = CreateKnowledgePageResponse(varCreateKnowledgePageResponse)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableCreateKnowledgePageResponse struct {
|
||||
value *CreateKnowledgePageResponse
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableCreateKnowledgePageResponse) Get() *CreateKnowledgePageResponse {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableCreateKnowledgePageResponse) Set(val *CreateKnowledgePageResponse) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableCreateKnowledgePageResponse) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableCreateKnowledgePageResponse) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableCreateKnowledgePageResponse(val *CreateKnowledgePageResponse) *NullableCreateKnowledgePageResponse {
|
||||
return &NullableCreateKnowledgePageResponse{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableCreateKnowledgePageResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableCreateKnowledgePageResponse) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the CreatePageRequest type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &CreatePageRequest{}
|
||||
|
||||
// CreatePageRequest Create a page (a mental model + tree node) under an optional parent folder.
|
||||
type CreatePageRequest struct {
|
||||
Name string `json:"name"`
|
||||
SourceQuery string `json:"source_query"`
|
||||
ParentId NullableString `json:"parent_id,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
MaxTokens NullableInt32 `json:"max_tokens,omitempty"`
|
||||
Trigger NullableMentalModelTriggerInput `json:"trigger,omitempty"`
|
||||
}
|
||||
|
||||
type _CreatePageRequest CreatePageRequest
|
||||
|
||||
// NewCreatePageRequest instantiates a new CreatePageRequest object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewCreatePageRequest(name string, sourceQuery string) *CreatePageRequest {
|
||||
this := CreatePageRequest{}
|
||||
this.Name = name
|
||||
this.SourceQuery = sourceQuery
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewCreatePageRequestWithDefaults instantiates a new CreatePageRequest object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewCreatePageRequestWithDefaults() *CreatePageRequest {
|
||||
this := CreatePageRequest{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetName returns the Name field value
|
||||
func (o *CreatePageRequest) GetName() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Name
|
||||
}
|
||||
|
||||
// GetNameOk returns a tuple with the Name field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *CreatePageRequest) GetNameOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Name, true
|
||||
}
|
||||
|
||||
// SetName sets field value
|
||||
func (o *CreatePageRequest) SetName(v string) {
|
||||
o.Name = v
|
||||
}
|
||||
|
||||
// GetSourceQuery returns the SourceQuery field value
|
||||
func (o *CreatePageRequest) GetSourceQuery() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.SourceQuery
|
||||
}
|
||||
|
||||
// GetSourceQueryOk returns a tuple with the SourceQuery field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *CreatePageRequest) GetSourceQueryOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.SourceQuery, true
|
||||
}
|
||||
|
||||
// SetSourceQuery sets field value
|
||||
func (o *CreatePageRequest) SetSourceQuery(v string) {
|
||||
o.SourceQuery = v
|
||||
}
|
||||
|
||||
// GetParentId returns the ParentId field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *CreatePageRequest) GetParentId() string {
|
||||
if o == nil || IsNil(o.ParentId.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.ParentId.Get()
|
||||
}
|
||||
|
||||
// GetParentIdOk returns a tuple with the ParentId 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 *CreatePageRequest) GetParentIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ParentId.Get(), o.ParentId.IsSet()
|
||||
}
|
||||
|
||||
// HasParentId returns a boolean if a field has been set.
|
||||
func (o *CreatePageRequest) HasParentId() bool {
|
||||
if o != nil && o.ParentId.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetParentId gets a reference to the given NullableString and assigns it to the ParentId field.
|
||||
func (o *CreatePageRequest) SetParentId(v string) {
|
||||
o.ParentId.Set(&v)
|
||||
}
|
||||
// SetParentIdNil sets the value for ParentId to be an explicit nil
|
||||
func (o *CreatePageRequest) SetParentIdNil() {
|
||||
o.ParentId.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetParentId ensures that no value is present for ParentId, not even an explicit nil
|
||||
func (o *CreatePageRequest) UnsetParentId() {
|
||||
o.ParentId.Unset()
|
||||
}
|
||||
|
||||
// GetTags returns the Tags field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *CreatePageRequest) GetTags() []string {
|
||||
if o == nil {
|
||||
var ret []string
|
||||
return ret
|
||||
}
|
||||
return o.Tags
|
||||
}
|
||||
|
||||
// GetTagsOk returns a tuple with the Tags 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 *CreatePageRequest) GetTagsOk() ([]string, bool) {
|
||||
if o == nil || IsNil(o.Tags) {
|
||||
return nil, false
|
||||
}
|
||||
return o.Tags, true
|
||||
}
|
||||
|
||||
// HasTags returns a boolean if a field has been set.
|
||||
func (o *CreatePageRequest) HasTags() bool {
|
||||
if o != nil && !IsNil(o.Tags) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetTags gets a reference to the given []string and assigns it to the Tags field.
|
||||
func (o *CreatePageRequest) SetTags(v []string) {
|
||||
o.Tags = v
|
||||
}
|
||||
|
||||
// GetMaxTokens returns the MaxTokens field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *CreatePageRequest) GetMaxTokens() int32 {
|
||||
if o == nil || IsNil(o.MaxTokens.Get()) {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
return *o.MaxTokens.Get()
|
||||
}
|
||||
|
||||
// GetMaxTokensOk returns a tuple with the MaxTokens field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *CreatePageRequest) GetMaxTokensOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.MaxTokens.Get(), o.MaxTokens.IsSet()
|
||||
}
|
||||
|
||||
// HasMaxTokens returns a boolean if a field has been set.
|
||||
func (o *CreatePageRequest) HasMaxTokens() bool {
|
||||
if o != nil && o.MaxTokens.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetMaxTokens gets a reference to the given NullableInt32 and assigns it to the MaxTokens field.
|
||||
func (o *CreatePageRequest) SetMaxTokens(v int32) {
|
||||
o.MaxTokens.Set(&v)
|
||||
}
|
||||
// SetMaxTokensNil sets the value for MaxTokens to be an explicit nil
|
||||
func (o *CreatePageRequest) SetMaxTokensNil() {
|
||||
o.MaxTokens.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetMaxTokens ensures that no value is present for MaxTokens, not even an explicit nil
|
||||
func (o *CreatePageRequest) UnsetMaxTokens() {
|
||||
o.MaxTokens.Unset()
|
||||
}
|
||||
|
||||
// GetTrigger returns the Trigger field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *CreatePageRequest) GetTrigger() MentalModelTriggerInput {
|
||||
if o == nil || IsNil(o.Trigger.Get()) {
|
||||
var ret MentalModelTriggerInput
|
||||
return ret
|
||||
}
|
||||
return *o.Trigger.Get()
|
||||
}
|
||||
|
||||
// GetTriggerOk returns a tuple with the Trigger field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *CreatePageRequest) GetTriggerOk() (*MentalModelTriggerInput, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Trigger.Get(), o.Trigger.IsSet()
|
||||
}
|
||||
|
||||
// HasTrigger returns a boolean if a field has been set.
|
||||
func (o *CreatePageRequest) HasTrigger() bool {
|
||||
if o != nil && o.Trigger.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetTrigger gets a reference to the given NullableMentalModelTriggerInput and assigns it to the Trigger field.
|
||||
func (o *CreatePageRequest) SetTrigger(v MentalModelTriggerInput) {
|
||||
o.Trigger.Set(&v)
|
||||
}
|
||||
// SetTriggerNil sets the value for Trigger to be an explicit nil
|
||||
func (o *CreatePageRequest) SetTriggerNil() {
|
||||
o.Trigger.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetTrigger ensures that no value is present for Trigger, not even an explicit nil
|
||||
func (o *CreatePageRequest) UnsetTrigger() {
|
||||
o.Trigger.Unset()
|
||||
}
|
||||
|
||||
func (o CreatePageRequest) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o CreatePageRequest) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["name"] = o.Name
|
||||
toSerialize["source_query"] = o.SourceQuery
|
||||
if o.ParentId.IsSet() {
|
||||
toSerialize["parent_id"] = o.ParentId.Get()
|
||||
}
|
||||
if o.Tags != nil {
|
||||
toSerialize["tags"] = o.Tags
|
||||
}
|
||||
if o.MaxTokens.IsSet() {
|
||||
toSerialize["max_tokens"] = o.MaxTokens.Get()
|
||||
}
|
||||
if o.Trigger.IsSet() {
|
||||
toSerialize["trigger"] = o.Trigger.Get()
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *CreatePageRequest) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"name",
|
||||
"source_query",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varCreatePageRequest := _CreatePageRequest{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varCreatePageRequest)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = CreatePageRequest(varCreatePageRequest)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableCreatePageRequest struct {
|
||||
value *CreatePageRequest
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableCreatePageRequest) Get() *CreatePageRequest {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableCreatePageRequest) Set(val *CreatePageRequest) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableCreatePageRequest) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableCreatePageRequest) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableCreatePageRequest(val *CreatePageRequest) *NullableCreatePageRequest {
|
||||
return &NullableCreatePageRequest{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableCreatePageRequest) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableCreatePageRequest) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the KnowledgeNode type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &KnowledgeNode{}
|
||||
|
||||
// KnowledgeNode A node in the knowledge-base tree — a folder or a page. Pages carry ``description``/``tags`` from their backing mental model. The knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node as system-owned vs. hand-authored.
|
||||
type KnowledgeNode struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
ParentId NullableString `json:"parent_id,omitempty"`
|
||||
MentalModelId NullableString `json:"mental_model_id,omitempty"`
|
||||
// Client-set flag: true = system-owned, false = hand-authored.
|
||||
Managed *bool `json:"managed,omitempty"`
|
||||
Description NullableString `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Timestamp NullableString `json:"timestamp,omitempty"`
|
||||
Children []KnowledgeNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type _KnowledgeNode KnowledgeNode
|
||||
|
||||
// NewKnowledgeNode instantiates a new KnowledgeNode object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewKnowledgeNode(id string, kind string, name string) *KnowledgeNode {
|
||||
this := KnowledgeNode{}
|
||||
this.Id = id
|
||||
this.Kind = kind
|
||||
this.Name = name
|
||||
var managed bool = false
|
||||
this.Managed = &managed
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewKnowledgeNodeWithDefaults instantiates a new KnowledgeNode object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewKnowledgeNodeWithDefaults() *KnowledgeNode {
|
||||
this := KnowledgeNode{}
|
||||
var managed bool = false
|
||||
this.Managed = &managed
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetId returns the Id field value
|
||||
func (o *KnowledgeNode) GetId() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Id
|
||||
}
|
||||
|
||||
// GetIdOk returns a tuple with the Id field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeNode) GetIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Id, true
|
||||
}
|
||||
|
||||
// SetId sets field value
|
||||
func (o *KnowledgeNode) SetId(v string) {
|
||||
o.Id = v
|
||||
}
|
||||
|
||||
// GetKind returns the Kind field value
|
||||
func (o *KnowledgeNode) GetKind() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Kind
|
||||
}
|
||||
|
||||
// GetKindOk returns a tuple with the Kind field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeNode) GetKindOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Kind, true
|
||||
}
|
||||
|
||||
// SetKind sets field value
|
||||
func (o *KnowledgeNode) SetKind(v string) {
|
||||
o.Kind = v
|
||||
}
|
||||
|
||||
// GetName returns the Name field value
|
||||
func (o *KnowledgeNode) GetName() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Name
|
||||
}
|
||||
|
||||
// GetNameOk returns a tuple with the Name field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeNode) GetNameOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Name, true
|
||||
}
|
||||
|
||||
// SetName sets field value
|
||||
func (o *KnowledgeNode) SetName(v string) {
|
||||
o.Name = v
|
||||
}
|
||||
|
||||
// GetParentId returns the ParentId field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgeNode) GetParentId() string {
|
||||
if o == nil || IsNil(o.ParentId.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.ParentId.Get()
|
||||
}
|
||||
|
||||
// GetParentIdOk returns a tuple with the ParentId 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 *KnowledgeNode) GetParentIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ParentId.Get(), o.ParentId.IsSet()
|
||||
}
|
||||
|
||||
// HasParentId returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasParentId() bool {
|
||||
if o != nil && o.ParentId.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetParentId gets a reference to the given NullableString and assigns it to the ParentId field.
|
||||
func (o *KnowledgeNode) SetParentId(v string) {
|
||||
o.ParentId.Set(&v)
|
||||
}
|
||||
// SetParentIdNil sets the value for ParentId to be an explicit nil
|
||||
func (o *KnowledgeNode) SetParentIdNil() {
|
||||
o.ParentId.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetParentId ensures that no value is present for ParentId, not even an explicit nil
|
||||
func (o *KnowledgeNode) UnsetParentId() {
|
||||
o.ParentId.Unset()
|
||||
}
|
||||
|
||||
// GetMentalModelId returns the MentalModelId field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgeNode) GetMentalModelId() string {
|
||||
if o == nil || IsNil(o.MentalModelId.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.MentalModelId.Get()
|
||||
}
|
||||
|
||||
// GetMentalModelIdOk returns a tuple with the MentalModelId 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 *KnowledgeNode) GetMentalModelIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.MentalModelId.Get(), o.MentalModelId.IsSet()
|
||||
}
|
||||
|
||||
// HasMentalModelId returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasMentalModelId() bool {
|
||||
if o != nil && o.MentalModelId.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetMentalModelId gets a reference to the given NullableString and assigns it to the MentalModelId field.
|
||||
func (o *KnowledgeNode) SetMentalModelId(v string) {
|
||||
o.MentalModelId.Set(&v)
|
||||
}
|
||||
// SetMentalModelIdNil sets the value for MentalModelId to be an explicit nil
|
||||
func (o *KnowledgeNode) SetMentalModelIdNil() {
|
||||
o.MentalModelId.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetMentalModelId ensures that no value is present for MentalModelId, not even an explicit nil
|
||||
func (o *KnowledgeNode) UnsetMentalModelId() {
|
||||
o.MentalModelId.Unset()
|
||||
}
|
||||
|
||||
// GetManaged returns the Managed field value if set, zero value otherwise.
|
||||
func (o *KnowledgeNode) GetManaged() bool {
|
||||
if o == nil || IsNil(o.Managed) {
|
||||
var ret bool
|
||||
return ret
|
||||
}
|
||||
return *o.Managed
|
||||
}
|
||||
|
||||
// GetManagedOk returns a tuple with the Managed field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeNode) GetManagedOk() (*bool, bool) {
|
||||
if o == nil || IsNil(o.Managed) {
|
||||
return nil, false
|
||||
}
|
||||
return o.Managed, true
|
||||
}
|
||||
|
||||
// HasManaged returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasManaged() bool {
|
||||
if o != nil && !IsNil(o.Managed) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetManaged gets a reference to the given bool and assigns it to the Managed field.
|
||||
func (o *KnowledgeNode) SetManaged(v bool) {
|
||||
o.Managed = &v
|
||||
}
|
||||
|
||||
// GetDescription returns the Description field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgeNode) GetDescription() string {
|
||||
if o == nil || IsNil(o.Description.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.Description.Get()
|
||||
}
|
||||
|
||||
// GetDescriptionOk returns a tuple with the Description 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 *KnowledgeNode) GetDescriptionOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Description.Get(), o.Description.IsSet()
|
||||
}
|
||||
|
||||
// HasDescription returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasDescription() bool {
|
||||
if o != nil && o.Description.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDescription gets a reference to the given NullableString and assigns it to the Description field.
|
||||
func (o *KnowledgeNode) SetDescription(v string) {
|
||||
o.Description.Set(&v)
|
||||
}
|
||||
// SetDescriptionNil sets the value for Description to be an explicit nil
|
||||
func (o *KnowledgeNode) SetDescriptionNil() {
|
||||
o.Description.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetDescription ensures that no value is present for Description, not even an explicit nil
|
||||
func (o *KnowledgeNode) UnsetDescription() {
|
||||
o.Description.Unset()
|
||||
}
|
||||
|
||||
// GetTags returns the Tags field value if set, zero value otherwise.
|
||||
func (o *KnowledgeNode) GetTags() []string {
|
||||
if o == nil || IsNil(o.Tags) {
|
||||
var ret []string
|
||||
return ret
|
||||
}
|
||||
return o.Tags
|
||||
}
|
||||
|
||||
// GetTagsOk returns a tuple with the Tags field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeNode) GetTagsOk() ([]string, bool) {
|
||||
if o == nil || IsNil(o.Tags) {
|
||||
return nil, false
|
||||
}
|
||||
return o.Tags, true
|
||||
}
|
||||
|
||||
// HasTags returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasTags() bool {
|
||||
if o != nil && !IsNil(o.Tags) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetTags gets a reference to the given []string and assigns it to the Tags field.
|
||||
func (o *KnowledgeNode) SetTags(v []string) {
|
||||
o.Tags = v
|
||||
}
|
||||
|
||||
// GetTimestamp returns the Timestamp field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgeNode) GetTimestamp() string {
|
||||
if o == nil || IsNil(o.Timestamp.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.Timestamp.Get()
|
||||
}
|
||||
|
||||
// GetTimestampOk returns a tuple with the Timestamp 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 *KnowledgeNode) GetTimestampOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Timestamp.Get(), o.Timestamp.IsSet()
|
||||
}
|
||||
|
||||
// HasTimestamp returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasTimestamp() bool {
|
||||
if o != nil && o.Timestamp.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetTimestamp gets a reference to the given NullableString and assigns it to the Timestamp field.
|
||||
func (o *KnowledgeNode) SetTimestamp(v string) {
|
||||
o.Timestamp.Set(&v)
|
||||
}
|
||||
// SetTimestampNil sets the value for Timestamp to be an explicit nil
|
||||
func (o *KnowledgeNode) SetTimestampNil() {
|
||||
o.Timestamp.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetTimestamp ensures that no value is present for Timestamp, not even an explicit nil
|
||||
func (o *KnowledgeNode) UnsetTimestamp() {
|
||||
o.Timestamp.Unset()
|
||||
}
|
||||
|
||||
// GetChildren returns the Children field value if set, zero value otherwise.
|
||||
func (o *KnowledgeNode) GetChildren() []KnowledgeNode {
|
||||
if o == nil || IsNil(o.Children) {
|
||||
var ret []KnowledgeNode
|
||||
return ret
|
||||
}
|
||||
return o.Children
|
||||
}
|
||||
|
||||
// GetChildrenOk returns a tuple with the Children field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeNode) GetChildrenOk() ([]KnowledgeNode, bool) {
|
||||
if o == nil || IsNil(o.Children) {
|
||||
return nil, false
|
||||
}
|
||||
return o.Children, true
|
||||
}
|
||||
|
||||
// HasChildren returns a boolean if a field has been set.
|
||||
func (o *KnowledgeNode) HasChildren() bool {
|
||||
if o != nil && !IsNil(o.Children) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetChildren gets a reference to the given []KnowledgeNode and assigns it to the Children field.
|
||||
func (o *KnowledgeNode) SetChildren(v []KnowledgeNode) {
|
||||
o.Children = v
|
||||
}
|
||||
|
||||
func (o KnowledgeNode) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o KnowledgeNode) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["id"] = o.Id
|
||||
toSerialize["kind"] = o.Kind
|
||||
toSerialize["name"] = o.Name
|
||||
if o.ParentId.IsSet() {
|
||||
toSerialize["parent_id"] = o.ParentId.Get()
|
||||
}
|
||||
if o.MentalModelId.IsSet() {
|
||||
toSerialize["mental_model_id"] = o.MentalModelId.Get()
|
||||
}
|
||||
if !IsNil(o.Managed) {
|
||||
toSerialize["managed"] = o.Managed
|
||||
}
|
||||
if o.Description.IsSet() {
|
||||
toSerialize["description"] = o.Description.Get()
|
||||
}
|
||||
if !IsNil(o.Tags) {
|
||||
toSerialize["tags"] = o.Tags
|
||||
}
|
||||
if o.Timestamp.IsSet() {
|
||||
toSerialize["timestamp"] = o.Timestamp.Get()
|
||||
}
|
||||
if !IsNil(o.Children) {
|
||||
toSerialize["children"] = o.Children
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *KnowledgeNode) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"id",
|
||||
"kind",
|
||||
"name",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varKnowledgeNode := _KnowledgeNode{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varKnowledgeNode)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = KnowledgeNode(varKnowledgeNode)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableKnowledgeNode struct {
|
||||
value *KnowledgeNode
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableKnowledgeNode) Get() *KnowledgeNode {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgeNode) Set(val *KnowledgeNode) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableKnowledgeNode) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgeNode) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableKnowledgeNode(val *KnowledgeNode) *NullableKnowledgeNode {
|
||||
return &NullableKnowledgeNode{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableKnowledgeNode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgeNode) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the KnowledgePageBundleFile type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &KnowledgePageBundleFile{}
|
||||
|
||||
// KnowledgePageBundleFile One file in a portable OKF bundle.
|
||||
type KnowledgePageBundleFile struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type _KnowledgePageBundleFile KnowledgePageBundleFile
|
||||
|
||||
// NewKnowledgePageBundleFile instantiates a new KnowledgePageBundleFile object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewKnowledgePageBundleFile(path string, content string) *KnowledgePageBundleFile {
|
||||
this := KnowledgePageBundleFile{}
|
||||
this.Path = path
|
||||
this.Content = content
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewKnowledgePageBundleFileWithDefaults instantiates a new KnowledgePageBundleFile object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewKnowledgePageBundleFileWithDefaults() *KnowledgePageBundleFile {
|
||||
this := KnowledgePageBundleFile{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetPath returns the Path field value
|
||||
func (o *KnowledgePageBundleFile) GetPath() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Path
|
||||
}
|
||||
|
||||
// GetPathOk returns a tuple with the Path field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageBundleFile) GetPathOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Path, true
|
||||
}
|
||||
|
||||
// SetPath sets field value
|
||||
func (o *KnowledgePageBundleFile) SetPath(v string) {
|
||||
o.Path = v
|
||||
}
|
||||
|
||||
// GetContent returns the Content field value
|
||||
func (o *KnowledgePageBundleFile) GetContent() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Content
|
||||
}
|
||||
|
||||
// GetContentOk returns a tuple with the Content field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageBundleFile) GetContentOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Content, true
|
||||
}
|
||||
|
||||
// SetContent sets field value
|
||||
func (o *KnowledgePageBundleFile) SetContent(v string) {
|
||||
o.Content = v
|
||||
}
|
||||
|
||||
func (o KnowledgePageBundleFile) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o KnowledgePageBundleFile) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["path"] = o.Path
|
||||
toSerialize["content"] = o.Content
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *KnowledgePageBundleFile) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"path",
|
||||
"content",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varKnowledgePageBundleFile := _KnowledgePageBundleFile{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varKnowledgePageBundleFile)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = KnowledgePageBundleFile(varKnowledgePageBundleFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableKnowledgePageBundleFile struct {
|
||||
value *KnowledgePageBundleFile
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageBundleFile) Get() *KnowledgePageBundleFile {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageBundleFile) Set(val *KnowledgePageBundleFile) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageBundleFile) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageBundleFile) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableKnowledgePageBundleFile(val *KnowledgePageBundleFile) *NullableKnowledgePageBundleFile {
|
||||
return &NullableKnowledgePageBundleFile{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageBundleFile) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageBundleFile) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the KnowledgePageBundleResponse type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &KnowledgePageBundleResponse{}
|
||||
|
||||
// KnowledgePageBundleResponse A portable OKF bundle — a flat set of markdown files (index + pages + logs).
|
||||
type KnowledgePageBundleResponse struct {
|
||||
Files []KnowledgePageBundleFile `json:"files"`
|
||||
}
|
||||
|
||||
type _KnowledgePageBundleResponse KnowledgePageBundleResponse
|
||||
|
||||
// NewKnowledgePageBundleResponse instantiates a new KnowledgePageBundleResponse object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewKnowledgePageBundleResponse(files []KnowledgePageBundleFile) *KnowledgePageBundleResponse {
|
||||
this := KnowledgePageBundleResponse{}
|
||||
this.Files = files
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewKnowledgePageBundleResponseWithDefaults instantiates a new KnowledgePageBundleResponse object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewKnowledgePageBundleResponseWithDefaults() *KnowledgePageBundleResponse {
|
||||
this := KnowledgePageBundleResponse{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetFiles returns the Files field value
|
||||
func (o *KnowledgePageBundleResponse) GetFiles() []KnowledgePageBundleFile {
|
||||
if o == nil {
|
||||
var ret []KnowledgePageBundleFile
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Files
|
||||
}
|
||||
|
||||
// GetFilesOk returns a tuple with the Files field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageBundleResponse) GetFilesOk() ([]KnowledgePageBundleFile, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Files, true
|
||||
}
|
||||
|
||||
// SetFiles sets field value
|
||||
func (o *KnowledgePageBundleResponse) SetFiles(v []KnowledgePageBundleFile) {
|
||||
o.Files = v
|
||||
}
|
||||
|
||||
func (o KnowledgePageBundleResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o KnowledgePageBundleResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["files"] = o.Files
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *KnowledgePageBundleResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"files",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varKnowledgePageBundleResponse := _KnowledgePageBundleResponse{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varKnowledgePageBundleResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = KnowledgePageBundleResponse(varKnowledgePageBundleResponse)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableKnowledgePageBundleResponse struct {
|
||||
value *KnowledgePageBundleResponse
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageBundleResponse) Get() *KnowledgePageBundleResponse {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageBundleResponse) Set(val *KnowledgePageBundleResponse) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageBundleResponse) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageBundleResponse) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableKnowledgePageBundleResponse(val *KnowledgePageBundleResponse) *NullableKnowledgePageBundleResponse {
|
||||
return &NullableKnowledgePageBundleResponse{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageBundleResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageBundleResponse) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the KnowledgePageGraphResponse type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &KnowledgePageGraphResponse{}
|
||||
|
||||
// KnowledgePageGraphResponse Constellation graph of knowledge pages linked by shared tags.
|
||||
type KnowledgePageGraphResponse struct {
|
||||
Nodes []map[string]interface{} `json:"nodes"`
|
||||
Edges []map[string]interface{} `json:"edges"`
|
||||
TotalPages int32 `json:"total_pages"`
|
||||
TotalEdges int32 `json:"total_edges"`
|
||||
}
|
||||
|
||||
type _KnowledgePageGraphResponse KnowledgePageGraphResponse
|
||||
|
||||
// NewKnowledgePageGraphResponse instantiates a new KnowledgePageGraphResponse object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewKnowledgePageGraphResponse(nodes []map[string]interface{}, edges []map[string]interface{}, totalPages int32, totalEdges int32) *KnowledgePageGraphResponse {
|
||||
this := KnowledgePageGraphResponse{}
|
||||
this.Nodes = nodes
|
||||
this.Edges = edges
|
||||
this.TotalPages = totalPages
|
||||
this.TotalEdges = totalEdges
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewKnowledgePageGraphResponseWithDefaults instantiates a new KnowledgePageGraphResponse object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewKnowledgePageGraphResponseWithDefaults() *KnowledgePageGraphResponse {
|
||||
this := KnowledgePageGraphResponse{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetNodes returns the Nodes field value
|
||||
func (o *KnowledgePageGraphResponse) GetNodes() []map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret []map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Nodes
|
||||
}
|
||||
|
||||
// GetNodesOk returns a tuple with the Nodes field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageGraphResponse) GetNodesOk() ([]map[string]interface{}, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Nodes, true
|
||||
}
|
||||
|
||||
// SetNodes sets field value
|
||||
func (o *KnowledgePageGraphResponse) SetNodes(v []map[string]interface{}) {
|
||||
o.Nodes = v
|
||||
}
|
||||
|
||||
// GetEdges returns the Edges field value
|
||||
func (o *KnowledgePageGraphResponse) GetEdges() []map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret []map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Edges
|
||||
}
|
||||
|
||||
// GetEdgesOk returns a tuple with the Edges field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageGraphResponse) GetEdgesOk() ([]map[string]interface{}, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Edges, true
|
||||
}
|
||||
|
||||
// SetEdges sets field value
|
||||
func (o *KnowledgePageGraphResponse) SetEdges(v []map[string]interface{}) {
|
||||
o.Edges = v
|
||||
}
|
||||
|
||||
// GetTotalPages returns the TotalPages field value
|
||||
func (o *KnowledgePageGraphResponse) GetTotalPages() int32 {
|
||||
if o == nil {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.TotalPages
|
||||
}
|
||||
|
||||
// GetTotalPagesOk returns a tuple with the TotalPages field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageGraphResponse) GetTotalPagesOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.TotalPages, true
|
||||
}
|
||||
|
||||
// SetTotalPages sets field value
|
||||
func (o *KnowledgePageGraphResponse) SetTotalPages(v int32) {
|
||||
o.TotalPages = v
|
||||
}
|
||||
|
||||
// GetTotalEdges returns the TotalEdges field value
|
||||
func (o *KnowledgePageGraphResponse) GetTotalEdges() int32 {
|
||||
if o == nil {
|
||||
var ret int32
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.TotalEdges
|
||||
}
|
||||
|
||||
// GetTotalEdgesOk returns a tuple with the TotalEdges field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageGraphResponse) GetTotalEdgesOk() (*int32, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.TotalEdges, true
|
||||
}
|
||||
|
||||
// SetTotalEdges sets field value
|
||||
func (o *KnowledgePageGraphResponse) SetTotalEdges(v int32) {
|
||||
o.TotalEdges = v
|
||||
}
|
||||
|
||||
func (o KnowledgePageGraphResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o KnowledgePageGraphResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["nodes"] = o.Nodes
|
||||
toSerialize["edges"] = o.Edges
|
||||
toSerialize["total_pages"] = o.TotalPages
|
||||
toSerialize["total_edges"] = o.TotalEdges
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *KnowledgePageGraphResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"nodes",
|
||||
"edges",
|
||||
"total_pages",
|
||||
"total_edges",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varKnowledgePageGraphResponse := _KnowledgePageGraphResponse{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varKnowledgePageGraphResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = KnowledgePageGraphResponse(varKnowledgePageGraphResponse)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableKnowledgePageGraphResponse struct {
|
||||
value *KnowledgePageGraphResponse
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageGraphResponse) Get() *KnowledgePageGraphResponse {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageGraphResponse) Set(val *KnowledgePageGraphResponse) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageGraphResponse) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageGraphResponse) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableKnowledgePageGraphResponse(val *KnowledgePageGraphResponse) *NullableKnowledgePageGraphResponse {
|
||||
return &NullableKnowledgePageGraphResponse{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageGraphResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageGraphResponse) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the KnowledgePageResponse type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &KnowledgePageResponse{}
|
||||
|
||||
// KnowledgePageResponse A knowledge page rendered as an OKF document.
|
||||
type KnowledgePageResponse struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
// OKF document type — from a `type:<x>` tag, else 'knowledge-page'.
|
||||
Type string `json:"type"`
|
||||
Description NullableString `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Timestamp NullableString `json:"timestamp,omitempty"`
|
||||
Body NullableString `json:"body,omitempty"`
|
||||
// The full OKF document: YAML frontmatter + markdown body.
|
||||
Markdown string `json:"markdown"`
|
||||
}
|
||||
|
||||
type _KnowledgePageResponse KnowledgePageResponse
|
||||
|
||||
// NewKnowledgePageResponse instantiates a new KnowledgePageResponse object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewKnowledgePageResponse(id string, name string, type_ string, markdown string) *KnowledgePageResponse {
|
||||
this := KnowledgePageResponse{}
|
||||
this.Id = id
|
||||
this.Name = name
|
||||
this.Type = type_
|
||||
this.Markdown = markdown
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewKnowledgePageResponseWithDefaults instantiates a new KnowledgePageResponse object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewKnowledgePageResponseWithDefaults() *KnowledgePageResponse {
|
||||
this := KnowledgePageResponse{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetId returns the Id field value
|
||||
func (o *KnowledgePageResponse) GetId() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Id
|
||||
}
|
||||
|
||||
// GetIdOk returns a tuple with the Id field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageResponse) GetIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Id, true
|
||||
}
|
||||
|
||||
// SetId sets field value
|
||||
func (o *KnowledgePageResponse) SetId(v string) {
|
||||
o.Id = v
|
||||
}
|
||||
|
||||
// GetName returns the Name field value
|
||||
func (o *KnowledgePageResponse) GetName() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Name
|
||||
}
|
||||
|
||||
// GetNameOk returns a tuple with the Name field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageResponse) GetNameOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Name, true
|
||||
}
|
||||
|
||||
// SetName sets field value
|
||||
func (o *KnowledgePageResponse) SetName(v string) {
|
||||
o.Name = v
|
||||
}
|
||||
|
||||
// GetType returns the Type field value
|
||||
func (o *KnowledgePageResponse) GetType() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Type
|
||||
}
|
||||
|
||||
// GetTypeOk returns a tuple with the Type field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageResponse) GetTypeOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Type, true
|
||||
}
|
||||
|
||||
// SetType sets field value
|
||||
func (o *KnowledgePageResponse) SetType(v string) {
|
||||
o.Type = v
|
||||
}
|
||||
|
||||
// GetDescription returns the Description field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgePageResponse) GetDescription() string {
|
||||
if o == nil || IsNil(o.Description.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.Description.Get()
|
||||
}
|
||||
|
||||
// GetDescriptionOk returns a tuple with the Description 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 *KnowledgePageResponse) GetDescriptionOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Description.Get(), o.Description.IsSet()
|
||||
}
|
||||
|
||||
// HasDescription returns a boolean if a field has been set.
|
||||
func (o *KnowledgePageResponse) HasDescription() bool {
|
||||
if o != nil && o.Description.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDescription gets a reference to the given NullableString and assigns it to the Description field.
|
||||
func (o *KnowledgePageResponse) SetDescription(v string) {
|
||||
o.Description.Set(&v)
|
||||
}
|
||||
// SetDescriptionNil sets the value for Description to be an explicit nil
|
||||
func (o *KnowledgePageResponse) SetDescriptionNil() {
|
||||
o.Description.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetDescription ensures that no value is present for Description, not even an explicit nil
|
||||
func (o *KnowledgePageResponse) UnsetDescription() {
|
||||
o.Description.Unset()
|
||||
}
|
||||
|
||||
// GetTags returns the Tags field value if set, zero value otherwise.
|
||||
func (o *KnowledgePageResponse) GetTags() []string {
|
||||
if o == nil || IsNil(o.Tags) {
|
||||
var ret []string
|
||||
return ret
|
||||
}
|
||||
return o.Tags
|
||||
}
|
||||
|
||||
// GetTagsOk returns a tuple with the Tags field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageResponse) GetTagsOk() ([]string, bool) {
|
||||
if o == nil || IsNil(o.Tags) {
|
||||
return nil, false
|
||||
}
|
||||
return o.Tags, true
|
||||
}
|
||||
|
||||
// HasTags returns a boolean if a field has been set.
|
||||
func (o *KnowledgePageResponse) HasTags() bool {
|
||||
if o != nil && !IsNil(o.Tags) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetTags gets a reference to the given []string and assigns it to the Tags field.
|
||||
func (o *KnowledgePageResponse) SetTags(v []string) {
|
||||
o.Tags = v
|
||||
}
|
||||
|
||||
// GetTimestamp returns the Timestamp field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgePageResponse) GetTimestamp() string {
|
||||
if o == nil || IsNil(o.Timestamp.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.Timestamp.Get()
|
||||
}
|
||||
|
||||
// GetTimestampOk returns a tuple with the Timestamp 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 *KnowledgePageResponse) GetTimestampOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Timestamp.Get(), o.Timestamp.IsSet()
|
||||
}
|
||||
|
||||
// HasTimestamp returns a boolean if a field has been set.
|
||||
func (o *KnowledgePageResponse) HasTimestamp() bool {
|
||||
if o != nil && o.Timestamp.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetTimestamp gets a reference to the given NullableString and assigns it to the Timestamp field.
|
||||
func (o *KnowledgePageResponse) SetTimestamp(v string) {
|
||||
o.Timestamp.Set(&v)
|
||||
}
|
||||
// SetTimestampNil sets the value for Timestamp to be an explicit nil
|
||||
func (o *KnowledgePageResponse) SetTimestampNil() {
|
||||
o.Timestamp.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetTimestamp ensures that no value is present for Timestamp, not even an explicit nil
|
||||
func (o *KnowledgePageResponse) UnsetTimestamp() {
|
||||
o.Timestamp.Unset()
|
||||
}
|
||||
|
||||
// GetBody returns the Body field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *KnowledgePageResponse) GetBody() string {
|
||||
if o == nil || IsNil(o.Body.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.Body.Get()
|
||||
}
|
||||
|
||||
// GetBodyOk returns a tuple with the Body 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 *KnowledgePageResponse) GetBodyOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Body.Get(), o.Body.IsSet()
|
||||
}
|
||||
|
||||
// HasBody returns a boolean if a field has been set.
|
||||
func (o *KnowledgePageResponse) HasBody() bool {
|
||||
if o != nil && o.Body.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetBody gets a reference to the given NullableString and assigns it to the Body field.
|
||||
func (o *KnowledgePageResponse) SetBody(v string) {
|
||||
o.Body.Set(&v)
|
||||
}
|
||||
// SetBodyNil sets the value for Body to be an explicit nil
|
||||
func (o *KnowledgePageResponse) SetBodyNil() {
|
||||
o.Body.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetBody ensures that no value is present for Body, not even an explicit nil
|
||||
func (o *KnowledgePageResponse) UnsetBody() {
|
||||
o.Body.Unset()
|
||||
}
|
||||
|
||||
// GetMarkdown returns the Markdown field value
|
||||
func (o *KnowledgePageResponse) GetMarkdown() string {
|
||||
if o == nil {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Markdown
|
||||
}
|
||||
|
||||
// GetMarkdownOk returns a tuple with the Markdown field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgePageResponse) GetMarkdownOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return &o.Markdown, true
|
||||
}
|
||||
|
||||
// SetMarkdown sets field value
|
||||
func (o *KnowledgePageResponse) SetMarkdown(v string) {
|
||||
o.Markdown = v
|
||||
}
|
||||
|
||||
func (o KnowledgePageResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o KnowledgePageResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["id"] = o.Id
|
||||
toSerialize["name"] = o.Name
|
||||
toSerialize["type"] = o.Type
|
||||
if o.Description.IsSet() {
|
||||
toSerialize["description"] = o.Description.Get()
|
||||
}
|
||||
if !IsNil(o.Tags) {
|
||||
toSerialize["tags"] = o.Tags
|
||||
}
|
||||
if o.Timestamp.IsSet() {
|
||||
toSerialize["timestamp"] = o.Timestamp.Get()
|
||||
}
|
||||
if o.Body.IsSet() {
|
||||
toSerialize["body"] = o.Body.Get()
|
||||
}
|
||||
toSerialize["markdown"] = o.Markdown
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *KnowledgePageResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"markdown",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varKnowledgePageResponse := _KnowledgePageResponse{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varKnowledgePageResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = KnowledgePageResponse(varKnowledgePageResponse)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableKnowledgePageResponse struct {
|
||||
value *KnowledgePageResponse
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageResponse) Get() *KnowledgePageResponse {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageResponse) Set(val *KnowledgePageResponse) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageResponse) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageResponse) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableKnowledgePageResponse(val *KnowledgePageResponse) *NullableKnowledgePageResponse {
|
||||
return &NullableKnowledgePageResponse{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableKnowledgePageResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgePageResponse) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checks if the KnowledgeTreeResponse type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &KnowledgeTreeResponse{}
|
||||
|
||||
// KnowledgeTreeResponse The knowledge base as a nested folder/page tree.
|
||||
type KnowledgeTreeResponse struct {
|
||||
Roots []KnowledgeNode `json:"roots"`
|
||||
}
|
||||
|
||||
type _KnowledgeTreeResponse KnowledgeTreeResponse
|
||||
|
||||
// NewKnowledgeTreeResponse instantiates a new KnowledgeTreeResponse object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewKnowledgeTreeResponse(roots []KnowledgeNode) *KnowledgeTreeResponse {
|
||||
this := KnowledgeTreeResponse{}
|
||||
this.Roots = roots
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewKnowledgeTreeResponseWithDefaults instantiates a new KnowledgeTreeResponse object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewKnowledgeTreeResponseWithDefaults() *KnowledgeTreeResponse {
|
||||
this := KnowledgeTreeResponse{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetRoots returns the Roots field value
|
||||
func (o *KnowledgeTreeResponse) GetRoots() []KnowledgeNode {
|
||||
if o == nil {
|
||||
var ret []KnowledgeNode
|
||||
return ret
|
||||
}
|
||||
|
||||
return o.Roots
|
||||
}
|
||||
|
||||
// GetRootsOk returns a tuple with the Roots field value
|
||||
// and a boolean to check if the value has been set.
|
||||
func (o *KnowledgeTreeResponse) GetRootsOk() ([]KnowledgeNode, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Roots, true
|
||||
}
|
||||
|
||||
// SetRoots sets field value
|
||||
func (o *KnowledgeTreeResponse) SetRoots(v []KnowledgeNode) {
|
||||
o.Roots = v
|
||||
}
|
||||
|
||||
func (o KnowledgeTreeResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o KnowledgeTreeResponse) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
toSerialize["roots"] = o.Roots
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
func (o *KnowledgeTreeResponse) UnmarshalJSON(data []byte) (err error) {
|
||||
// This validates that all required properties are included in the JSON object
|
||||
// by unmarshalling the object into a generic map with string keys and checking
|
||||
// that every required field exists as a key in the generic map.
|
||||
requiredProperties := []string{
|
||||
"roots",
|
||||
}
|
||||
|
||||
allProperties := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(data, &allProperties)
|
||||
|
||||
if err != nil {
|
||||
return err;
|
||||
}
|
||||
|
||||
for _, requiredProperty := range(requiredProperties) {
|
||||
if _, exists := allProperties[requiredProperty]; !exists {
|
||||
return fmt.Errorf("no value given for required property %v", requiredProperty)
|
||||
}
|
||||
}
|
||||
|
||||
varKnowledgeTreeResponse := _KnowledgeTreeResponse{}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err = decoder.Decode(&varKnowledgeTreeResponse)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = KnowledgeTreeResponse(varKnowledgeTreeResponse)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type NullableKnowledgeTreeResponse struct {
|
||||
value *KnowledgeTreeResponse
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableKnowledgeTreeResponse) Get() *KnowledgeTreeResponse {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgeTreeResponse) Set(val *KnowledgeTreeResponse) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableKnowledgeTreeResponse) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgeTreeResponse) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableKnowledgeTreeResponse(val *KnowledgeTreeResponse) *NullableKnowledgeTreeResponse {
|
||||
return &NullableKnowledgeTreeResponse{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableKnowledgeTreeResponse) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableKnowledgeTreeResponse) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
API version: 0.8.3
|
||||
*/
|
||||
|
||||
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
|
||||
|
||||
package hindsight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// checks if the UpdateNodeRequest type satisfies the MappedNullable interface at compile time
|
||||
var _ MappedNullable = &UpdateNodeRequest{}
|
||||
|
||||
// UpdateNodeRequest Rename and/or move a node. Each field applies only when present.
|
||||
type UpdateNodeRequest struct {
|
||||
Name NullableString `json:"name,omitempty"`
|
||||
ParentId NullableString `json:"parent_id,omitempty"`
|
||||
}
|
||||
|
||||
// NewUpdateNodeRequest instantiates a new UpdateNodeRequest object
|
||||
// This constructor will assign default values to properties that have it defined,
|
||||
// and makes sure properties required by API are set, but the set of arguments
|
||||
// will change when the set of required properties is changed
|
||||
func NewUpdateNodeRequest() *UpdateNodeRequest {
|
||||
this := UpdateNodeRequest{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// NewUpdateNodeRequestWithDefaults instantiates a new UpdateNodeRequest object
|
||||
// This constructor will only assign default values to properties that have it defined,
|
||||
// but it doesn't guarantee that properties required by API are set
|
||||
func NewUpdateNodeRequestWithDefaults() *UpdateNodeRequest {
|
||||
this := UpdateNodeRequest{}
|
||||
return &this
|
||||
}
|
||||
|
||||
// GetName returns the Name field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *UpdateNodeRequest) GetName() string {
|
||||
if o == nil || IsNil(o.Name.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.Name.Get()
|
||||
}
|
||||
|
||||
// GetNameOk returns a tuple with the Name 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 *UpdateNodeRequest) GetNameOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.Name.Get(), o.Name.IsSet()
|
||||
}
|
||||
|
||||
// HasName returns a boolean if a field has been set.
|
||||
func (o *UpdateNodeRequest) HasName() bool {
|
||||
if o != nil && o.Name.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetName gets a reference to the given NullableString and assigns it to the Name field.
|
||||
func (o *UpdateNodeRequest) SetName(v string) {
|
||||
o.Name.Set(&v)
|
||||
}
|
||||
// SetNameNil sets the value for Name to be an explicit nil
|
||||
func (o *UpdateNodeRequest) SetNameNil() {
|
||||
o.Name.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetName ensures that no value is present for Name, not even an explicit nil
|
||||
func (o *UpdateNodeRequest) UnsetName() {
|
||||
o.Name.Unset()
|
||||
}
|
||||
|
||||
// GetParentId returns the ParentId field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *UpdateNodeRequest) GetParentId() string {
|
||||
if o == nil || IsNil(o.ParentId.Get()) {
|
||||
var ret string
|
||||
return ret
|
||||
}
|
||||
return *o.ParentId.Get()
|
||||
}
|
||||
|
||||
// GetParentIdOk returns a tuple with the ParentId 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 *UpdateNodeRequest) GetParentIdOk() (*string, bool) {
|
||||
if o == nil {
|
||||
return nil, false
|
||||
}
|
||||
return o.ParentId.Get(), o.ParentId.IsSet()
|
||||
}
|
||||
|
||||
// HasParentId returns a boolean if a field has been set.
|
||||
func (o *UpdateNodeRequest) HasParentId() bool {
|
||||
if o != nil && o.ParentId.IsSet() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetParentId gets a reference to the given NullableString and assigns it to the ParentId field.
|
||||
func (o *UpdateNodeRequest) SetParentId(v string) {
|
||||
o.ParentId.Set(&v)
|
||||
}
|
||||
// SetParentIdNil sets the value for ParentId to be an explicit nil
|
||||
func (o *UpdateNodeRequest) SetParentIdNil() {
|
||||
o.ParentId.Set(nil)
|
||||
}
|
||||
|
||||
// UnsetParentId ensures that no value is present for ParentId, not even an explicit nil
|
||||
func (o *UpdateNodeRequest) UnsetParentId() {
|
||||
o.ParentId.Unset()
|
||||
}
|
||||
|
||||
func (o UpdateNodeRequest) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return json.Marshal(toSerialize)
|
||||
}
|
||||
|
||||
func (o UpdateNodeRequest) ToMap() (map[string]interface{}, error) {
|
||||
toSerialize := map[string]interface{}{}
|
||||
if o.Name.IsSet() {
|
||||
toSerialize["name"] = o.Name.Get()
|
||||
}
|
||||
if o.ParentId.IsSet() {
|
||||
toSerialize["parent_id"] = o.ParentId.Get()
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
type NullableUpdateNodeRequest struct {
|
||||
value *UpdateNodeRequest
|
||||
isSet bool
|
||||
}
|
||||
|
||||
func (v NullableUpdateNodeRequest) Get() *UpdateNodeRequest {
|
||||
return v.value
|
||||
}
|
||||
|
||||
func (v *NullableUpdateNodeRequest) Set(val *UpdateNodeRequest) {
|
||||
v.value = val
|
||||
v.isSet = true
|
||||
}
|
||||
|
||||
func (v NullableUpdateNodeRequest) IsSet() bool {
|
||||
return v.isSet
|
||||
}
|
||||
|
||||
func (v *NullableUpdateNodeRequest) Unset() {
|
||||
v.value = nil
|
||||
v.isSet = false
|
||||
}
|
||||
|
||||
func NewNullableUpdateNodeRequest(val *UpdateNodeRequest) *NullableUpdateNodeRequest {
|
||||
return &NullableUpdateNodeRequest{value: val, isSet: true}
|
||||
}
|
||||
|
||||
func (v NullableUpdateNodeRequest) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.value)
|
||||
}
|
||||
|
||||
func (v *NullableUpdateNodeRequest) UnmarshalJSON(src []byte) error {
|
||||
v.isSet = true
|
||||
return json.Unmarshal(src, &v.value)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ hindsight_client_api/api/document_transfer_api.py
|
||||
hindsight_client_api/api/documents_api.py
|
||||
hindsight_client_api/api/entities_api.py
|
||||
hindsight_client_api/api/files_api.py
|
||||
hindsight_client_api/api/knowledge_base_api.py
|
||||
hindsight_client_api/api/llm_traces_api.py
|
||||
hindsight_client_api/api/memory_api.py
|
||||
hindsight_client_api/api/mental_models_api.py
|
||||
@@ -50,8 +51,11 @@ hindsight_client_api/models/consolidation_request.py
|
||||
hindsight_client_api/models/consolidation_response.py
|
||||
hindsight_client_api/models/create_bank_request.py
|
||||
hindsight_client_api/models/create_directive_request.py
|
||||
hindsight_client_api/models/create_folder_request.py
|
||||
hindsight_client_api/models/create_knowledge_page_response.py
|
||||
hindsight_client_api/models/create_mental_model_request.py
|
||||
hindsight_client_api/models/create_mental_model_response.py
|
||||
hindsight_client_api/models/create_page_request.py
|
||||
hindsight_client_api/models/create_webhook_request.py
|
||||
hindsight_client_api/models/delete_document_response.py
|
||||
hindsight_client_api/models/delete_response.py
|
||||
@@ -76,6 +80,12 @@ hindsight_client_api/models/file_retain_response.py
|
||||
hindsight_client_api/models/graph_data_response.py
|
||||
hindsight_client_api/models/http_validation_error.py
|
||||
hindsight_client_api/models/include_options.py
|
||||
hindsight_client_api/models/knowledge_node.py
|
||||
hindsight_client_api/models/knowledge_page_bundle_file.py
|
||||
hindsight_client_api/models/knowledge_page_bundle_response.py
|
||||
hindsight_client_api/models/knowledge_page_graph_response.py
|
||||
hindsight_client_api/models/knowledge_page_response.py
|
||||
hindsight_client_api/models/knowledge_tree_response.py
|
||||
hindsight_client_api/models/list_chunks_response.py
|
||||
hindsight_client_api/models/list_documents_response.py
|
||||
hindsight_client_api/models/list_memory_units_response.py
|
||||
@@ -142,6 +152,7 @@ hindsight_client_api/models/update_document_request.py
|
||||
hindsight_client_api/models/update_document_response.py
|
||||
hindsight_client_api/models/update_memory_request.py
|
||||
hindsight_client_api/models/update_mental_model_request.py
|
||||
hindsight_client_api/models/update_node_request.py
|
||||
hindsight_client_api/models/update_webhook_request.py
|
||||
hindsight_client_api/models/validation_error.py
|
||||
hindsight_client_api/models/validation_error_loc_inner.py
|
||||
|
||||
@@ -25,6 +25,7 @@ from hindsight_client_api.api.document_transfer_api import DocumentTransferApi
|
||||
from hindsight_client_api.api.documents_api import DocumentsApi
|
||||
from hindsight_client_api.api.entities_api import EntitiesApi
|
||||
from hindsight_client_api.api.files_api import FilesApi
|
||||
from hindsight_client_api.api.knowledge_base_api import KnowledgeBaseApi
|
||||
from hindsight_client_api.api.llm_traces_api import LLMTracesApi
|
||||
from hindsight_client_api.api.memory_api import MemoryApi
|
||||
from hindsight_client_api.api.mental_models_api import MentalModelsApi
|
||||
@@ -74,8 +75,11 @@ from hindsight_client_api.models.consolidation_request import ConsolidationReque
|
||||
from hindsight_client_api.models.consolidation_response import ConsolidationResponse
|
||||
from hindsight_client_api.models.create_bank_request import CreateBankRequest
|
||||
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
|
||||
from hindsight_client_api.models.create_folder_request import CreateFolderRequest
|
||||
from hindsight_client_api.models.create_knowledge_page_response import CreateKnowledgePageResponse
|
||||
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
|
||||
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
|
||||
from hindsight_client_api.models.create_page_request import CreatePageRequest
|
||||
from hindsight_client_api.models.create_webhook_request import CreateWebhookRequest
|
||||
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
|
||||
from hindsight_client_api.models.delete_response import DeleteResponse
|
||||
@@ -100,6 +104,12 @@ from hindsight_client_api.models.file_retain_response import FileRetainResponse
|
||||
from hindsight_client_api.models.graph_data_response import GraphDataResponse
|
||||
from hindsight_client_api.models.http_validation_error import HTTPValidationError
|
||||
from hindsight_client_api.models.include_options import IncludeOptions
|
||||
from hindsight_client_api.models.knowledge_node import KnowledgeNode
|
||||
from hindsight_client_api.models.knowledge_page_bundle_file import KnowledgePageBundleFile
|
||||
from hindsight_client_api.models.knowledge_page_bundle_response import KnowledgePageBundleResponse
|
||||
from hindsight_client_api.models.knowledge_page_graph_response import KnowledgePageGraphResponse
|
||||
from hindsight_client_api.models.knowledge_page_response import KnowledgePageResponse
|
||||
from hindsight_client_api.models.knowledge_tree_response import KnowledgeTreeResponse
|
||||
from hindsight_client_api.models.llm_request_entry import LLMRequestEntry
|
||||
from hindsight_client_api.models.llm_request_list_response import LLMRequestListResponse
|
||||
from hindsight_client_api.models.llm_request_stats_bucket import LLMRequestStatsBucket
|
||||
@@ -166,6 +176,7 @@ from hindsight_client_api.models.update_document_request import UpdateDocumentRe
|
||||
from hindsight_client_api.models.update_document_response import UpdateDocumentResponse
|
||||
from hindsight_client_api.models.update_memory_request import UpdateMemoryRequest
|
||||
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
|
||||
from hindsight_client_api.models.update_node_request import UpdateNodeRequest
|
||||
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
|
||||
from hindsight_client_api.models.validation_error import ValidationError
|
||||
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
|
||||
|
||||
@@ -9,6 +9,7 @@ from hindsight_client_api.api.document_transfer_api import DocumentTransferApi
|
||||
from hindsight_client_api.api.documents_api import DocumentsApi
|
||||
from hindsight_client_api.api.entities_api import EntitiesApi
|
||||
from hindsight_client_api.api.files_api import FilesApi
|
||||
from hindsight_client_api.api.knowledge_base_api import KnowledgeBaseApi
|
||||
from hindsight_client_api.api.llm_traces_api import LLMTracesApi
|
||||
from hindsight_client_api.api.memory_api import MemoryApi
|
||||
from hindsight_client_api.api.mental_models_api import MentalModelsApi
|
||||
|
||||
@@ -44,8 +44,11 @@ from hindsight_client_api.models.consolidation_request import ConsolidationReque
|
||||
from hindsight_client_api.models.consolidation_response import ConsolidationResponse
|
||||
from hindsight_client_api.models.create_bank_request import CreateBankRequest
|
||||
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
|
||||
from hindsight_client_api.models.create_folder_request import CreateFolderRequest
|
||||
from hindsight_client_api.models.create_knowledge_page_response import CreateKnowledgePageResponse
|
||||
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
|
||||
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
|
||||
from hindsight_client_api.models.create_page_request import CreatePageRequest
|
||||
from hindsight_client_api.models.create_webhook_request import CreateWebhookRequest
|
||||
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
|
||||
from hindsight_client_api.models.delete_response import DeleteResponse
|
||||
@@ -70,6 +73,12 @@ from hindsight_client_api.models.file_retain_response import FileRetainResponse
|
||||
from hindsight_client_api.models.graph_data_response import GraphDataResponse
|
||||
from hindsight_client_api.models.http_validation_error import HTTPValidationError
|
||||
from hindsight_client_api.models.include_options import IncludeOptions
|
||||
from hindsight_client_api.models.knowledge_node import KnowledgeNode
|
||||
from hindsight_client_api.models.knowledge_page_bundle_file import KnowledgePageBundleFile
|
||||
from hindsight_client_api.models.knowledge_page_bundle_response import KnowledgePageBundleResponse
|
||||
from hindsight_client_api.models.knowledge_page_graph_response import KnowledgePageGraphResponse
|
||||
from hindsight_client_api.models.knowledge_page_response import KnowledgePageResponse
|
||||
from hindsight_client_api.models.knowledge_tree_response import KnowledgeTreeResponse
|
||||
from hindsight_client_api.models.llm_request_entry import LLMRequestEntry
|
||||
from hindsight_client_api.models.llm_request_list_response import LLMRequestListResponse
|
||||
from hindsight_client_api.models.llm_request_stats_bucket import LLMRequestStatsBucket
|
||||
@@ -136,6 +145,7 @@ from hindsight_client_api.models.update_document_request import UpdateDocumentRe
|
||||
from hindsight_client_api.models.update_document_response import UpdateDocumentResponse
|
||||
from hindsight_client_api.models.update_memory_request import UpdateMemoryRequest
|
||||
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
|
||||
from hindsight_client_api.models.update_node_request import UpdateNodeRequest
|
||||
from hindsight_client_api.models.update_webhook_request import UpdateWebhookRequest
|
||||
from hindsight_client_api.models.validation_error import ValidationError
|
||||
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class CreateFolderRequest(BaseModel):
|
||||
"""
|
||||
Create a folder under an optional parent folder.
|
||||
""" # noqa: E501
|
||||
name: StrictStr
|
||||
parent_id: Optional[StrictStr] = None
|
||||
__properties: ClassVar[List[str]] = ["name", "parent_id"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of CreateFolderRequest from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# set to None if parent_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.parent_id is None and "parent_id" in self.model_fields_set:
|
||||
_dict['parent_id'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of CreateFolderRequest from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"name": obj.get("name"),
|
||||
"parent_id": obj.get("parent_id")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class CreateKnowledgePageResponse(BaseModel):
|
||||
"""
|
||||
Result of creating a page: the node id, its mental model, and the refresh op.
|
||||
""" # noqa: E501
|
||||
page_id: StrictStr
|
||||
mental_model_id: StrictStr
|
||||
operation_id: Optional[StrictStr] = None
|
||||
__properties: ClassVar[List[str]] = ["page_id", "mental_model_id", "operation_id"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of CreateKnowledgePageResponse from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# set to None if operation_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.operation_id is None and "operation_id" in self.model_fields_set:
|
||||
_dict['operation_id'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of CreateKnowledgePageResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"page_id": obj.get("page_id"),
|
||||
"mental_model_id": obj.get("mental_model_id"),
|
||||
"operation_id": obj.get("operation_id")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.mental_model_trigger_input import MentalModelTriggerInput
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class CreatePageRequest(BaseModel):
|
||||
"""
|
||||
Create a page (a mental model + tree node) under an optional parent folder.
|
||||
""" # noqa: E501
|
||||
name: StrictStr
|
||||
source_query: StrictStr
|
||||
parent_id: Optional[StrictStr] = None
|
||||
tags: Optional[List[StrictStr]] = None
|
||||
max_tokens: Optional[StrictInt] = None
|
||||
trigger: Optional[MentalModelTriggerInput] = None
|
||||
__properties: ClassVar[List[str]] = ["name", "source_query", "parent_id", "tags", "max_tokens", "trigger"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of CreatePageRequest from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of trigger
|
||||
if self.trigger:
|
||||
_dict['trigger'] = self.trigger.to_dict()
|
||||
# set to None if parent_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.parent_id is None and "parent_id" in self.model_fields_set:
|
||||
_dict['parent_id'] = None
|
||||
|
||||
# set to None if tags (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.tags is None and "tags" in self.model_fields_set:
|
||||
_dict['tags'] = None
|
||||
|
||||
# set to None if max_tokens (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.max_tokens is None and "max_tokens" in self.model_fields_set:
|
||||
_dict['max_tokens'] = None
|
||||
|
||||
# set to None if trigger (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.trigger is None and "trigger" in self.model_fields_set:
|
||||
_dict['trigger'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of CreatePageRequest from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"name": obj.get("name"),
|
||||
"source_query": obj.get("source_query"),
|
||||
"parent_id": obj.get("parent_id"),
|
||||
"tags": obj.get("tags"),
|
||||
"max_tokens": obj.get("max_tokens"),
|
||||
"trigger": MentalModelTriggerInput.from_dict(obj["trigger"]) if obj.get("trigger") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
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
|
||||
|
||||
class KnowledgeNode(BaseModel):
|
||||
"""
|
||||
A node in the knowledge-base tree — a folder or a page. Pages carry ``description``/``tags`` from their backing mental model. The knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node as system-owned vs. hand-authored.
|
||||
""" # noqa: E501
|
||||
id: StrictStr
|
||||
kind: StrictStr
|
||||
name: StrictStr
|
||||
parent_id: Optional[StrictStr] = None
|
||||
mental_model_id: Optional[StrictStr] = None
|
||||
managed: Optional[StrictBool] = Field(default=False, description="Client-set flag: true = system-owned, false = hand-authored.")
|
||||
description: Optional[StrictStr] = None
|
||||
tags: Optional[List[StrictStr]] = None
|
||||
timestamp: Optional[StrictStr] = None
|
||||
children: Optional[List[KnowledgeNode]] = None
|
||||
__properties: ClassVar[List[str]] = ["id", "kind", "name", "parent_id", "mental_model_id", "managed", "description", "tags", "timestamp", "children"]
|
||||
|
||||
@field_validator('kind')
|
||||
def kind_validate_enum(cls, value):
|
||||
"""Validates the enum"""
|
||||
if value not in set(['folder', 'page']):
|
||||
raise ValueError("must be one of enum values ('folder', 'page')")
|
||||
return value
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgeNode from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in children (list)
|
||||
_items = []
|
||||
if self.children:
|
||||
for _item_children in self.children:
|
||||
if _item_children:
|
||||
_items.append(_item_children.to_dict())
|
||||
_dict['children'] = _items
|
||||
# set to None if parent_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.parent_id is None and "parent_id" in self.model_fields_set:
|
||||
_dict['parent_id'] = None
|
||||
|
||||
# set to None if mental_model_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.mental_model_id is None and "mental_model_id" in self.model_fields_set:
|
||||
_dict['mental_model_id'] = None
|
||||
|
||||
# set to None if description (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.description is None and "description" in self.model_fields_set:
|
||||
_dict['description'] = None
|
||||
|
||||
# set to None if timestamp (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.timestamp is None and "timestamp" in self.model_fields_set:
|
||||
_dict['timestamp'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgeNode from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"id": obj.get("id"),
|
||||
"kind": obj.get("kind"),
|
||||
"name": obj.get("name"),
|
||||
"parent_id": obj.get("parent_id"),
|
||||
"mental_model_id": obj.get("mental_model_id"),
|
||||
"managed": obj.get("managed") if obj.get("managed") is not None else False,
|
||||
"description": obj.get("description"),
|
||||
"tags": obj.get("tags"),
|
||||
"timestamp": obj.get("timestamp"),
|
||||
"children": [KnowledgeNode.from_dict(_item) for _item in obj["children"]] if obj.get("children") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
# TODO: Rewrite to not use raise_errors
|
||||
KnowledgeNode.model_rebuild(raise_errors=False)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class KnowledgePageBundleFile(BaseModel):
|
||||
"""
|
||||
One file in a portable OKF bundle.
|
||||
""" # noqa: E501
|
||||
path: StrictStr
|
||||
content: StrictStr
|
||||
__properties: ClassVar[List[str]] = ["path", "content"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageBundleFile from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageBundleFile from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"path": obj.get("path"),
|
||||
"content": obj.get("content")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Any, ClassVar, Dict, List
|
||||
from hindsight_client_api.models.knowledge_page_bundle_file import KnowledgePageBundleFile
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class KnowledgePageBundleResponse(BaseModel):
|
||||
"""
|
||||
A portable OKF bundle — a flat set of markdown files (index + pages + logs).
|
||||
""" # noqa: E501
|
||||
files: List[KnowledgePageBundleFile]
|
||||
__properties: ClassVar[List[str]] = ["files"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageBundleResponse from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in files (list)
|
||||
_items = []
|
||||
if self.files:
|
||||
for _item_files in self.files:
|
||||
if _item_files:
|
||||
_items.append(_item_files.to_dict())
|
||||
_dict['files'] = _items
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageBundleResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"files": [KnowledgePageBundleFile.from_dict(_item) for _item in obj["files"]] if obj.get("files") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictInt
|
||||
from typing import Any, ClassVar, Dict, List
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class KnowledgePageGraphResponse(BaseModel):
|
||||
"""
|
||||
Constellation graph of knowledge pages linked by shared tags.
|
||||
""" # noqa: E501
|
||||
nodes: List[Dict[str, Any]]
|
||||
edges: List[Dict[str, Any]]
|
||||
total_pages: StrictInt
|
||||
total_edges: StrictInt
|
||||
__properties: ClassVar[List[str]] = ["nodes", "edges", "total_pages", "total_edges"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageGraphResponse from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageGraphResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"nodes": obj.get("nodes"),
|
||||
"edges": obj.get("edges"),
|
||||
"total_pages": obj.get("total_pages"),
|
||||
"total_edges": obj.get("total_edges")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class KnowledgePageResponse(BaseModel):
|
||||
"""
|
||||
A knowledge page rendered as an OKF document.
|
||||
""" # noqa: E501
|
||||
id: StrictStr
|
||||
name: StrictStr
|
||||
type: StrictStr = Field(description="OKF document type — from a `type:<x>` tag, else 'knowledge-page'.")
|
||||
description: Optional[StrictStr] = None
|
||||
tags: Optional[List[StrictStr]] = None
|
||||
timestamp: Optional[StrictStr] = None
|
||||
body: Optional[StrictStr] = None
|
||||
markdown: StrictStr = Field(description="The full OKF document: YAML frontmatter + markdown body.")
|
||||
__properties: ClassVar[List[str]] = ["id", "name", "type", "description", "tags", "timestamp", "body", "markdown"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageResponse from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# set to None if description (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.description is None and "description" in self.model_fields_set:
|
||||
_dict['description'] = None
|
||||
|
||||
# set to None if timestamp (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.timestamp is None and "timestamp" in self.model_fields_set:
|
||||
_dict['timestamp'] = None
|
||||
|
||||
# set to None if body (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.body is None and "body" in self.model_fields_set:
|
||||
_dict['body'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgePageResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"id": obj.get("id"),
|
||||
"name": obj.get("name"),
|
||||
"type": obj.get("type"),
|
||||
"description": obj.get("description"),
|
||||
"tags": obj.get("tags"),
|
||||
"timestamp": obj.get("timestamp"),
|
||||
"body": obj.get("body"),
|
||||
"markdown": obj.get("markdown")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Any, ClassVar, Dict, List
|
||||
from hindsight_client_api.models.knowledge_node import KnowledgeNode
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class KnowledgeTreeResponse(BaseModel):
|
||||
"""
|
||||
The knowledge base as a nested folder/page tree.
|
||||
""" # noqa: E501
|
||||
roots: List[KnowledgeNode]
|
||||
__properties: ClassVar[List[str]] = ["roots"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgeTreeResponse from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in roots (list)
|
||||
_items = []
|
||||
if self.roots:
|
||||
for _item_roots in self.roots:
|
||||
if _item_roots:
|
||||
_items.append(_item_roots.to_dict())
|
||||
_dict['roots'] = _items
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of KnowledgeTreeResponse from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"roots": [KnowledgeNode.from_dict(_item) for _item in obj["roots"]] if obj.get("roots") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 0.8.3
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class UpdateNodeRequest(BaseModel):
|
||||
"""
|
||||
Rename and/or move a node. Each field applies only when present.
|
||||
""" # noqa: E501
|
||||
name: Optional[StrictStr] = None
|
||||
parent_id: Optional[StrictStr] = None
|
||||
__properties: ClassVar[List[str]] = ["name", "parent_id"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of UpdateNodeRequest from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# set to None if name (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.name is None and "name" in self.model_fields_set:
|
||||
_dict['name'] = None
|
||||
|
||||
# set to None if parent_id (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.parent_id is None and "parent_id" in self.model_fields_set:
|
||||
_dict['parent_id'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of UpdateNodeRequest from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"name": obj.get("name"),
|
||||
"parent_id": obj.get("parent_id")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@ import type {
|
||||
CreateDirectiveData,
|
||||
CreateDirectiveErrors,
|
||||
CreateDirectiveResponses,
|
||||
CreateKnowledgeFolderData,
|
||||
CreateKnowledgeFolderErrors,
|
||||
CreateKnowledgeFolderResponses,
|
||||
CreateKnowledgePageData,
|
||||
CreateKnowledgePageErrors,
|
||||
CreateKnowledgePageResponses,
|
||||
CreateMentalModelData,
|
||||
CreateMentalModelErrors,
|
||||
CreateMentalModelResponses,
|
||||
@@ -50,6 +56,9 @@ import type {
|
||||
DeleteDocumentData,
|
||||
DeleteDocumentErrors,
|
||||
DeleteDocumentResponses,
|
||||
DeleteKnowledgeNodeData,
|
||||
DeleteKnowledgeNodeErrors,
|
||||
DeleteKnowledgeNodeResponses,
|
||||
DeleteMentalModelData,
|
||||
DeleteMentalModelErrors,
|
||||
DeleteMentalModelResponses,
|
||||
@@ -65,6 +74,9 @@ import type {
|
||||
ExportDocumentsData,
|
||||
ExportDocumentsErrors,
|
||||
ExportDocumentsResponses,
|
||||
ExportKnowledgeBaseData,
|
||||
ExportKnowledgeBaseErrors,
|
||||
ExportKnowledgeBaseResponses,
|
||||
FileRetainData,
|
||||
FileRetainErrors,
|
||||
FileRetainResponses,
|
||||
@@ -97,6 +109,15 @@ import type {
|
||||
GetGraphData,
|
||||
GetGraphErrors,
|
||||
GetGraphResponses,
|
||||
GetKnowledgeBaseGraphData,
|
||||
GetKnowledgeBaseGraphErrors,
|
||||
GetKnowledgeBaseGraphResponses,
|
||||
GetKnowledgeBaseTreeData,
|
||||
GetKnowledgeBaseTreeErrors,
|
||||
GetKnowledgeBaseTreeResponses,
|
||||
GetKnowledgePageData,
|
||||
GetKnowledgePageErrors,
|
||||
GetKnowledgePageResponses,
|
||||
GetMemoriesTimeseriesData,
|
||||
GetMemoriesTimeseriesErrors,
|
||||
GetMemoriesTimeseriesResponses,
|
||||
@@ -220,6 +241,9 @@ import type {
|
||||
UpdateDocumentData,
|
||||
UpdateDocumentErrors,
|
||||
UpdateDocumentResponses,
|
||||
UpdateKnowledgeNodeData,
|
||||
UpdateKnowledgeNodeErrors,
|
||||
UpdateKnowledgeNodeResponses,
|
||||
UpdateMemoryData,
|
||||
UpdateMemoryErrors,
|
||||
UpdateMemoryResponses,
|
||||
@@ -654,6 +678,138 @@ export const clearMentalModel = <ThrowOnError extends boolean = false>(
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the knowledge-base tree
|
||||
*
|
||||
* Return the knowledge base as a nested tree of folders and pages.
|
||||
*/
|
||||
export const getKnowledgeBaseTree = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetKnowledgeBaseTreeData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).get<
|
||||
GetKnowledgeBaseTreeResponses,
|
||||
GetKnowledgeBaseTreeErrors,
|
||||
ThrowOnError
|
||||
>({ url: "/v1/default/banks/{bank_id}/knowledge-base/tree", ...options });
|
||||
|
||||
/**
|
||||
* Create a knowledge-base folder
|
||||
*
|
||||
* Create a folder, optionally nested under a parent folder.
|
||||
*/
|
||||
export const createKnowledgeFolder = <ThrowOnError extends boolean = false>(
|
||||
options: Options<CreateKnowledgeFolderData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).post<
|
||||
CreateKnowledgeFolderResponses,
|
||||
CreateKnowledgeFolderErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/folders",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a knowledge-base page
|
||||
*
|
||||
* Create a page (a mental model + tree node). Content is generated asynchronously; use the returned operation_id to track completion.
|
||||
*/
|
||||
export const createKnowledgePage = <ThrowOnError extends boolean = false>(
|
||||
options: Options<CreateKnowledgePageData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).post<
|
||||
CreateKnowledgePageResponses,
|
||||
CreateKnowledgePageErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/pages",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Knowledge-base constellation graph
|
||||
*
|
||||
* Return pages as nodes linked by shared tags, for the constellation view.
|
||||
*/
|
||||
export const getKnowledgeBaseGraph = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetKnowledgeBaseGraphData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).get<
|
||||
GetKnowledgeBaseGraphResponses,
|
||||
GetKnowledgeBaseGraphErrors,
|
||||
ThrowOnError
|
||||
>({ url: "/v1/default/banks/{bank_id}/knowledge-base/graph", ...options });
|
||||
|
||||
/**
|
||||
* Export the knowledge base as an OKF bundle
|
||||
*
|
||||
* Return a portable OKF bundle: a nested index.md, one <id>.md per page, and history logs.
|
||||
*/
|
||||
export const exportKnowledgeBase = <ThrowOnError extends boolean = false>(
|
||||
options: Options<ExportKnowledgeBaseData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).get<
|
||||
ExportKnowledgeBaseResponses,
|
||||
ExportKnowledgeBaseErrors,
|
||||
ThrowOnError
|
||||
>({ url: "/v1/default/banks/{bank_id}/knowledge-base/export", ...options });
|
||||
|
||||
/**
|
||||
* Get a knowledge-base page
|
||||
*
|
||||
* Return a single page as an OKF document (frontmatter + markdown body).
|
||||
*/
|
||||
export const getKnowledgePage = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetKnowledgePageData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).get<GetKnowledgePageResponses, GetKnowledgePageErrors, ThrowOnError>({
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}",
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a knowledge-base node
|
||||
*
|
||||
* Delete a folder or page and its whole subtree (pages' mental models are removed too).
|
||||
*/
|
||||
export const deleteKnowledgeNode = <ThrowOnError extends boolean = false>(
|
||||
options: Options<DeleteKnowledgeNodeData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).delete<
|
||||
DeleteKnowledgeNodeResponses,
|
||||
DeleteKnowledgeNodeErrors,
|
||||
ThrowOnError
|
||||
>({ url: "/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}", ...options });
|
||||
|
||||
/**
|
||||
* Rename or move a knowledge-base node
|
||||
*
|
||||
* Rename a node (set `name`) and/or move it under another folder (set `parent_id`, null for the root).
|
||||
*/
|
||||
export const updateKnowledgeNode = <ThrowOnError extends boolean = false>(
|
||||
options: Options<UpdateKnowledgeNodeData, ThrowOnError>
|
||||
) =>
|
||||
(options.client ?? client).patch<
|
||||
UpdateKnowledgeNodeResponses,
|
||||
UpdateKnowledgeNodeErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}",
|
||||
...options,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* List directives
|
||||
*
|
||||
|
||||
@@ -1153,6 +1153,42 @@ export type CreateDirectiveRequest = {
|
||||
tags?: Array<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateFolderRequest
|
||||
*
|
||||
* Create a folder under an optional parent folder.
|
||||
*/
|
||||
export type CreateFolderRequest = {
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Parent Id
|
||||
*/
|
||||
parent_id?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateKnowledgePageResponse
|
||||
*
|
||||
* Result of creating a page: the node id, its mental model, and the refresh op.
|
||||
*/
|
||||
export type CreateKnowledgePageResponse = {
|
||||
/**
|
||||
* Page Id
|
||||
*/
|
||||
page_id: string;
|
||||
/**
|
||||
* Mental Model Id
|
||||
*/
|
||||
mental_model_id: string;
|
||||
/**
|
||||
* Operation Id
|
||||
*/
|
||||
operation_id?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateMentalModelRequest
|
||||
*
|
||||
@@ -1215,6 +1251,35 @@ export type CreateMentalModelResponse = {
|
||||
operation_id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreatePageRequest
|
||||
*
|
||||
* Create a page (a mental model + tree node) under an optional parent folder.
|
||||
*/
|
||||
export type CreatePageRequest = {
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Source Query
|
||||
*/
|
||||
source_query: string;
|
||||
/**
|
||||
* Parent Id
|
||||
*/
|
||||
parent_id?: string | null;
|
||||
/**
|
||||
* Tags
|
||||
*/
|
||||
tags?: Array<string> | null;
|
||||
/**
|
||||
* Max Tokens
|
||||
*/
|
||||
max_tokens?: number | null;
|
||||
trigger?: MentalModelTriggerInput | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* CreateWebhookRequest
|
||||
*
|
||||
@@ -1956,6 +2021,184 @@ export type IncludeOptions = {
|
||||
source_facts?: SourceFactsIncludeOptions | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* KnowledgeNode
|
||||
*
|
||||
* A node in the knowledge-base tree — a folder or a page.
|
||||
*
|
||||
* Pages carry ``description``/``tags`` from their backing mental model. The
|
||||
* knowledge base is client-managed (CRUD); ``managed`` lets a client tag a node
|
||||
* as system-owned vs. hand-authored.
|
||||
*/
|
||||
export type KnowledgeNode = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Kind
|
||||
*/
|
||||
kind: "folder" | "page";
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Parent Id
|
||||
*/
|
||||
parent_id?: string | null;
|
||||
/**
|
||||
* Mental Model Id
|
||||
*
|
||||
* Backing mental model id (pages only).
|
||||
*/
|
||||
mental_model_id?: string | null;
|
||||
/**
|
||||
* Managed
|
||||
*
|
||||
* Client-set flag: true = system-owned, false = hand-authored.
|
||||
*/
|
||||
managed?: boolean;
|
||||
/**
|
||||
* Description
|
||||
*
|
||||
* Page source query (OKF `description`).
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Tags
|
||||
*/
|
||||
tags?: Array<string>;
|
||||
/**
|
||||
* Timestamp
|
||||
*
|
||||
* Last refresh (page) or last update (folder).
|
||||
*/
|
||||
timestamp?: string | null;
|
||||
/**
|
||||
* Children
|
||||
*/
|
||||
children?: Array<KnowledgeNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* KnowledgePageBundleFile
|
||||
*
|
||||
* One file in a portable OKF bundle.
|
||||
*/
|
||||
export type KnowledgePageBundleFile = {
|
||||
/**
|
||||
* Path
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* Content
|
||||
*/
|
||||
content: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* KnowledgePageBundleResponse
|
||||
*
|
||||
* A portable OKF bundle — a flat set of markdown files (index + pages + logs).
|
||||
*/
|
||||
export type KnowledgePageBundleResponse = {
|
||||
/**
|
||||
* Files
|
||||
*/
|
||||
files: Array<KnowledgePageBundleFile>;
|
||||
};
|
||||
|
||||
/**
|
||||
* KnowledgePageGraphResponse
|
||||
*
|
||||
* Constellation graph of knowledge pages linked by shared tags.
|
||||
*/
|
||||
export type KnowledgePageGraphResponse = {
|
||||
/**
|
||||
* Nodes
|
||||
*/
|
||||
nodes: Array<{
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
/**
|
||||
* Edges
|
||||
*/
|
||||
edges: Array<{
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
/**
|
||||
* Total Pages
|
||||
*/
|
||||
total_pages: number;
|
||||
/**
|
||||
* Total Edges
|
||||
*/
|
||||
total_edges: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* KnowledgePageResponse
|
||||
*
|
||||
* A knowledge page rendered as an OKF document.
|
||||
*/
|
||||
export type KnowledgePageResponse = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Type
|
||||
*
|
||||
* OKF document type — from a `type:<x>` tag, else 'knowledge-page'.
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* Description
|
||||
*
|
||||
* The source query that rebuilds the page.
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Tags
|
||||
*/
|
||||
tags?: Array<string>;
|
||||
/**
|
||||
* Timestamp
|
||||
*
|
||||
* Last refresh time (falls back to creation).
|
||||
*/
|
||||
timestamp?: string | null;
|
||||
/**
|
||||
* Body
|
||||
*
|
||||
* The page's synthesized markdown body.
|
||||
*/
|
||||
body?: string | null;
|
||||
/**
|
||||
* Markdown
|
||||
*
|
||||
* The full OKF document: YAML frontmatter + markdown body.
|
||||
*/
|
||||
markdown: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* KnowledgeTreeResponse
|
||||
*
|
||||
* The knowledge base as a nested folder/page tree.
|
||||
*/
|
||||
export type KnowledgeTreeResponse = {
|
||||
/**
|
||||
* Roots
|
||||
*/
|
||||
roots: Array<KnowledgeNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* LLMRequestEntry
|
||||
*
|
||||
@@ -3938,6 +4181,22 @@ export type UpdateMentalModelRequest = {
|
||||
trigger?: MentalModelTriggerInput | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateNodeRequest
|
||||
*
|
||||
* Rename and/or move a node. Each field applies only when present.
|
||||
*/
|
||||
export type UpdateNodeRequest = {
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
name?: string | null;
|
||||
/**
|
||||
* Parent Id
|
||||
*/
|
||||
parent_id?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateWebhookRequest
|
||||
*
|
||||
@@ -5271,6 +5530,313 @@ export type ClearMentalModelResponses = {
|
||||
|
||||
export type ClearMentalModelResponse = ClearMentalModelResponses[keyof ClearMentalModelResponses];
|
||||
|
||||
export type GetKnowledgeBaseTreeData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/tree";
|
||||
};
|
||||
|
||||
export type GetKnowledgeBaseTreeErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetKnowledgeBaseTreeError =
|
||||
GetKnowledgeBaseTreeErrors[keyof GetKnowledgeBaseTreeErrors];
|
||||
|
||||
export type GetKnowledgeBaseTreeResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: KnowledgeTreeResponse;
|
||||
};
|
||||
|
||||
export type GetKnowledgeBaseTreeResponse =
|
||||
GetKnowledgeBaseTreeResponses[keyof GetKnowledgeBaseTreeResponses];
|
||||
|
||||
export type CreateKnowledgeFolderData = {
|
||||
body: CreateFolderRequest;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/folders";
|
||||
};
|
||||
|
||||
export type CreateKnowledgeFolderErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CreateKnowledgeFolderError =
|
||||
CreateKnowledgeFolderErrors[keyof CreateKnowledgeFolderErrors];
|
||||
|
||||
export type CreateKnowledgeFolderResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
201: KnowledgeNode;
|
||||
};
|
||||
|
||||
export type CreateKnowledgeFolderResponse =
|
||||
CreateKnowledgeFolderResponses[keyof CreateKnowledgeFolderResponses];
|
||||
|
||||
export type CreateKnowledgePageData = {
|
||||
body: CreatePageRequest;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/pages";
|
||||
};
|
||||
|
||||
export type CreateKnowledgePageErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CreateKnowledgePageError = CreateKnowledgePageErrors[keyof CreateKnowledgePageErrors];
|
||||
|
||||
export type CreateKnowledgePageResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
201: CreateKnowledgePageResponse;
|
||||
};
|
||||
|
||||
export type CreateKnowledgePageResponse2 =
|
||||
CreateKnowledgePageResponses[keyof CreateKnowledgePageResponses];
|
||||
|
||||
export type GetKnowledgeBaseGraphData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/graph";
|
||||
};
|
||||
|
||||
export type GetKnowledgeBaseGraphErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetKnowledgeBaseGraphError =
|
||||
GetKnowledgeBaseGraphErrors[keyof GetKnowledgeBaseGraphErrors];
|
||||
|
||||
export type GetKnowledgeBaseGraphResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: KnowledgePageGraphResponse;
|
||||
};
|
||||
|
||||
export type GetKnowledgeBaseGraphResponse =
|
||||
GetKnowledgeBaseGraphResponses[keyof GetKnowledgeBaseGraphResponses];
|
||||
|
||||
export type ExportKnowledgeBaseData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/export";
|
||||
};
|
||||
|
||||
export type ExportKnowledgeBaseErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ExportKnowledgeBaseError = ExportKnowledgeBaseErrors[keyof ExportKnowledgeBaseErrors];
|
||||
|
||||
export type ExportKnowledgeBaseResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: KnowledgePageBundleResponse;
|
||||
};
|
||||
|
||||
export type ExportKnowledgeBaseResponse =
|
||||
ExportKnowledgeBaseResponses[keyof ExportKnowledgeBaseResponses];
|
||||
|
||||
export type GetKnowledgePageData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
/**
|
||||
* Page Id
|
||||
*/
|
||||
page_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}";
|
||||
};
|
||||
|
||||
export type GetKnowledgePageErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetKnowledgePageError = GetKnowledgePageErrors[keyof GetKnowledgePageErrors];
|
||||
|
||||
export type GetKnowledgePageResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: KnowledgePageResponse;
|
||||
};
|
||||
|
||||
export type GetKnowledgePageResponse = GetKnowledgePageResponses[keyof GetKnowledgePageResponses];
|
||||
|
||||
export type DeleteKnowledgeNodeData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
/**
|
||||
* Node Id
|
||||
*/
|
||||
node_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}";
|
||||
};
|
||||
|
||||
export type DeleteKnowledgeNodeErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteKnowledgeNodeError = DeleteKnowledgeNodeErrors[keyof DeleteKnowledgeNodeErrors];
|
||||
|
||||
export type DeleteKnowledgeNodeResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type UpdateKnowledgeNodeData = {
|
||||
body: UpdateNodeRequest;
|
||||
headers?: {
|
||||
/**
|
||||
* Authorization
|
||||
*/
|
||||
authorization?: string | null;
|
||||
};
|
||||
path: {
|
||||
/**
|
||||
* Bank Id
|
||||
*/
|
||||
bank_id: string;
|
||||
/**
|
||||
* Node Id
|
||||
*/
|
||||
node_id: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}";
|
||||
};
|
||||
|
||||
export type UpdateKnowledgeNodeErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateKnowledgeNodeError = UpdateKnowledgeNodeErrors[keyof UpdateKnowledgeNodeErrors];
|
||||
|
||||
export type UpdateKnowledgeNodeResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: KnowledgeNode;
|
||||
};
|
||||
|
||||
export type UpdateKnowledgeNodeResponse =
|
||||
UpdateKnowledgeNodeResponses[keyof UpdateKnowledgeNodeResponses];
|
||||
|
||||
export type ListDirectivesData = {
|
||||
body?: never;
|
||||
headers?: {
|
||||
|
||||
@@ -38,12 +38,14 @@
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-visually-hidden": "^1.2.5",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -53,6 +55,8 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"cron-parser": "^5.6.1",
|
||||
"cronstrue": "^3.21.0",
|
||||
"cytoscape": "^3.33.1",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.0.1",
|
||||
"lucide-react": "^0.553.0",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Sidebar } from "@/components/sidebar";
|
||||
import { DataView } from "@/components/data-view";
|
||||
import { DocumentsView } from "@/components/documents-view";
|
||||
import { EntitiesView } from "@/components/entities-view";
|
||||
import { KnowledgeBaseView } from "@/components/knowledge-base-view";
|
||||
import { ThinkView } from "@/components/think-view";
|
||||
import { SearchDebugView } from "@/components/search-debug-view";
|
||||
import { BankProfileView } from "@/components/bank-profile-view";
|
||||
@@ -57,7 +58,7 @@ import {
|
||||
import { LlmHealthDialog } from "@/components/llm-health-dialog";
|
||||
import { ExtractDialog } from "@/components/extract-dialog";
|
||||
|
||||
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
|
||||
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "knowledge" | "profile";
|
||||
type DataSubTab = "world" | "experience" | "observations" | "mental-models";
|
||||
type BankConfigTab =
|
||||
| "general"
|
||||
@@ -638,6 +639,13 @@ export default function BankPage() {
|
||||
<EntitiesView />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Knowledge base Tab — KnowledgeBaseView renders its own header. */}
|
||||
{view === "knowledge" && (
|
||||
<div>
|
||||
<KnowledgeBaseView />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ bank
|
||||
const { searchParams } = new URL(request.url);
|
||||
const tags = searchParams.getAll("tags");
|
||||
const tagsMatch = searchParams.get("tags_match");
|
||||
const limit = searchParams.get("limit");
|
||||
const offset = searchParams.get("offset");
|
||||
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
@@ -28,12 +26,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ bank
|
||||
if (tagsMatch) {
|
||||
queryParams.append("tags_match", tagsMatch);
|
||||
}
|
||||
if (limit) {
|
||||
queryParams.append("limit", limit);
|
||||
}
|
||||
if (offset) {
|
||||
queryParams.append("offset", offset);
|
||||
}
|
||||
|
||||
const url = dataplaneBankUrl(
|
||||
bankId,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const bankId = request.nextUrl.searchParams.get("bank_id");
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const response = await fetch(dataplaneBankUrl(bankId, "/knowledge-base/export"), {
|
||||
headers: getDataplaneHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Failed to export knowledge base:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to export knowledge base",
|
||||
errorKey: "api.errors.knowledgeBase.export",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const bankId = request.nextUrl.searchParams.get("bank_id");
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const body = await request.json();
|
||||
const response = await fetch(dataplaneBankUrl(bankId, "/knowledge-base/folders"), {
|
||||
method: "POST",
|
||||
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: response.status });
|
||||
} catch (error) {
|
||||
console.error("Failed to create folder:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to create folder",
|
||||
errorKey: "api.errors.knowledgeBase.createFolder",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const bankId = request.nextUrl.searchParams.get("bank_id");
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const response = await fetch(dataplaneBankUrl(bankId, "/knowledge-base/graph"), {
|
||||
headers: getDataplaneHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch knowledge base graph:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to fetch knowledge base graph",
|
||||
errorKey: "api.errors.knowledgeBase.graph",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
function bankId(request: NextRequest): string | null {
|
||||
return request.nextUrl.searchParams.get("bank_id");
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ nodeId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { nodeId } = await params;
|
||||
const bank = bankId(request);
|
||||
if (!bank) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const body = await request.json();
|
||||
const response = await fetch(
|
||||
dataplaneBankUrl(
|
||||
bank,
|
||||
`/knowledge-base/nodes/${encodeURIComponent(decodeURIComponent(nodeId))}`
|
||||
),
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Failed to update node:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to update node",
|
||||
errorKey: "api.errors.knowledgeBase.updateNode",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ nodeId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { nodeId } = await params;
|
||||
const bank = bankId(request);
|
||||
if (!bank) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const response = await fetch(
|
||||
dataplaneBankUrl(
|
||||
bank,
|
||||
`/knowledge-base/nodes/${encodeURIComponent(decodeURIComponent(nodeId))}`
|
||||
),
|
||||
{ method: "DELETE", headers: getDataplaneHeaders() }
|
||||
);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete node:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to delete node",
|
||||
errorKey: "api.errors.knowledgeBase.deleteNode",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ pageId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { pageId } = await params;
|
||||
const bankId = request.nextUrl.searchParams.get("bank_id");
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const response = await fetch(
|
||||
dataplaneBankUrl(
|
||||
bankId,
|
||||
`/knowledge-base/pages/${encodeURIComponent(decodeURIComponent(pageId))}`
|
||||
),
|
||||
{ headers: getDataplaneHeaders() }
|
||||
);
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch knowledge page:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to fetch knowledge page",
|
||||
errorKey: "api.errors.knowledgeBase.fetchPage",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const bankId = request.nextUrl.searchParams.get("bank_id");
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const body = await request.json();
|
||||
const response = await fetch(dataplaneBankUrl(bankId, "/knowledge-base/pages"), {
|
||||
method: "POST",
|
||||
headers: getDataplaneHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: response.status });
|
||||
} catch (error) {
|
||||
console.error("Failed to create page:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to create page",
|
||||
errorKey: "api.errors.knowledgeBase.createPage",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { localizeApiErrorPayload } from "@/lib/i18n/api-errors";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const bankId = request.nextUrl.searchParams.get("bank_id");
|
||||
if (!bankId) {
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "bank_id is required",
|
||||
errorKey: "api.errors.validation.bankIdRequired",
|
||||
}),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const response = await fetch(dataplaneBankUrl(bankId, "/knowledge-base/tree"), {
|
||||
headers: getDataplaneHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
return NextResponse.json(error, { status: response.status });
|
||||
}
|
||||
return NextResponse.json(await response.json(), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch knowledge base tree:", error);
|
||||
return NextResponse.json(
|
||||
localizeApiErrorPayload(request, {
|
||||
error: "Failed to fetch knowledge base tree",
|
||||
errorKey: "api.errors.knowledgeBase.tree",
|
||||
}),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useRef, useEffect, useCallback, useMemo, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { prepare, layout, prepareWithSegments, layoutWithLines } from "@chenglou/pretext";
|
||||
import type { GraphData, GraphNode, GraphLink } from "./graph-data";
|
||||
import type { GraphData, GraphNode, GraphLink } from "./graph-2d";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -21,12 +21,6 @@ interface PreparedNode {
|
||||
/** Color derived from link count (heat gradient) */
|
||||
heatColor: string;
|
||||
linkCount: number;
|
||||
/**
|
||||
* Per-node phase in [0, 2π), derived from the id hash. Desynchronizes the
|
||||
* ambient drift + pulse so the field breathes organically instead of in
|
||||
* lockstep. Precomputed here so the animation loop stays trig-only.
|
||||
*/
|
||||
phase: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -378,7 +372,6 @@ export function Constellation({
|
||||
// so the grouping reads at a glance; otherwise it keeps the heat gradient.
|
||||
heatColor: centroid ? color : heat,
|
||||
linkCount: lc,
|
||||
phase: ((Math.abs(seed) % 1000) / 1000) * Math.PI * 2,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -455,24 +448,15 @@ export function Constellation({
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// Ambient-motion clock (seconds).
|
||||
const time = (typeof performance !== "undefined" ? performance.now() : 0) / 1000;
|
||||
// Drift amplitude in world units — nodes slowly wander around their home
|
||||
// position so the whole field visibly breathes.
|
||||
const DRIFT_AMP = 16;
|
||||
|
||||
// Screen positions (with a slow per-node ambient drift baked in, so links —
|
||||
// which read straight from screenX/screenY below — follow for free).
|
||||
// Screen positions
|
||||
const screenX = new Float32Array(preparedNodes.length);
|
||||
const screenY = new Float32Array(preparedNodes.length);
|
||||
const visible = new Uint8Array(preparedNodes.length);
|
||||
|
||||
for (let i = 0; i < preparedNodes.length; i++) {
|
||||
const n = preparedNodes[i];
|
||||
const driftX = DRIFT_AMP * Math.sin(time * 0.6 + n.phase);
|
||||
const driftY = DRIFT_AMP * Math.cos(time * 0.5 + n.phase * 1.3);
|
||||
const sx = cx + (n.wx + driftX) * zoom;
|
||||
const sy = cy + (n.wy + driftY) * zoom;
|
||||
const sx = cx + n.wx * zoom;
|
||||
const sy = cy + n.wy * zoom;
|
||||
screenX[i] = sx;
|
||||
screenY[i] = sy;
|
||||
visible[i] = sx > -margin && sx < W + margin && sy > -margin && sy < H + margin ? 1 : 0;
|
||||
@@ -524,39 +508,11 @@ export function Constellation({
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.quadraticCurveTo(midX, midY, bx, by);
|
||||
ctx.stroke();
|
||||
|
||||
// A small bead of light travels the curve from the hovered node outward,
|
||||
// so connections read as live signal paths rather than static lines.
|
||||
{
|
||||
// Phase-offset per link so beads don't march in lockstep. Travel runs
|
||||
// from the hovered node (u=0) toward its neighbor (u=1).
|
||||
const fromHovered = link.a === hoverIndex;
|
||||
const raw = (time * 0.22 + (li % 13) / 13) % 1;
|
||||
const u = fromHovered ? raw : 1 - raw;
|
||||
const iu = 1 - u;
|
||||
// Point on the quadratic Bézier at parameter u.
|
||||
const px = iu * iu * ax + 2 * iu * u * midX + u * u * bx;
|
||||
const py = iu * iu * ay + 2 * iu * u * midY + u * u * by;
|
||||
ctx.globalAlpha = 0.9 * (0.4 + 0.6 * Math.sin(u * Math.PI)); // fade at the ends
|
||||
ctx.fillStyle = link.color;
|
||||
ctx.shadowColor = link.color;
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
// Restore the stroke state the loop's next iteration expects.
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.lineWidth = 1.5;
|
||||
}
|
||||
linksDrawn++;
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
} else {
|
||||
// Faint, slow breathing across the whole web so idle links feel alive
|
||||
// without flickering (one global sine, not per-link — stays calm).
|
||||
const shimmer = 1 + 0.18 * Math.sin(time * 0.6);
|
||||
const baseAlpha = (0.06 + Math.min(zoom * 0.04, 0.1)) * shimmer;
|
||||
const baseAlpha = 0.06 + Math.min(zoom * 0.04, 0.1);
|
||||
ctx.lineWidth = 0.4;
|
||||
|
||||
for (const link of linksWithIndices) {
|
||||
@@ -705,18 +661,11 @@ export function Constellation({
|
||||
// Size varies slightly by link count — subtle range like star magnitudes.
|
||||
// When nodeSizeFn is provided (e.g. entities view), it overrides linkCount
|
||||
// sizing so dots can scale by an external weight like co-occurrence count.
|
||||
const rawR = nodeSizeFn ? nodeSizeFn(n.node) : 2.5 + Math.min(n.linkCount * 0.15, 2.5);
|
||||
// Gentle pulse — each dot "breathes" in size, out of phase with its
|
||||
// neighbors, so the field twinkles like a living star map.
|
||||
const pulse = 1 + 0.13 * Math.sin(time * 1.05 + n.phase);
|
||||
const baseR = rawR * pulse;
|
||||
const baseR = nodeSizeFn ? nodeSizeFn(n.node) : 2.5 + Math.min(n.linkCount * 0.15, 2.5);
|
||||
const r = Math.max(1.5, baseR * Math.min(zoom, 2));
|
||||
|
||||
// Opacity varies — fewer links = dimmer, more links = brighter. A brightness
|
||||
// twinkle (offset from the size pulse) makes even tiny dots read as alive,
|
||||
// where a radius pulse alone would be imperceptible.
|
||||
const twinkleAlpha = 0.82 + 0.18 * Math.sin(time * 1.4 + n.phase * 2.1);
|
||||
const baseAlpha = (0.45 + Math.min(n.linkCount * 0.03, 0.5)) * twinkleAlpha;
|
||||
// Opacity varies — fewer links = dimmer, more links = brighter
|
||||
const baseAlpha = 0.45 + Math.min(n.linkCount * 0.03, 0.5);
|
||||
|
||||
// Dot — star-like: heat-gradient color, varied size & opacity
|
||||
ctx.beginPath();
|
||||
@@ -730,14 +679,12 @@ export function Constellation({
|
||||
}
|
||||
ctx.fill();
|
||||
|
||||
// Soft glow halo for brighter stars (high link count) — the halo twinkles
|
||||
// a little (out of phase with the dot's pulse) so hubs feel radiant.
|
||||
// Soft glow halo for brighter stars (high link count)
|
||||
if (n.linkCount > 3 && !isHovered && hoverIndex < 0) {
|
||||
const twinkle = 1 + 0.25 * Math.sin(time * 0.9 + n.phase * 1.7);
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, r * 2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = n.heatColor;
|
||||
ctx.globalAlpha = (0.06 + Math.min(n.linkCount * 0.005, 0.08)) * twinkle;
|
||||
ctx.globalAlpha = 0.06 + Math.min(n.linkCount * 0.005, 0.08);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
@@ -1172,17 +1119,9 @@ export function Constellation({
|
||||
canvas.addEventListener("mouseup", handleMouseUp);
|
||||
canvas.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
// The canvas also changes size when its container reflows (e.g. the side
|
||||
// panel opening/closing) with no window "resize" event. Observe the element
|
||||
// so the backing store is re-measured — otherwise CSS stretches the old
|
||||
// bitmap and the text/dots look squeezed.
|
||||
const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => resize()) : null;
|
||||
ro?.observe(canvas);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animRef.current);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
ro?.disconnect();
|
||||
canvas.removeEventListener("wheel", handleWheel);
|
||||
canvas.removeEventListener("mousemove", handleMouseMove);
|
||||
canvas.removeEventListener("mousedown", handleMouseDown);
|
||||
|
||||
@@ -16,9 +16,11 @@ import {
|
||||
ChevronsRight,
|
||||
Settings2,
|
||||
Eye,
|
||||
EyeOff,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Network,
|
||||
List,
|
||||
Search,
|
||||
Layers,
|
||||
@@ -31,6 +33,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
@@ -39,15 +43,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { MemoryDetailPanel } from "./memory-detail-panel";
|
||||
import { MemoryDetailModal } from "./memory-detail-modal";
|
||||
import { convertHindsightGraphData, GraphNode } from "./graph-data";
|
||||
import { Graph2D, convertHindsightGraphData, GraphNode } from "./graph-2d";
|
||||
import { Constellation } from "./constellation";
|
||||
import { TagFilterInput } from "./tag-filter-input";
|
||||
import { ObservationScopeFilter, ObservationScope } from "./observation-scope-filter";
|
||||
import { ScatterChart, Plus, FileText } from "lucide-react";
|
||||
|
||||
type FactType = "world" | "experience" | "observation";
|
||||
type ViewMode = "table" | "timeline" | "constellation";
|
||||
type ViewMode = "graph" | "table" | "timeline" | "constellation";
|
||||
|
||||
// Categorical palette for coloring observation scopes (exact tag sets) when
|
||||
// "Group by scope" clusters the constellation. Distinct, reasonably separable hues.
|
||||
@@ -100,6 +105,7 @@ export function DataView({
|
||||
const [scopes, setScopes] = useState<ObservationScope[]>([]);
|
||||
const [selectedScope, setSelectedScope] = useState<string[] | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||
const [modalMemoryId, setModalMemoryId] = useState<string | null>(null);
|
||||
// Table view: toggle between live facts (graph-fed) and invalidated facts (archive).
|
||||
const [showInvalidated, setShowInvalidated] = useState(false);
|
||||
@@ -126,7 +132,10 @@ export function DataView({
|
||||
last_consolidated_at: string | null;
|
||||
} | null>(null);
|
||||
|
||||
// Constellation controls state
|
||||
// Graph controls state
|
||||
const [showLabels, setShowLabels] = useState(true);
|
||||
const [maxNodes, setMaxNodes] = useState<number | undefined>(undefined);
|
||||
const [showControlPanel, setShowControlPanel] = useState(true);
|
||||
const [visibleLinkTypes, setVisibleLinkTypes] = useState<Set<string>>(
|
||||
new Set(["semantic", "temporal", "entity", "causal"])
|
||||
);
|
||||
@@ -143,6 +152,17 @@ export function DataView({
|
||||
});
|
||||
};
|
||||
|
||||
// Esc key handler to deselect graph node
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && selectedGraphNode) {
|
||||
setSelectedGraphNode(null);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedGraphNode]);
|
||||
|
||||
// `silent` skips the loading spinner — used by the background consolidation
|
||||
// poll so the view refreshes in place without flashing.
|
||||
const loadData = async (
|
||||
@@ -210,8 +230,6 @@ export function DataView({
|
||||
if (showInvalidated) return invalidatedRows;
|
||||
return data?.table_rows ?? [];
|
||||
}, [data, showInvalidated, invalidatedRows]);
|
||||
const hasActiveMemoryFilters =
|
||||
searchQuery.trim().length > 0 || tagFilters.length > 0 || selectedScope !== null;
|
||||
|
||||
// Helper to get normalized link type
|
||||
const getLinkTypeCategory = (type: string | undefined): string => {
|
||||
@@ -221,7 +239,7 @@ export function DataView({
|
||||
return "semantic";
|
||||
};
|
||||
|
||||
// Convert data for the constellation (graph data is already filtered server-side)
|
||||
// Convert data for Graph2D (graph data is already filtered server-side)
|
||||
const graph2DData = useMemo(() => {
|
||||
if (!data) return { nodes: [], links: [] };
|
||||
const fullData = convertHindsightGraphData(data);
|
||||
@@ -235,11 +253,44 @@ export function DataView({
|
||||
return { nodes: fullData.nodes, links };
|
||||
}, [data, visibleLinkTypes]);
|
||||
|
||||
// Calculate link stats for display
|
||||
const linkStats = useMemo(() => {
|
||||
let semantic = 0,
|
||||
temporal = 0,
|
||||
entity = 0,
|
||||
causal = 0,
|
||||
total = 0;
|
||||
const otherTypes: Record<string, number> = {};
|
||||
graph2DData.links.forEach((l) => {
|
||||
total++;
|
||||
const type = l.type || "unknown";
|
||||
if (type === "semantic") semantic++;
|
||||
else if (type === "temporal") temporal++;
|
||||
else if (type === "entity") entity++;
|
||||
else if (
|
||||
type === "causes" ||
|
||||
type === "caused_by" ||
|
||||
type === "enables" ||
|
||||
type === "prevents"
|
||||
)
|
||||
causal++;
|
||||
else {
|
||||
otherTypes[type] = (otherTypes[type] || 0) + 1;
|
||||
}
|
||||
});
|
||||
return { semantic, temporal, entity, causal, total, otherTypes };
|
||||
}, [graph2DData]);
|
||||
|
||||
// Handle node click in graph - show in panel
|
||||
const handleGraphNodeClick = useCallback((node: GraphNode) => {
|
||||
// Open the memory dialog for the clicked node (same dialog the table/timeline use).
|
||||
setModalMemoryId(node.id);
|
||||
}, []);
|
||||
const handleGraphNodeClick = useCallback(
|
||||
(node: GraphNode) => {
|
||||
const nodeData = data?.table_rows?.find((row: any) => row.id === node.id);
|
||||
if (nodeData) {
|
||||
setSelectedGraphNode(nodeData);
|
||||
}
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
// Memoized color functions to prevent graph re-initialization
|
||||
// Uses brand colors: primary blue (#0074d9), teal (#009296), amber for entity, purple for causal
|
||||
@@ -458,6 +509,19 @@ export function DataView({
|
||||
return () => clearInterval(id);
|
||||
}, [isConsolidating, currentBank]);
|
||||
|
||||
// Enforce 50 node limit to prevent UI instability, default to 20 or max whichever is smaller
|
||||
useEffect(() => {
|
||||
if (data && maxNodes === undefined) {
|
||||
if (graph2DData.nodes.length > 50) {
|
||||
// Always set maxNodes to 20 when we have >50 nodes (never leave as undefined)
|
||||
setMaxNodes(20);
|
||||
} else if (graph2DData.nodes.length > 20) {
|
||||
setMaxNodes(20);
|
||||
}
|
||||
// If ≤20 nodes, leave maxNodes undefined to show all
|
||||
}
|
||||
}, [data, graph2DData.nodes.length, maxNodes]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && !data ? (
|
||||
@@ -465,7 +529,7 @@ export function DataView({
|
||||
<RefreshCw className="w-8 h-8 mx-auto mb-3 text-muted-foreground animate-spin" />
|
||||
<p className="text-muted-foreground">{t("loadingMemories")}</p>
|
||||
</div>
|
||||
) : data && data.total_units === 0 && !hasActiveMemoryFilters ? (
|
||||
) : data && data.total_units === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<FileText className="w-10 h-10 mx-auto mb-4 text-muted-foreground/50" />
|
||||
<h3 className="text-base font-medium text-foreground mb-1">{t("noMemoriesYet")}</h3>
|
||||
@@ -580,7 +644,7 @@ export function DataView({
|
||||
</Button>
|
||||
)}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{hasActiveMemoryFilters ? (
|
||||
{searchQuery || tagFilters.length > 0 ? (
|
||||
t("matchingMemories", { count: filteredTableRows.length })
|
||||
) : data.table_rows?.length < data.total_units ? (
|
||||
<span>
|
||||
@@ -592,8 +656,11 @@ export function DataView({
|
||||
onClick={() => {
|
||||
const newLimit = Math.min(data.total_units, fetchLimit + 1000);
|
||||
setFetchLimit(newLimit);
|
||||
const { tags, match } = resolveTagQuery();
|
||||
loadData(newLimit, searchQuery || undefined, tags, match);
|
||||
loadData(
|
||||
newLimit,
|
||||
searchQuery || undefined,
|
||||
tagFilters.length > 0 ? tagFilters : undefined
|
||||
);
|
||||
}}
|
||||
className="ml-2 text-primary hover:underline"
|
||||
>
|
||||
@@ -638,10 +705,11 @@ export function DataView({
|
||||
{t("pendingCount", { count: consolidationStatus.pending_consolidation })}
|
||||
<button
|
||||
onClick={() =>
|
||||
(() => {
|
||||
const { tags, match } = resolveTagQuery();
|
||||
loadData(fetchLimit, searchQuery || undefined, tags, match);
|
||||
})()
|
||||
loadData(
|
||||
fetchLimit,
|
||||
searchQuery || undefined,
|
||||
tagFilters.length > 0 ? tagFilters : undefined
|
||||
)
|
||||
}
|
||||
disabled={loading}
|
||||
className="ml-0.5 opacity-70 hover:opacity-100 disabled:opacity-40 transition-opacity"
|
||||
@@ -666,6 +734,17 @@ export function DataView({
|
||||
<ScatterChart className="w-4 h-4" />
|
||||
{t("constellation")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("graph")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
viewMode === "graph"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Network className="w-4 h-4" />
|
||||
{t("graph")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("table")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
@@ -692,76 +771,244 @@ export function DataView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(compactMode || viewMode === "constellation") && (
|
||||
<div className="space-y-3">
|
||||
{/* Constellation controls — moved out of the old side panel to sit
|
||||
inline above the graph, next to the view toggle / filters. */}
|
||||
{!compactMode && (
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2">
|
||||
{factType === "observation" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t("groupByScope")}
|
||||
</span>
|
||||
<Switch checked={groupByScope} onCheckedChange={setGroupByScope} />
|
||||
</div>
|
||||
)}
|
||||
{!(factType === "observation" && groupByScope) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t("colorBy")}
|
||||
</span>
|
||||
<Select
|
||||
value={recencyBasis}
|
||||
onValueChange={(v) => setRecencyBasis(v as RecencyBasis)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-44 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mentioned_at">{t("mentioned")}</SelectItem>
|
||||
<SelectItem value="occurred_start">{t("occurredStart")}</SelectItem>
|
||||
<SelectItem value="occurred_end">{t("occurredEnd")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{t("linkTypes")}
|
||||
</span>
|
||||
{Object.entries({
|
||||
semantic: "#0074d9",
|
||||
temporal: "#009296",
|
||||
entity: "#f59e0b",
|
||||
causal: "#8b5cf6",
|
||||
}).map(([type, color]) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
className="flex items-center gap-1.5"
|
||||
onClick={() => toggleLinkType(type)}
|
||||
>
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
opacity: visibleLinkTypes.has(type) ? 1 : 0.2,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={`text-xs capitalize ${visibleLinkTypes.has(type) ? "text-foreground" : "text-muted-foreground line-through"}`}
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!compactMode && viewMode === "graph" && (
|
||||
<div className="flex gap-0">
|
||||
{/* Graph */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Graph2D
|
||||
data={graph2DData}
|
||||
height={700}
|
||||
showLabels={showLabels}
|
||||
onNodeClick={handleGraphNodeClick}
|
||||
maxNodes={maxNodes}
|
||||
nodeColorFn={nodeColorFn}
|
||||
linkColorFn={linkColorFn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
{/* Right Toggle Button */}
|
||||
<button
|
||||
onClick={() => setShowControlPanel(!showControlPanel)}
|
||||
className="flex-shrink-0 w-5 h-[700px] bg-transparent hover:bg-muted/50 flex items-center justify-center transition-colors"
|
||||
title={showControlPanel ? t("hidePanel") : t("showPanel")}
|
||||
>
|
||||
{showControlPanel ? (
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground/60" />
|
||||
) : (
|
||||
<ChevronLeft className="w-3 h-3 text-muted-foreground/60" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Right Panel - Legend/Controls OR Memory Details */}
|
||||
<div
|
||||
className={`${showControlPanel ? "w-80" : "w-0"} transition-all duration-300 overflow-hidden flex-shrink-0`}
|
||||
>
|
||||
<div className="w-80 h-[700px] bg-card border-l border-border overflow-y-auto">
|
||||
{selectedGraphNode ? (
|
||||
/* Memory Detail View */
|
||||
<MemoryDetailPanel
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
inPanel
|
||||
bankId={currentBank || undefined}
|
||||
/>
|
||||
) : (
|
||||
/* Legend & Controls View */
|
||||
<div className="p-4 space-y-5">
|
||||
{/* Legend & Stats */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-foreground">
|
||||
{t("graphTitle")}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{/* Nodes */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: "#0074d9" }}
|
||||
/>
|
||||
<span className="text-foreground">{t("nodes")}</span>
|
||||
</div>
|
||||
<span className="font-mono text-foreground">
|
||||
{Math.min(
|
||||
maxNodes ?? graph2DData.nodes.length,
|
||||
graph2DData.nodes.length
|
||||
)}
|
||||
/{graph2DData.nodes.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-medium text-muted-foreground mt-2 mb-1">
|
||||
{t("linksWithCount", { count: linkStats.total })}{" "}
|
||||
<span className="text-muted-foreground/60">{t("clickToFilter")}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggleLinkType("semantic")}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has("semantic")
|
||||
? "hover:bg-muted"
|
||||
: "opacity-40 hover:opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#0074d9]" />
|
||||
<span className="text-foreground">{t("semantic")}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`font-mono ${linkStats.semantic === 0 ? "text-destructive" : "text-foreground"}`}
|
||||
>
|
||||
{linkStats.semantic}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleLinkType("temporal")}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has("temporal")
|
||||
? "hover:bg-muted"
|
||||
: "opacity-40 hover:opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#009296]" />
|
||||
<span className="text-foreground">{t("temporal")}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`font-mono ${linkStats.temporal === 0 ? "text-destructive" : "text-foreground"}`}
|
||||
>
|
||||
{linkStats.temporal}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleLinkType("entity")}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has("entity")
|
||||
? "hover:bg-muted"
|
||||
: "opacity-40 hover:opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#f59e0b]" />
|
||||
<span className="text-foreground">{t("entity")}</span>
|
||||
</div>
|
||||
<span className="font-mono text-foreground">{linkStats.entity}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleLinkType("causal")}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has("causal")
|
||||
? "hover:bg-muted"
|
||||
: "opacity-40 hover:opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#8b5cf6]" />
|
||||
<span className="text-foreground">{t("causal")}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`font-mono ${linkStats.causal === 0 ? "text-muted-foreground" : "text-foreground"}`}
|
||||
>
|
||||
{linkStats.causal}
|
||||
</span>
|
||||
</button>
|
||||
{Object.entries(linkStats.otherTypes || {}).map(([type, count]) => (
|
||||
<div key={type} className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground capitalize ml-6">{type}</span>
|
||||
<span className="font-mono text-muted-foreground">
|
||||
{count as number}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Controls Section */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-foreground">
|
||||
{t("displayTitle")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="show-labels" className="text-sm text-foreground">
|
||||
{t("showLabels")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="show-labels"
|
||||
checked={showLabels}
|
||||
onCheckedChange={setShowLabels}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Limits Section */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-foreground">
|
||||
{t("performanceTitle")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-sm text-foreground">{t("maxNodes")}</Label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{graph2DData.nodes.length > 50
|
||||
? `${maxNodes ?? 50} / ${graph2DData.nodes.length}`
|
||||
: `${maxNodes ?? "All"} / ${graph2DData.nodes.length}`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[
|
||||
graph2DData.nodes.length > 50
|
||||
? maxNodes || 20
|
||||
: maxNodes || Math.min(graph2DData.nodes.length, 20),
|
||||
]}
|
||||
min={10}
|
||||
max={Math.min(Math.max(graph2DData.nodes.length, 10), 50)}
|
||||
step={10}
|
||||
onValueChange={([v]) => {
|
||||
const effectiveMax = Math.min(graph2DData.nodes.length, 50);
|
||||
// If we have >50 nodes, never allow "All" (undefined), cap at 50
|
||||
if (graph2DData.nodes.length > 50) {
|
||||
setMaxNodes(v);
|
||||
} else {
|
||||
// Original behavior for ≤50 nodes: allow "All" when slider reaches max
|
||||
setMaxNodes(v >= effectiveMax ? undefined : v);
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("allLinksVisible")}
|
||||
{graph2DData.nodes.length > 50 && (
|
||||
<span className="block text-amber-600 dark:text-amber-400 mt-1">
|
||||
{t("limitedTo50Nodes", { count: graph2DData.nodes.length })}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Hint */}
|
||||
<div className="text-xs text-muted-foreground/60 text-center pt-2">
|
||||
{t("clickNodeForDetails")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(compactMode || viewMode === "constellation") && (
|
||||
<div className="flex gap-0">
|
||||
<div className="flex-1 min-w-0 border border-border rounded-lg overflow-hidden">
|
||||
<Constellation
|
||||
key={compactMode ? "compact" : "full"}
|
||||
data={graph2DData}
|
||||
@@ -802,6 +1049,119 @@ export function DataView({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Toggle Button + Panel (hidden in compact mode) */}
|
||||
{!compactMode && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowControlPanel(!showControlPanel)}
|
||||
className="flex-shrink-0 w-5 h-[700px] bg-transparent hover:bg-muted/50 flex items-center justify-center transition-colors"
|
||||
title={showControlPanel ? t("hidePanel") : t("showPanel")}
|
||||
>
|
||||
{showControlPanel ? (
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronLeft className="w-3 h-3 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Right Panel — reuse the same panel as graph view */}
|
||||
{showControlPanel && (
|
||||
<div className="w-72 flex-shrink-0 border border-border rounded-lg bg-muted/20 overflow-y-auto h-[700px]">
|
||||
{selectedGraphNode ? (
|
||||
<MemoryDetailPanel
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
inPanel
|
||||
bankId={currentBank || undefined}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-4 space-y-4">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{t("constellationViewTitle")}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("constellationViewDescription")}
|
||||
</p>
|
||||
{factType === "observation" && (
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Layers className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<h4 className="text-xs font-medium text-muted-foreground">
|
||||
{t("groupByScope")}
|
||||
</h4>
|
||||
</div>
|
||||
<Switch checked={groupByScope} onCheckedChange={setGroupByScope} />
|
||||
</div>
|
||||
)}
|
||||
{!(factType === "observation" && groupByScope) && (
|
||||
<div className="space-y-2 pt-2">
|
||||
<h4 className="text-xs font-medium text-muted-foreground">
|
||||
{t("colorBy")}
|
||||
</h4>
|
||||
<Select
|
||||
value={recencyBasis}
|
||||
onValueChange={(v) => setRecencyBasis(v as RecencyBasis)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mentioned_at">{t("mentioned")}</SelectItem>
|
||||
<SelectItem value="occurred_start">
|
||||
{t("occurredStart")}
|
||||
</SelectItem>
|
||||
<SelectItem value="occurred_end">{t("occurredEnd")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2 pt-2">
|
||||
<h4 className="text-xs font-medium text-muted-foreground">
|
||||
{t("linkTypes")}
|
||||
</h4>
|
||||
{Object.entries({
|
||||
semantic: "#0074d9",
|
||||
temporal: "#009296",
|
||||
entity: "#f59e0b",
|
||||
causal: "#8b5cf6",
|
||||
}).map(([type, color]) => (
|
||||
<div
|
||||
key={type}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => toggleLinkType(type)}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
opacity: visibleLinkTypes.has(type) ? 1 : 0.2,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={`text-xs capitalize ${visibleLinkTypes.has(type) ? "text-foreground" : "text-muted-foreground line-through"}`}
|
||||
>
|
||||
{type}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1 pt-2">
|
||||
<div>
|
||||
{t("nodes")}:{" "}
|
||||
<span className="text-foreground">{graph2DData.nodes.length}</span>
|
||||
</div>
|
||||
<div>
|
||||
{t("links")}:{" "}
|
||||
<span className="text-foreground">{graph2DData.links.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1036,7 +1396,9 @@ export function DataView({
|
||||
})()
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
{hasActiveMemoryFilters ? t("noMemoriesMatchFilter") : t("noMemoriesFound")}
|
||||
{data.table_rows?.length > 0
|
||||
? t("noMemoriesMatchFilter")
|
||||
: t("noMemoriesFound")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Constellation } from "./constellation";
|
||||
import { convertHindsightGraphData, GraphNode } from "./graph-data";
|
||||
import { convertHindsightGraphData, GraphNode } from "./graph-2d";
|
||||
|
||||
type EntityGraphResponse = Awaited<ReturnType<typeof client.getEntityGraph>>;
|
||||
|
||||
|
||||
@@ -0,0 +1,726 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useState, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import cytoscape from "cytoscape";
|
||||
|
||||
import fcose from "cytoscape-fcose";
|
||||
|
||||
// Register the fcose extension
|
||||
cytoscape.use(fcose);
|
||||
|
||||
// Hook to detect dark mode
|
||||
function useIsDarkMode() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDark = () => {
|
||||
setIsDark(document.documentElement.classList.contains("dark"));
|
||||
};
|
||||
|
||||
checkDark();
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(checkDark);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return isDark;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Types & Interfaces
|
||||
// ============================================================================
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
group?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphLink {
|
||||
source: string;
|
||||
target: string;
|
||||
color?: string;
|
||||
width?: number;
|
||||
type?: string;
|
||||
entity?: string;
|
||||
weight?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[];
|
||||
links: GraphLink[];
|
||||
}
|
||||
|
||||
export interface Graph2DProps {
|
||||
data: GraphData;
|
||||
height?: number;
|
||||
showLabels?: boolean;
|
||||
onNodeClick?: (node: GraphNode) => void;
|
||||
onNodeHover?: (node: GraphNode | null) => void;
|
||||
nodeColorFn?: (node: GraphNode) => string;
|
||||
nodeSizeFn?: (node: GraphNode) => number;
|
||||
linkColorFn?: (link: GraphLink) => string;
|
||||
linkWidthFn?: (link: GraphLink) => number;
|
||||
maxNodes?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Values
|
||||
// ============================================================================
|
||||
|
||||
// Brand colors
|
||||
const BRAND_PRIMARY = "#0074d9";
|
||||
const LINK_SEMANTIC = "#0074d9"; // Primary blue for semantic
|
||||
|
||||
const DEFAULT_NODE_COLOR = BRAND_PRIMARY;
|
||||
const DEFAULT_LINK_COLOR = LINK_SEMANTIC;
|
||||
const DEFAULT_LINK_WIDTH = 1;
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
|
||||
export function Graph2D({
|
||||
data,
|
||||
height = 600,
|
||||
showLabels = true,
|
||||
onNodeClick,
|
||||
onNodeHover,
|
||||
nodeColorFn,
|
||||
nodeSizeFn,
|
||||
linkColorFn,
|
||||
linkWidthFn,
|
||||
maxNodes,
|
||||
}: Graph2DProps) {
|
||||
const t = useTranslations("graph2d");
|
||||
const [containerDiv, setContainerDiv] = useState<HTMLDivElement | null>(null);
|
||||
const cyRef = useRef<any>(null);
|
||||
const isInitializingRef = useRef(false);
|
||||
const lastDataSignatureRef = useRef<string>("");
|
||||
const [_hoveredNode, setHoveredNode] = useState<GraphNode | null>(null);
|
||||
const [hoveredLink, setHoveredLink] = useState<GraphLink | null>(null);
|
||||
const [linkTooltipPos, setLinkTooltipPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isFocusMode, setIsFocusMode] = useState(false);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
// Use refs to store callbacks and data to prevent re-renders from resetting the graph
|
||||
const onNodeClickRef = useRef(onNodeClick);
|
||||
const onNodeHoverRef = useRef(onNodeHover);
|
||||
const fullDataRef = useRef(data);
|
||||
const nodeColorFnRef = useRef(nodeColorFn);
|
||||
const linkColorFnRef = useRef(linkColorFn);
|
||||
const isFocusModeRef = useRef(isFocusMode);
|
||||
onNodeClickRef.current = onNodeClick;
|
||||
onNodeHoverRef.current = onNodeHover;
|
||||
fullDataRef.current = data;
|
||||
nodeColorFnRef.current = nodeColorFn;
|
||||
linkColorFnRef.current = linkColorFn;
|
||||
isFocusModeRef.current = isFocusMode;
|
||||
|
||||
// Transform and limit data - only limit nodes, show ALL links between visible nodes
|
||||
const graphData = useMemo(() => {
|
||||
let nodes = [...data.nodes];
|
||||
|
||||
// Limit nodes if needed
|
||||
if (maxNodes && nodes.length > maxNodes) {
|
||||
nodes = nodes.slice(0, maxNodes);
|
||||
}
|
||||
|
||||
// Show ALL links between visible nodes (no random link limiting)
|
||||
const nodeIds = new Set(nodes.map((n) => n.id));
|
||||
const links = data.links.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
|
||||
|
||||
return { nodes, links };
|
||||
}, [data, maxNodes]);
|
||||
|
||||
// Track mounting state
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
return () => setIsMounted(false);
|
||||
}, []);
|
||||
|
||||
// Convert to Cytoscape format
|
||||
const cyElements = useMemo(() => {
|
||||
// Calculate node importance based on connections
|
||||
const nodeConnections = new Map<string, number>();
|
||||
graphData.links.forEach((link) => {
|
||||
nodeConnections.set(link.source, (nodeConnections.get(link.source) || 0) + 1);
|
||||
nodeConnections.set(link.target, (nodeConnections.get(link.target) || 0) + 1);
|
||||
});
|
||||
|
||||
const nodes = graphData.nodes.map((node) => {
|
||||
const connections = nodeConnections.get(node.id) || 0;
|
||||
const dynamicSize = nodeSizeFn
|
||||
? nodeSizeFn(node)
|
||||
: Math.max(16, Math.min(40, 16 + connections * 4)); // Smaller, more subtle sizing
|
||||
|
||||
return {
|
||||
data: {
|
||||
id: node.id,
|
||||
label: node.label || node.id.substring(0, 8),
|
||||
color: nodeColorFn ? nodeColorFn(node) : node.color || DEFAULT_NODE_COLOR,
|
||||
size: node.size || dynamicSize,
|
||||
originalNode: node,
|
||||
connections: connections,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const edges = graphData.links.map((link, idx) => ({
|
||||
data: {
|
||||
id: `edge-${idx}`,
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
color: linkColorFn ? linkColorFn(link) : link.color || DEFAULT_LINK_COLOR,
|
||||
width: linkWidthFn ? linkWidthFn(link) : link.width || DEFAULT_LINK_WIDTH,
|
||||
type: link.type,
|
||||
entity: link.entity,
|
||||
weight: link.weight,
|
||||
originalLink: link,
|
||||
},
|
||||
}));
|
||||
|
||||
return [...nodes, ...edges];
|
||||
}, [graphData, nodeColorFn, nodeSizeFn, linkColorFn, linkWidthFn]);
|
||||
|
||||
// Create data signature to prevent double initialization
|
||||
const dataSignature = useMemo(() => {
|
||||
return JSON.stringify({
|
||||
nodeCount: graphData.nodes.length,
|
||||
linkCount: graphData.links.length,
|
||||
nodeIds: graphData.nodes
|
||||
.map((n) => n.id)
|
||||
.sort()
|
||||
.join(","),
|
||||
showLabels,
|
||||
isDarkMode,
|
||||
maxNodes,
|
||||
});
|
||||
}, [graphData.nodes, graphData.links, showLabels, isDarkMode, maxNodes]);
|
||||
|
||||
// Initialize Cytoscape
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
// Small delay to ensure container is mounted
|
||||
const timeout = setTimeout(() => {
|
||||
if (isCancelled || !isMounted || !containerDiv || isInitializingRef.current) return;
|
||||
|
||||
// Check if data has actually changed to prevent double initialization
|
||||
if (lastDataSignatureRef.current === dataSignature) {
|
||||
console.log("Data signature unchanged, skipping graph initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
// Additional validation - check if element has dimensions
|
||||
const rect = containerDiv.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
console.warn("Container has no dimensions, skipping cytoscape initialization");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle empty data case
|
||||
if (cyElements.length === 0) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we already have a graph with the same data
|
||||
if (cyRef.current && !cyRef.current.destroyed()) {
|
||||
const currentNodes = cyRef.current.nodes().length;
|
||||
const currentEdges = cyRef.current.edges().length;
|
||||
const newNodes = cyElements.filter((el) => !(el.data as any).source).length;
|
||||
const newEdges = cyElements.filter((el) => (el.data as any).source).length;
|
||||
|
||||
// If the element counts are the same, just update styles and skip reinitialization
|
||||
if (currentNodes === newNodes && currentEdges === newEdges) {
|
||||
console.log("Graph already initialized with same data, skipping reinitialization");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up existing graph before creating new one
|
||||
console.log("Data changed, destroying existing graph");
|
||||
cyRef.current.destroy();
|
||||
cyRef.current = null;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
isInitializingRef.current = true;
|
||||
|
||||
// Theme-aware colors
|
||||
const textColor = isDarkMode ? "#ffffff" : "#1f2937";
|
||||
const textBgColor = isDarkMode ? "rgba(0,0,0,0.8)" : "rgba(255,255,255,0.9)";
|
||||
|
||||
try {
|
||||
console.log("Initializing cytoscape with container:", containerDiv);
|
||||
console.log("Elements count:", cyElements.length);
|
||||
console.log("Sample elements:", cyElements.slice(0, 2));
|
||||
|
||||
// Try minimal initialization first
|
||||
const cy = cytoscape({
|
||||
container: containerDiv,
|
||||
elements: [],
|
||||
// Disable edge selection to prevent gray border on click
|
||||
selectionType: "single",
|
||||
userZoomingEnabled: true,
|
||||
userPanningEnabled: true,
|
||||
boxSelectionEnabled: false,
|
||||
// Disable automatic layout on initialization
|
||||
layout: { name: "preset" },
|
||||
style: [
|
||||
{
|
||||
selector: "node",
|
||||
style: {
|
||||
"background-color": "data(color)",
|
||||
width: "data(size)",
|
||||
height: "data(size)",
|
||||
label: showLabels ? "data(label)" : "",
|
||||
color: textColor,
|
||||
"text-valign": "bottom",
|
||||
"text-halign": "center",
|
||||
"font-size": "8px",
|
||||
"font-weight": 500,
|
||||
"text-margin-y": 3,
|
||||
"text-wrap": "wrap",
|
||||
"text-max-width": "80px",
|
||||
"text-background-color": textBgColor,
|
||||
"text-background-opacity": 0.9,
|
||||
"text-background-padding": "2px",
|
||||
"text-background-shape": "roundrectangle",
|
||||
"border-width": 1,
|
||||
"border-color": isDarkMode ? "#ffffff20" : "#00000020",
|
||||
"border-opacity": 0.3,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node:selected",
|
||||
style: {
|
||||
"border-width": 3,
|
||||
"border-color": "#0074d9",
|
||||
"border-opacity": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "edge",
|
||||
style: {
|
||||
width: "data(width)",
|
||||
"line-color": "data(color)",
|
||||
"target-arrow-color": "data(color)",
|
||||
"target-arrow-shape": "triangle",
|
||||
"target-arrow-size": 6,
|
||||
"curve-style": "bezier",
|
||||
opacity: isDarkMode ? 0.6 : 0.7,
|
||||
},
|
||||
},
|
||||
// Focus mode styles
|
||||
{
|
||||
selector: ".dimmed",
|
||||
style: {
|
||||
opacity: 0.2,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ".focused",
|
||||
style: {
|
||||
"border-width": 4,
|
||||
"border-color": "#ff6b35",
|
||||
"border-opacity": 1,
|
||||
"z-index": 999,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: ".connected",
|
||||
style: {
|
||||
"border-width": 2,
|
||||
"border-color": "#0074d9",
|
||||
"border-opacity": 0.8,
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "edge.connection",
|
||||
style: {
|
||||
width: 2,
|
||||
opacity: 1,
|
||||
"z-index": 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "edge.connection:hover",
|
||||
style: {
|
||||
width: 3,
|
||||
opacity: 1,
|
||||
"z-index": 200,
|
||||
},
|
||||
},
|
||||
// Disable edge selection styling
|
||||
{
|
||||
selector: "edge:selected",
|
||||
style: {
|
||||
"overlay-opacity": 0,
|
||||
"overlay-color": "transparent",
|
||||
"overlay-padding": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
cyRef.current = cy;
|
||||
|
||||
console.log("Cytoscape initialized successfully");
|
||||
|
||||
// Add elements after initialization
|
||||
if (cyElements.length > 0) {
|
||||
console.log("Adding elements to cytoscape");
|
||||
cy.add(cyElements);
|
||||
cy.layout({
|
||||
name: "fcose",
|
||||
quality: "default",
|
||||
randomize: false,
|
||||
animate: true,
|
||||
animationDuration: 1500,
|
||||
// Separation settings - increase to spread nodes more
|
||||
nodeSeparation: 200,
|
||||
idealEdgeLength: () => 250,
|
||||
edgeElasticity: () => 0.05,
|
||||
nestingFactor: 0.05,
|
||||
gravity: 0.05, // Reduced gravity spreads nodes more
|
||||
numIter: 2500,
|
||||
// Overlap prevention
|
||||
nodeOverlap: 30,
|
||||
avoidOverlap: true,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
// Layout bounds - reduce padding to use more space
|
||||
padding: 20,
|
||||
boundingBox: undefined,
|
||||
// Tiling - increase spacing between disconnected components
|
||||
tile: true,
|
||||
tilingPaddingVertical: 30,
|
||||
tilingPaddingHorizontal: 30,
|
||||
// Force more spread
|
||||
uniformNodeDimensions: false,
|
||||
packComponents: false, // Don't pack components tightly
|
||||
}).run();
|
||||
|
||||
// Fit to viewport
|
||||
cy.fit();
|
||||
}
|
||||
|
||||
// Add basic interactions
|
||||
cy.on("tap", "node", (evt: any) => {
|
||||
const node = evt.target as cytoscape.NodeSingular;
|
||||
const originalNode = node.data("originalNode") as GraphNode;
|
||||
if (onNodeClickRef.current && originalNode) {
|
||||
onNodeClickRef.current(originalNode);
|
||||
}
|
||||
});
|
||||
|
||||
cy.on("mouseover", "node", (evt: any) => {
|
||||
const node = evt.target as cytoscape.NodeSingular;
|
||||
const originalNode = node.data("originalNode") as GraphNode;
|
||||
setHoveredNode(originalNode);
|
||||
if (onNodeHoverRef.current && originalNode) {
|
||||
onNodeHoverRef.current(originalNode);
|
||||
}
|
||||
if (containerDiv) containerDiv.style.cursor = "pointer";
|
||||
});
|
||||
|
||||
cy.on("mouseout", "node", () => {
|
||||
setHoveredNode(null);
|
||||
if (onNodeHoverRef.current) {
|
||||
onNodeHoverRef.current(null);
|
||||
}
|
||||
if (containerDiv) containerDiv.style.cursor = "default";
|
||||
});
|
||||
|
||||
// Edge hover handlers - only work in focus mode and on highlighted edges
|
||||
cy.on("mouseover", "edge", (evt: any) => {
|
||||
const edge = evt.target;
|
||||
|
||||
// Only allow interaction if we're in focus mode and edge is highlighted
|
||||
if (!isFocusModeRef.current || !edge.hasClass("connection")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalLink = edge.data("originalLink") as GraphLink;
|
||||
if (originalLink) {
|
||||
setHoveredLink(originalLink);
|
||||
// Get position for tooltip
|
||||
const renderedPos = edge.renderedMidpoint();
|
||||
setLinkTooltipPos({ x: renderedPos.x, y: renderedPos.y });
|
||||
}
|
||||
});
|
||||
|
||||
cy.on("mouseout", "edge", (evt: any) => {
|
||||
const edge = evt.target;
|
||||
|
||||
// Only clear hover state if we were actually hovering a highlighted edge
|
||||
if (!isFocusModeRef.current || !edge.hasClass("connection")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHoveredLink(null);
|
||||
setLinkTooltipPos(null);
|
||||
});
|
||||
|
||||
// Prevent edge selection to avoid gray border on click
|
||||
cy.on("select", "edge", (evt: any) => {
|
||||
evt.target.unselect();
|
||||
});
|
||||
|
||||
// Double-click to focus on node and its connections
|
||||
cy.on("dblclick", "node", (evt: any) => {
|
||||
const focusedNode = evt.target as cytoscape.NodeSingular;
|
||||
const focusedNodeId = focusedNode.id();
|
||||
|
||||
console.log("Double-clicked node:", focusedNodeId);
|
||||
|
||||
// Enter focus mode
|
||||
setIsFocusMode(true);
|
||||
|
||||
// Clear any existing focus classes
|
||||
cy.elements().removeClass("dimmed focused connected connection");
|
||||
|
||||
// Get all connected nodes and edges
|
||||
const connectedElements = focusedNode.neighborhood();
|
||||
const connectedNodes = connectedElements.nodes();
|
||||
const connectedEdges = connectedElements.edges();
|
||||
|
||||
// Apply styling classes
|
||||
cy.elements().addClass("dimmed"); // Dim everything first
|
||||
focusedNode.removeClass("dimmed").addClass("focused"); // Highlight the focused node
|
||||
connectedNodes.removeClass("dimmed").addClass("connected"); // Highlight connected nodes
|
||||
connectedEdges.removeClass("dimmed").addClass("connection"); // Highlight connecting edges
|
||||
|
||||
// Create a collection of all relevant elements for positioning
|
||||
const relevantElements = focusedNode.union(connectedElements);
|
||||
|
||||
// Reorient the graph to focus on this subgraph
|
||||
cy.animate(
|
||||
{
|
||||
fit: {
|
||||
eles: relevantElements,
|
||||
padding: 100,
|
||||
},
|
||||
center: {
|
||||
eles: focusedNode,
|
||||
},
|
||||
},
|
||||
{
|
||||
duration: 800,
|
||||
easing: "ease-out-cubic",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Click on background to reset focus
|
||||
cy.on("tap", (evt: any) => {
|
||||
if (evt.target === cy) {
|
||||
console.log("Clicked background - resetting focus");
|
||||
|
||||
// Exit focus mode
|
||||
setIsFocusMode(false);
|
||||
|
||||
// Remove all focus classes
|
||||
cy.elements().removeClass("dimmed focused connected connection");
|
||||
|
||||
// Zoom out to show all elements
|
||||
cy.animate(
|
||||
{
|
||||
fit: {
|
||||
eles: cy.elements(),
|
||||
padding: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
duration: 600,
|
||||
easing: "ease-out",
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
isInitializingRef.current = false;
|
||||
lastDataSignatureRef.current = dataSignature;
|
||||
} catch (error) {
|
||||
console.error("Error initializing cytoscape:", error);
|
||||
setIsLoading(false);
|
||||
isInitializingRef.current = false;
|
||||
}
|
||||
}, 100); // 100ms delay
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
clearTimeout(timeout);
|
||||
isInitializingRef.current = false;
|
||||
if (cyRef.current) {
|
||||
cyRef.current.destroy();
|
||||
cyRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [dataSignature, isMounted, containerDiv]);
|
||||
|
||||
// Handle resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (cyRef.current) {
|
||||
cyRef.current.resize();
|
||||
cyRef.current.fit(undefined, 80);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full rounded-lg overflow-hidden border border-border"
|
||||
style={{ height }}
|
||||
>
|
||||
{/* Loading state */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background z-10">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
|
||||
<p className="text-sm text-muted-foreground">{t("loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cytoscape container */}
|
||||
{isMounted && (
|
||||
<div
|
||||
ref={setContainerDiv}
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
backgroundImage: isDarkMode
|
||||
? "radial-gradient(circle at 1px 1px, rgba(255,255,255,0.08) 1px, transparent 0)"
|
||||
: "radial-gradient(circle at 1px 1px, rgba(0,0,0,0.06) 1px, transparent 0)",
|
||||
backgroundSize: "20px 20px",
|
||||
backgroundColor: isDarkMode ? "#0f1419" : "#f8fafc",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{!isLoading && graphData.nodes.length === 0 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">{t("emptyState")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link hover tooltip */}
|
||||
{hoveredLink && linkTooltipPos && (
|
||||
<div
|
||||
className="absolute z-30 pointer-events-none"
|
||||
style={{
|
||||
left: linkTooltipPos.x,
|
||||
top: linkTooltipPos.y,
|
||||
transform: "translate(-50%, -100%) translateY(-8px)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`px-3 py-2 rounded-lg shadow-lg text-sm ${
|
||||
isDarkMode
|
||||
? "bg-gray-800 text-white"
|
||||
: "bg-white text-gray-900 border border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium capitalize mb-1">
|
||||
{(() => {
|
||||
const type = hoveredLink.type || "semantic";
|
||||
if (["causes", "caused_by", "enables", "prevents"].includes(type)) {
|
||||
return t("linkTypeCausal", { type: type.replace("_", " ") });
|
||||
}
|
||||
return t("linkTypeGeneric", { type });
|
||||
})()}
|
||||
</div>
|
||||
{hoveredLink.entity && (
|
||||
<div className="text-xs opacity-80">
|
||||
{t("linkTooltipEntity")} <span className="font-medium">{hoveredLink.entity}</span>
|
||||
</div>
|
||||
)}
|
||||
{hoveredLink.weight !== undefined && (
|
||||
<div className="text-xs opacity-80">
|
||||
{t("linkTooltipWeight")}{" "}
|
||||
<span className="font-medium">{hoveredLink.weight.toFixed(3)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controls hint */}
|
||||
<div className="absolute bottom-4 right-4 text-xs text-muted-foreground/60 z-20">
|
||||
{t("controlsHint")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
export function convertHindsightGraphData(hindsightData: {
|
||||
nodes?: Array<{ data: { id: string; label?: string; color?: string } }>;
|
||||
edges?: Array<{
|
||||
data: {
|
||||
source: string;
|
||||
target: string;
|
||||
color?: string;
|
||||
lineStyle?: string;
|
||||
linkType?: string;
|
||||
entityName?: string;
|
||||
weight?: number;
|
||||
similarity?: number;
|
||||
};
|
||||
}>;
|
||||
table_rows?: Array<{ id: string; text: string; entities?: string; context?: string }>;
|
||||
}): GraphData {
|
||||
const nodes: GraphNode[] = (hindsightData.nodes || []).map((n) => {
|
||||
const tableRow = hindsightData.table_rows?.find((r) => r.id === n.data.id);
|
||||
// Use memory text as label, truncated to ~40 chars
|
||||
let label = n.data.label;
|
||||
if (!label && tableRow?.text) {
|
||||
label = tableRow.text.length > 40 ? tableRow.text.substring(0, 40) + "..." : tableRow.text;
|
||||
}
|
||||
if (!label) {
|
||||
label = n.data.id.substring(0, 8);
|
||||
}
|
||||
return {
|
||||
id: n.data.id,
|
||||
label,
|
||||
color: n.data.color,
|
||||
metadata: tableRow,
|
||||
};
|
||||
});
|
||||
|
||||
const links: GraphLink[] = (hindsightData.edges || []).map((e) => ({
|
||||
source: e.data.source,
|
||||
target: e.data.target,
|
||||
color: e.data.color,
|
||||
// Use linkType directly from API, fallback to lineStyle check, default to semantic
|
||||
type: e.data.linkType || (e.data.lineStyle === "dashed" ? "temporal" : "semantic"),
|
||||
entity: e.data.entityName, // API returns entityName
|
||||
weight: e.data.weight ?? e.data.similarity,
|
||||
}));
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
// Shared graph data model + conversion used by the memory visualizations
|
||||
// (Constellation, entities view). The Cytoscape-based "Graph" view that used to
|
||||
// live here was removed; only the framework-agnostic types and the API-response
|
||||
// converter remain, since the constellation and entity views build on them.
|
||||
|
||||
// ============================================================================
|
||||
// Types & Interfaces
|
||||
// ============================================================================
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
group?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphLink {
|
||||
source: string;
|
||||
target: string;
|
||||
color?: string;
|
||||
width?: number;
|
||||
type?: string;
|
||||
entity?: string;
|
||||
weight?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[];
|
||||
links: GraphLink[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
export function convertHindsightGraphData(hindsightData: {
|
||||
nodes?: Array<{ data: { id: string; label?: string; color?: string } }>;
|
||||
edges?: Array<{
|
||||
data: {
|
||||
source: string;
|
||||
target: string;
|
||||
color?: string;
|
||||
lineStyle?: string;
|
||||
linkType?: string;
|
||||
entityName?: string;
|
||||
weight?: number;
|
||||
similarity?: number;
|
||||
};
|
||||
}>;
|
||||
table_rows?: Array<{ id: string; text: string; entities?: string; context?: string }>;
|
||||
}): GraphData {
|
||||
const nodes: GraphNode[] = (hindsightData.nodes || []).map((n) => {
|
||||
const tableRow = hindsightData.table_rows?.find((r) => r.id === n.data.id);
|
||||
// Use memory text as label, truncated to ~40 chars
|
||||
let label = n.data.label;
|
||||
if (!label && tableRow?.text) {
|
||||
label = tableRow.text.length > 40 ? tableRow.text.substring(0, 40) + "..." : tableRow.text;
|
||||
}
|
||||
if (!label) {
|
||||
label = n.data.id.substring(0, 8);
|
||||
}
|
||||
return {
|
||||
id: n.data.id,
|
||||
label,
|
||||
color: n.data.color,
|
||||
metadata: tableRow,
|
||||
};
|
||||
});
|
||||
|
||||
const links: GraphLink[] = (hindsightData.edges || []).map((e) => ({
|
||||
source: e.data.source,
|
||||
target: e.data.target,
|
||||
color: e.data.color,
|
||||
// Use linkType directly from API, fallback to lineStyle check, default to semantic
|
||||
type: e.data.linkType || (e.data.lineStyle === "dashed" ? "temporal" : "semantic"),
|
||||
entity: e.data.entityName, // API returns entityName
|
||||
weight: e.data.weight ?? e.data.similarity,
|
||||
}));
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
@@ -0,0 +1,684 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { client, type KnowledgeNode } from "@/lib/api";
|
||||
import { useBank } from "@/lib/bank-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Download,
|
||||
FilePlus,
|
||||
FileText,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderPlus,
|
||||
Loader2,
|
||||
Network,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { formatAbsoluteDateTime, formatRelativeTime } from "@/lib/relative-time";
|
||||
import { CompactMarkdown } from "./compact-markdown";
|
||||
import { Constellation } from "./constellation";
|
||||
import type { GraphData, GraphLink, GraphNode } from "./graph-2d";
|
||||
|
||||
type ViewMode = "tree" | "graph";
|
||||
type GraphResponse = Awaited<ReturnType<typeof client.getKnowledgeBaseGraph>>;
|
||||
type PageDetail = Awaited<ReturnType<typeof client.getKnowledgePage>>;
|
||||
|
||||
const FALLBACK_COLOR = "#0074d9";
|
||||
|
||||
function flatten(nodes: KnowledgeNode[], out: KnowledgeNode[] = []): KnowledgeNode[] {
|
||||
for (const n of nodes) {
|
||||
out.push(n);
|
||||
if (n.children?.length) flatten(n.children, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function KnowledgeBaseView() {
|
||||
const t = useTranslations("knowledgeBase");
|
||||
const { currentBank } = useBank();
|
||||
|
||||
const [roots, setRoots] = useState<KnowledgeNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [view, setView] = useState<ViewMode>("tree");
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
|
||||
const [selected, setSelected] = useState<PageDetail | null>(null);
|
||||
const [loadingDetail, setLoadingDetail] = useState(false);
|
||||
|
||||
const [graph, setGraph] = useState<GraphResponse | null>(null);
|
||||
const [graphLoading, setGraphLoading] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const [createKind, setCreateKind] = useState<"folder" | "page" | null>(null);
|
||||
const [form, setForm] = useState({ name: "", sourceQuery: "", parentId: "" });
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = useState<KnowledgeNode | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const loadTree = useCallback(async () => {
|
||||
if (!currentBank) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.getKnowledgeTree(currentBank);
|
||||
setRoots(result.roots || []);
|
||||
} catch {
|
||||
// toast handled by interceptor
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentBank]);
|
||||
|
||||
const loadGraph = useCallback(async () => {
|
||||
if (!currentBank) return;
|
||||
setGraphLoading(true);
|
||||
try {
|
||||
setGraph(await client.getKnowledgeBaseGraph(currentBank));
|
||||
} catch {
|
||||
// toast handled by interceptor
|
||||
} finally {
|
||||
setGraphLoading(false);
|
||||
}
|
||||
}, [currentBank]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentBank) {
|
||||
setSelected(null);
|
||||
setGraph(null);
|
||||
loadTree();
|
||||
}
|
||||
}, [currentBank, loadTree]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view === "graph" && currentBank && !graph && !graphLoading) loadGraph();
|
||||
}, [view, currentBank, graph, graphLoading, loadGraph]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setSelected(null);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
const allNodes = useMemo(() => flatten(roots), [roots]);
|
||||
const folders = useMemo(() => allNodes.filter((n) => n.kind === "folder"), [allNodes]);
|
||||
const folderCount = folders.length;
|
||||
const pageCount = allNodes.length - folderCount;
|
||||
|
||||
const openPage = useCallback(
|
||||
async (pageId: string) => {
|
||||
if (!currentBank) return;
|
||||
setLoadingDetail(true);
|
||||
try {
|
||||
setSelected(await client.getKnowledgePage(currentBank, pageId));
|
||||
} catch {
|
||||
// toast handled by interceptor
|
||||
} finally {
|
||||
setLoadingDetail(false);
|
||||
}
|
||||
},
|
||||
[currentBank]
|
||||
);
|
||||
|
||||
const toggleFolder = useCallback((id: string) => {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openCreate = (kind: "folder" | "page", parentId = "") => {
|
||||
setForm({ name: "", sourceQuery: "", parentId });
|
||||
setCreateKind(kind);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!currentBank || !createKind || !form.name.trim()) return;
|
||||
if (createKind === "page" && !form.sourceQuery.trim()) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const parent_id = form.parentId || null;
|
||||
if (createKind === "folder") {
|
||||
await client.createKnowledgeFolder(currentBank, {
|
||||
name: form.name.trim(),
|
||||
parent_id,
|
||||
});
|
||||
} else {
|
||||
await client.createKnowledgePage(currentBank, {
|
||||
name: form.name.trim(),
|
||||
source_query: form.sourceQuery.trim(),
|
||||
parent_id,
|
||||
});
|
||||
}
|
||||
if (parent_id) setExpanded((prev) => new Set(prev).add(parent_id));
|
||||
setCreateKind(null);
|
||||
await loadTree();
|
||||
setGraph(null);
|
||||
} catch {
|
||||
// toast handled by interceptor
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!currentBank || !deleteTarget) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await client.deleteKnowledgeNode(currentBank, deleteTarget.id);
|
||||
if (selected?.id === deleteTarget.id) setSelected(null);
|
||||
setDeleteTarget(null);
|
||||
await loadTree();
|
||||
setGraph(null);
|
||||
} catch {
|
||||
// toast handled by interceptor
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!currentBank) return;
|
||||
setExporting(true);
|
||||
try {
|
||||
const bundle = await client.exportKnowledgeBase(currentBank);
|
||||
const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${currentBank}-okf.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
// toast handled by interceptor
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Constellation (graph view) — clustered by parent folder ───────────────
|
||||
const typeColors = useMemo(() => {
|
||||
const colors = new Map<string, string>();
|
||||
for (const n of graph?.nodes ?? []) colors.set(n.data.type, n.data.color);
|
||||
return colors;
|
||||
}, [graph]);
|
||||
|
||||
const constellationData = useMemo<GraphData>(() => {
|
||||
if (!graph) return { nodes: [], links: [] };
|
||||
const nodes: GraphNode[] = graph.nodes.map((n) => ({
|
||||
id: n.data.id,
|
||||
label: n.data.label,
|
||||
color: n.data.color,
|
||||
group: n.data.type,
|
||||
}));
|
||||
const links: GraphLink[] = graph.edges.map((e) => ({
|
||||
source: e.data.source,
|
||||
target: e.data.target,
|
||||
color: e.data.color,
|
||||
weight: e.data.weight,
|
||||
}));
|
||||
return { nodes, links };
|
||||
}, [graph]);
|
||||
|
||||
const nodeWeights = useMemo(() => {
|
||||
const weights = new Map<string, number>();
|
||||
for (const link of constellationData.links) {
|
||||
const w = typeof link.weight === "number" && link.weight > 0 ? link.weight : 1;
|
||||
weights.set(link.source, (weights.get(link.source) || 0) + w);
|
||||
weights.set(link.target, (weights.get(link.target) || 0) + w);
|
||||
}
|
||||
return weights;
|
||||
}, [constellationData]);
|
||||
|
||||
const maxNodeWeight = useMemo(() => {
|
||||
let max = 1;
|
||||
for (const w of nodeWeights.values()) if (w > max) max = w;
|
||||
return max;
|
||||
}, [nodeWeights]);
|
||||
|
||||
const nodeSizeFn = useCallback(
|
||||
(node: GraphNode) => 4 + Math.sqrt((nodeWeights.get(node.id) || 0) / maxNodeWeight) * 10,
|
||||
[nodeWeights, maxNodeWeight]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t("title")}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t("description")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => openCreate("folder")}>
|
||||
<FolderPlus className="w-4 h-4 mr-2" />
|
||||
{t("newFolder")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => openCreate("page")}>
|
||||
<FilePlus className="w-4 h-4 mr-2" />
|
||||
{t("newPage")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExport}
|
||||
disabled={exporting || !pageCount}
|
||||
>
|
||||
{exporting ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{t("exportButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{view === "graph"
|
||||
? t("graphCount", { pages: graph?.total_pages ?? 0, links: graph?.total_edges ?? 0 })
|
||||
: t("count", { folders: folderCount, pages: pageCount })}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setView("tree")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
view === "tree"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Folder className="w-4 h-4" />
|
||||
{t("viewTree")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView("graph")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
view === "graph"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Network className="w-4 h-4" />
|
||||
{t("viewGraph")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === "tree" ? (
|
||||
<div className="border border-border rounded-lg overflow-hidden min-h-[480px]">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<Loader2 className="w-7 h-7 text-muted-foreground animate-spin" />
|
||||
</div>
|
||||
) : roots.length > 0 ? (
|
||||
<ul className="py-2">
|
||||
{roots.map((node) => (
|
||||
<TreeRow
|
||||
key={node.id}
|
||||
node={node}
|
||||
depth={0}
|
||||
expanded={expanded}
|
||||
selectedId={selected?.id ?? null}
|
||||
onToggle={toggleFolder}
|
||||
onOpenPage={openPage}
|
||||
onAddChild={openCreate}
|
||||
onDelete={setDeleteTarget}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="text-center max-w-md px-6">
|
||||
<FolderOpen className="w-8 h-8 mx-auto mb-3 text-muted-foreground opacity-60" />
|
||||
<div className="text-sm text-muted-foreground">{t("empty")}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">{t("emptyHint")}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
{graphLoading ? (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<Loader2 className="w-7 h-7 text-muted-foreground animate-spin" />
|
||||
</div>
|
||||
) : constellationData.nodes.length > 0 ? (
|
||||
<Constellation
|
||||
data={constellationData}
|
||||
height={620}
|
||||
onNodeClick={(node) => openPage(node.id)}
|
||||
nodeSizeFn={nodeSizeFn}
|
||||
clusterKeyFn={(node) => node.group ?? null}
|
||||
clusterColorFn={(key) => typeColors.get(key) || FALLBACK_COLOR}
|
||||
clusterLabelFn={(key) => key}
|
||||
sizeLegendLabel={t("sizeLegendLabel")}
|
||||
compactLabels
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="text-sm text-muted-foreground">{t("empty")}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page detail panel */}
|
||||
{(selected || loadingDetail) && (
|
||||
<div className="fixed right-0 top-0 h-screen w-[460px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
|
||||
<div className="p-5">
|
||||
<div className="flex justify-between items-start mb-4 pb-4 border-b border-border">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-xl font-bold text-card-foreground truncate">
|
||||
{selected?.name ?? t("loadingPage")}
|
||||
</h3>
|
||||
{selected?.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1 italic">
|
||||
“{selected.description}”
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelected(null)}
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{loadingDetail && !selected ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="w-6 h-6 text-muted-foreground animate-spin" />
|
||||
</div>
|
||||
) : selected ? (
|
||||
<div className="space-y-4">
|
||||
{selected.tags.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap text-xs">
|
||||
{selected.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selected.timestamp && (
|
||||
<div
|
||||
className="text-xs text-muted-foreground"
|
||||
title={formatAbsoluteDateTime(selected.timestamp)}
|
||||
>
|
||||
{t("updatedLabel")} {formatRelativeTime(selected.timestamp)}
|
||||
</div>
|
||||
)}
|
||||
{selected.body ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none border-t border-border pt-4">
|
||||
<CompactMarkdown>{selected.body}</CompactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic border-t border-border pt-4">
|
||||
{t("noBody")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create folder/page dialog */}
|
||||
<Dialog open={createKind !== null} onOpenChange={(o) => !o && setCreateKind(null)}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{createKind === "folder" ? t("createFolderTitle") : t("createPageTitle")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t("description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">{t("fieldName")}</label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{createKind === "page" && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("fieldSourceQuery")}
|
||||
</label>
|
||||
<Textarea
|
||||
value={form.sourceQuery}
|
||||
onChange={(e) => setForm({ ...form, sourceQuery: e.target.value })}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">{t("fieldParent")}</label>
|
||||
<Select
|
||||
value={form.parentId || "__root__"}
|
||||
onValueChange={(v) => setForm({ ...form, parentId: v === "__root__" ? "" : v })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__root__">{t("rootFolder")}</SelectItem>
|
||||
{folders.map((f) => (
|
||||
<SelectItem key={f.id} value={f.id}>
|
||||
{f.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateKind(null)} disabled={creating}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
creating || !form.name.trim() || (createKind === "page" && !form.sourceQuery.trim())
|
||||
}
|
||||
>
|
||||
{creating ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : null}
|
||||
{t("create")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete confirmation */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={(o) => !o && setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("deleteButton")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("deleteConfirm", { name: deleteTarget?.name ?? "" })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="flex-row justify-end space-x-2">
|
||||
<AlertDialogCancel className="mt-0">{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{deleting ? <Loader2 className="w-4 h-4 mr-1 animate-spin" /> : null}
|
||||
{t("deleteButton")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeRow({
|
||||
node,
|
||||
depth,
|
||||
expanded,
|
||||
selectedId,
|
||||
onToggle,
|
||||
onOpenPage,
|
||||
onAddChild,
|
||||
onDelete,
|
||||
t,
|
||||
}: {
|
||||
node: KnowledgeNode;
|
||||
depth: number;
|
||||
expanded: Set<string>;
|
||||
selectedId: string | null;
|
||||
onToggle: (id: string) => void;
|
||||
onOpenPage: (id: string) => void;
|
||||
onAddChild: (kind: "folder" | "page", parentId: string) => void;
|
||||
onDelete: (node: KnowledgeNode) => void;
|
||||
t: ReturnType<typeof useTranslations>;
|
||||
}) {
|
||||
const isFolder = node.kind === "folder";
|
||||
const isOpen = expanded.has(node.id);
|
||||
const isActive = selectedId === node.id;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div
|
||||
className={`group flex items-center gap-1.5 pr-2 py-1.5 cursor-pointer border-l-2 transition-colors ${
|
||||
isActive
|
||||
? "bg-primary/10 border-primary text-foreground"
|
||||
: "border-transparent hover:bg-muted text-foreground"
|
||||
}`}
|
||||
style={{ paddingLeft: `${depth * 18 + 10}px` }}
|
||||
onClick={() => (isFolder ? onToggle(node.id) : onOpenPage(node.id))}
|
||||
>
|
||||
{isFolder ? (
|
||||
<>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="w-3.5 h-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
{isOpen ? (
|
||||
<FolderOpen className="w-4 h-4 flex-shrink-0 text-amber-500" />
|
||||
) : (
|
||||
<Folder className="w-4 h-4 flex-shrink-0 text-amber-500" />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-3.5 flex-shrink-0" />
|
||||
<FileText className="w-4 h-4 flex-shrink-0 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm truncate">{node.name}</span>
|
||||
{!isFolder && node.managed && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-violet-500/10 text-violet-600 dark:text-violet-400 flex-shrink-0">
|
||||
{t("autoBadge")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
||||
{isFolder && (
|
||||
<>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-background text-muted-foreground hover:text-foreground"
|
||||
title={t("newFolder")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddChild("folder", node.id);
|
||||
}}
|
||||
>
|
||||
<FolderPlus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-background text-muted-foreground hover:text-foreground"
|
||||
title={t("newPage")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddChild("page", node.id);
|
||||
}}
|
||||
>
|
||||
<FilePlus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="p-1 rounded hover:bg-background text-muted-foreground hover:text-red-600"
|
||||
title={t("deleteButton")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(node);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isFolder && isOpen && node.children?.length > 0 && (
|
||||
<ul>
|
||||
{node.children.map((child) => (
|
||||
<TreeRow
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
expanded={expanded}
|
||||
selectedId={selectedId}
|
||||
onToggle={onToggle}
|
||||
onOpenPage={onOpenPage}
|
||||
onAddChild={onAddChild}
|
||||
onDelete={onDelete}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -154,23 +154,12 @@ export function MentalModelsView() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// The API caps each response at PAGE_SIZE, so page through until a short
|
||||
// page is returned to load every mental model for this bank.
|
||||
const PAGE_SIZE = 100;
|
||||
const all: MentalModel[] = [];
|
||||
for (let offset = 0; ; offset += PAGE_SIZE) {
|
||||
const page = await client.listMentalModels(
|
||||
currentBank,
|
||||
selectedTags.length > 0 ? selectedTags : undefined,
|
||||
selectedTags.length > 0 ? tagsMatch : undefined,
|
||||
PAGE_SIZE,
|
||||
offset
|
||||
);
|
||||
const items = page.items || [];
|
||||
all.push(...items);
|
||||
if (items.length < PAGE_SIZE) break;
|
||||
}
|
||||
setMentalModels(all);
|
||||
const mentalModelsData = await client.listMentalModels(
|
||||
currentBank,
|
||||
selectedTags.length > 0 ? selectedTags : undefined,
|
||||
selectedTags.length > 0 ? tagsMatch : undefined
|
||||
);
|
||||
setMentalModels(mentalModelsData.items || []);
|
||||
} catch (error) {
|
||||
console.error("Error loading mental models:", error);
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Database,
|
||||
FileText,
|
||||
Users,
|
||||
Network,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Settings,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
|
||||
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "knowledge" | "profile";
|
||||
|
||||
interface SidebarProps {
|
||||
currentTab: NavItem;
|
||||
@@ -35,6 +36,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: "knowledge" as NavItem, label: t("knowledge"), icon: Network },
|
||||
{ id: "data" as NavItem, label: t("memories"), icon: Database },
|
||||
{ id: "recall" as NavItem, label: t("recall"), icon: Search },
|
||||
{ id: "reflect" as NavItem, label: t("reflect"), icon: Sparkles },
|
||||
|
||||
@@ -14,19 +14,7 @@ interface JsonViewerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function parseJsonString(value: string): unknown {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function toDisplayText(value: unknown): string {
|
||||
value = typeof value === "string" ? parseJsonString(value) : value;
|
||||
if (typeof value === "string") return value;
|
||||
// Unescape newlines inside string values so multi-line content (e.g. prompts)
|
||||
// renders as real line breaks under `whitespace-pre-wrap` instead of literal "\n".
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex w-full touch-none select-none items-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary/50 border border-border">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
));
|
||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||
|
||||
export { Slider };
|
||||
@@ -38,6 +38,19 @@ export interface WebhookHttpConfig {
|
||||
params: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface KnowledgeNode {
|
||||
id: string;
|
||||
kind: "folder" | "page";
|
||||
name: string;
|
||||
parent_id: string | null;
|
||||
mental_model_id: string | null;
|
||||
managed: boolean;
|
||||
description: string | null;
|
||||
tags: string[];
|
||||
timestamp: string | null;
|
||||
children: KnowledgeNode[];
|
||||
}
|
||||
|
||||
export interface Webhook {
|
||||
id: string;
|
||||
bank_id: string | null;
|
||||
@@ -593,6 +606,115 @@ export class ControlPlaneClient {
|
||||
}>(`/api/entities/graph?${queryParams}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the knowledge base as a nested folder/page tree.
|
||||
*/
|
||||
async getKnowledgeTree(bankId: string) {
|
||||
return this.fetchApi<{ roots: KnowledgeNode[] }>(
|
||||
`/api/knowledge-base/tree?bank_id=${encodeURIComponent(bankId)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the knowledge-base constellation graph (pages linked by shared tags).
|
||||
*/
|
||||
async getKnowledgeBaseGraph(bankId: string) {
|
||||
return this.fetchApi<{
|
||||
nodes: Array<{
|
||||
data: { id: string; label: string; type: string; tagCount: number; color: string };
|
||||
}>;
|
||||
edges: Array<{
|
||||
data: {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
sharedTags: string[];
|
||||
weight: number;
|
||||
color: string;
|
||||
};
|
||||
}>;
|
||||
total_pages: number;
|
||||
total_edges: number;
|
||||
}>(`/api/knowledge-base/graph?bank_id=${encodeURIComponent(bankId)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single knowledge page rendered as an OKF document.
|
||||
*/
|
||||
async getKnowledgePage(bankId: string, pageId: string) {
|
||||
return this.fetchApi<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description: string | null;
|
||||
tags: string[];
|
||||
timestamp: string | null;
|
||||
body: string | null;
|
||||
markdown: string;
|
||||
}>(
|
||||
`/api/knowledge-base/pages/${encodeURIComponent(pageId)}?bank_id=${encodeURIComponent(bankId)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a folder, optionally under a parent folder.
|
||||
*/
|
||||
async createKnowledgeFolder(
|
||||
bankId: string,
|
||||
body: { name: string; parent_id?: string | null }
|
||||
) {
|
||||
return this.fetchApi<KnowledgeNode>(
|
||||
`/api/knowledge-base/folders?bank_id=${encodeURIComponent(bankId)}`,
|
||||
{ method: "POST", body: JSON.stringify(body) }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a page (mental model + tree node). Content is generated asynchronously.
|
||||
*/
|
||||
async createKnowledgePage(
|
||||
bankId: string,
|
||||
body: { name: string; source_query: string; parent_id?: string | null; tags?: string[] }
|
||||
) {
|
||||
return this.fetchApi<{ page_id: string; mental_model_id: string; operation_id: string | null }>(
|
||||
`/api/knowledge-base/pages?bank_id=${encodeURIComponent(bankId)}`,
|
||||
{ method: "POST", body: JSON.stringify(body) }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename and/or move a node. Pass `parent_id: null` to move to the root.
|
||||
*/
|
||||
async updateKnowledgeNode(
|
||||
bankId: string,
|
||||
nodeId: string,
|
||||
body: { name?: string; parent_id?: string | null }
|
||||
) {
|
||||
return this.fetchApi<KnowledgeNode>(
|
||||
`/api/knowledge-base/nodes/${encodeURIComponent(nodeId)}?bank_id=${encodeURIComponent(bankId)}`,
|
||||
{ method: "PATCH", body: JSON.stringify(body) }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a node and its whole subtree.
|
||||
*/
|
||||
async deleteKnowledgeNode(bankId: string, nodeId: string) {
|
||||
return this.fetchApi<{ status: string }>(
|
||||
`/api/knowledge-base/nodes/${encodeURIComponent(nodeId)}?bank_id=${encodeURIComponent(bankId)}`,
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the knowledge base as a portable OKF bundle (markdown files).
|
||||
*/
|
||||
async exportKnowledgeBase(bankId: string) {
|
||||
return this.fetchApi<{ files: Array<{ path: string; content: string }> }>(
|
||||
`/api/knowledge-base/export?bank_id=${encodeURIComponent(bankId)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity details
|
||||
*/
|
||||
@@ -1205,13 +1327,7 @@ export class ControlPlaneClient {
|
||||
/**
|
||||
* List mental models for a bank
|
||||
*/
|
||||
async listMentalModels(
|
||||
bankId: string,
|
||||
tags?: string[],
|
||||
tagsMatch?: string,
|
||||
limit?: number,
|
||||
offset?: number
|
||||
) {
|
||||
async listMentalModels(bankId: string, tags?: string[], tagsMatch?: string) {
|
||||
const params = new URLSearchParams();
|
||||
if (tags && tags.length > 0) {
|
||||
tags.forEach((t) => params.append("tags", t));
|
||||
@@ -1219,12 +1335,6 @@ export class ControlPlaneClient {
|
||||
if (tagsMatch) {
|
||||
params.append("tags_match", tagsMatch);
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
params.append("limit", String(limit));
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
params.append("offset", String(offset));
|
||||
}
|
||||
const query = params.toString();
|
||||
return this.fetchApi<{
|
||||
items: Array<{
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "Speicherbank",
|
||||
"expandSidebar": "Seitenleiste ausklappen",
|
||||
"collapseSidebar": "Seitenleiste einklappen",
|
||||
"collapse": "Einklappen"
|
||||
"collapse": "Einklappen",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "Übersichtsstatistik und Hintergrundoperationen für diesen Speicherbank.",
|
||||
"operations": "Operationen",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "Alle Erinnerungen konsolidiert (zuletzt: {date})",
|
||||
"pendingConsolidation": "{count} Erinnerungen stehen zur Konsolidierung aus",
|
||||
"constellation": "Konstellation",
|
||||
"graph": "Graph",
|
||||
"table": "Tabelle",
|
||||
"timeline": "Zeitleiste",
|
||||
"hidePanel": "Bereich ausblenden",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "Verknüpfungstypen",
|
||||
"nodes": "Knoten",
|
||||
"links": "Verknüpfungen",
|
||||
"graphTitle": "Graph",
|
||||
"linksWithCount": "Verknüpfungen ({count})",
|
||||
"clickToFilter": "· klicken zum Filtern",
|
||||
"semantic": "Semantisch",
|
||||
"temporal": "Zeitlich",
|
||||
"entity": "Entität",
|
||||
"causal": "Kausal",
|
||||
"displayTitle": "Anzeige",
|
||||
"showLabels": "Beschriftungen anzeigen",
|
||||
"performanceTitle": "Leistung",
|
||||
"maxNodes": "Maximale Knoten",
|
||||
"allLinksVisible": "Alle Verknüpfungen zwischen sichtbaren Knoten werden angezeigt.",
|
||||
"limitedTo50Nodes": "⚠️ Aus Leistungsgründen auf 50 Knoten begrenzt. Gesamt: {count}",
|
||||
"clickNodeForDetails": "Auf einen Knoten klicken, um Details anzuzeigen",
|
||||
"columnObservation": "Beobachtung",
|
||||
"columnMemory": "Erinnerung",
|
||||
"columnSources": "Quellen",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "Beobachtung",
|
||||
"actionClearContent": "Inhalt löschen"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "Diagramm wird geladen...",
|
||||
"emptyState": "Keine Erinnerungen zum Anzeigen",
|
||||
"linkTypeCausal": "Kausal ({type})",
|
||||
"linkTypeGeneric": "{type}-Verknüpfung",
|
||||
"linkTooltipEntity": "Entität:",
|
||||
"linkTooltipWeight": "Gewicht:",
|
||||
"controlsHint": "Ziehen zum Verschieben • Scrollen zum Zoomen • Doppelklick auf Knoten zum Fokussieren • Klick auf den Hintergrund zum Zurücksetzen"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "Scrollen zum Zoomen · Ziehen zum Verschieben · Hover zum Erkunden · Klicken zum Auswählen",
|
||||
"hudStats": "{memories} Erinnerungen · {visible} sichtbar · {labels} Beschriftungen · {links} Verknüpfungen · Zoom {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "Webhook konnte nicht aktualisiert werden",
|
||||
"delete": "Webhook konnte nicht gelöscht werden",
|
||||
"deliveries": "Webhook-Zustellungen konnten nicht abgerufen werden"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,8 @@
|
||||
"memoryBank": "Memory Bank",
|
||||
"expandSidebar": "Expand sidebar",
|
||||
"collapseSidebar": "Collapse sidebar",
|
||||
"collapse": "Collapse"
|
||||
"collapse": "Collapse",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "Overview statistics and background operations for this memory bank.",
|
||||
"operations": "Operations",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "All memories consolidated (last: {date})",
|
||||
"pendingConsolidation": "{count} memories pending consolidation",
|
||||
"constellation": "Constellation",
|
||||
"graph": "Graph",
|
||||
"table": "Table",
|
||||
"timeline": "Timeline",
|
||||
"hidePanel": "Hide panel",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "Link types",
|
||||
"nodes": "Nodes",
|
||||
"links": "Links",
|
||||
"graphTitle": "Graph",
|
||||
"linksWithCount": "Links ({count})",
|
||||
"clickToFilter": "· click to filter",
|
||||
"semantic": "Semantic",
|
||||
"temporal": "Temporal",
|
||||
"entity": "Entity",
|
||||
"causal": "Causal",
|
||||
"displayTitle": "Display",
|
||||
"showLabels": "Show labels",
|
||||
"performanceTitle": "Performance",
|
||||
"maxNodes": "Max nodes",
|
||||
"allLinksVisible": "All links between visible nodes are shown.",
|
||||
"limitedTo50Nodes": "⚠️ Limited to 50 nodes for performance. Total: {count}",
|
||||
"clickNodeForDetails": "Click a node to see details",
|
||||
"columnObservation": "Observation",
|
||||
"columnMemory": "Memory",
|
||||
"columnSources": "Sources",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "observation",
|
||||
"actionClearContent": "Clear Content"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "Loading graph...",
|
||||
"emptyState": "No memories to display",
|
||||
"linkTypeCausal": "Causal ({type})",
|
||||
"linkTypeGeneric": "{type} link",
|
||||
"linkTooltipEntity": "Entity:",
|
||||
"linkTooltipWeight": "Weight:",
|
||||
"controlsHint": "Drag to pan • Scroll to zoom • Double-click node to focus • Click background to reset"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "Scroll to zoom · Drag to pan · Hover to explore · Click to select",
|
||||
"hudStats": "{memories} memories · {visible} visible · {labels} labels · {links} links · zoom {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "Failed to update webhook",
|
||||
"delete": "Failed to delete webhook",
|
||||
"deliveries": "Failed to fetch webhook deliveries"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "Banco de memoria",
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"collapseSidebar": "Contraer barra lateral",
|
||||
"collapse": "Contraer"
|
||||
"collapse": "Contraer",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "Estadísticas generales y operaciones en segundo plano de este banco de memoria.",
|
||||
"operations": "Operaciones",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "Todas las memorias consolidadas (última: {date})",
|
||||
"pendingConsolidation": "{count} memorias pendientes de consolidación",
|
||||
"constellation": "Constelación",
|
||||
"graph": "Grafo",
|
||||
"table": "Tabla",
|
||||
"timeline": "Línea de tiempo",
|
||||
"hidePanel": "Ocultar panel",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "Tipos de vínculo",
|
||||
"nodes": "Nodos",
|
||||
"links": "Vínculos",
|
||||
"graphTitle": "Grafo",
|
||||
"linksWithCount": "Vínculos ({count})",
|
||||
"clickToFilter": "· clic para filtrar",
|
||||
"semantic": "Semántico",
|
||||
"temporal": "Temporal",
|
||||
"entity": "Entidad",
|
||||
"causal": "Causal",
|
||||
"displayTitle": "Visualización",
|
||||
"showLabels": "Mostrar etiquetas",
|
||||
"performanceTitle": "Rendimiento",
|
||||
"maxNodes": "Nodos máximos",
|
||||
"allLinksVisible": "Se muestran todos los vínculos entre nodos visibles.",
|
||||
"limitedTo50Nodes": "⚠️ Limitado a 50 nodos por rendimiento. Total: {count}",
|
||||
"clickNodeForDetails": "Haz clic en un nodo para ver detalles",
|
||||
"columnObservation": "Observación",
|
||||
"columnMemory": "Memoria",
|
||||
"columnSources": "Fuentes",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "observación",
|
||||
"actionClearContent": "Borrar contenido"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "Cargando gráfico...",
|
||||
"emptyState": "No hay memorias que mostrar",
|
||||
"linkTypeCausal": "Causal ({type})",
|
||||
"linkTypeGeneric": "Enlace {type}",
|
||||
"linkTooltipEntity": "Entidad:",
|
||||
"linkTooltipWeight": "Peso:",
|
||||
"controlsHint": "Arrastra para mover • Desplaza para hacer zoom • Doble clic en un nodo para enfocar • Clic en el fondo para restablecer"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "Desplaza para hacer zoom · Arrastra para mover · Pasa el cursor para explorar · Haz clic para seleccionar",
|
||||
"hudStats": "{memories} memorias · {visible} visibles · {labels} etiquetas · {links} enlaces · zoom {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "No se pudo actualizar el webhook",
|
||||
"delete": "No se pudo eliminar el webhook",
|
||||
"deliveries": "No se pudieron obtener las entregas del webhook"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "Banque de mémoire",
|
||||
"expandSidebar": "Développer la barre latérale",
|
||||
"collapseSidebar": "Réduire la barre latérale",
|
||||
"collapse": "Réduire"
|
||||
"collapse": "Réduire",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "Statistiques générales et opérations en arrière-plan pour cette banque mémoire.",
|
||||
"operations": "Opérations",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "Tous les souvenirs consolidés (dernier : {date})",
|
||||
"pendingConsolidation": "{count} souvenirs en attente de consolidation",
|
||||
"constellation": "Constellation",
|
||||
"graph": "Graphe",
|
||||
"table": "Tableau",
|
||||
"timeline": "Chronologie",
|
||||
"hidePanel": "Masquer le panneau",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "Types de liens",
|
||||
"nodes": "Nœuds",
|
||||
"links": "Liens",
|
||||
"graphTitle": "Graphe",
|
||||
"linksWithCount": "Liens ({count})",
|
||||
"clickToFilter": "· cliquer pour filtrer",
|
||||
"semantic": "Sémantique",
|
||||
"temporal": "Temporel",
|
||||
"entity": "Entité",
|
||||
"causal": "Causal",
|
||||
"displayTitle": "Affichage",
|
||||
"showLabels": "Afficher les étiquettes",
|
||||
"performanceTitle": "Performance",
|
||||
"maxNodes": "Nœuds max",
|
||||
"allLinksVisible": "Tous les liens entre les nœuds visibles sont affichés.",
|
||||
"limitedTo50Nodes": "⚠️ Limité à 50 nœuds pour les performances. Total : {count}",
|
||||
"clickNodeForDetails": "Cliquez sur un nœud pour voir les détails",
|
||||
"columnObservation": "Observation",
|
||||
"columnMemory": "Souvenir",
|
||||
"columnSources": "Sources",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "observation",
|
||||
"actionClearContent": "Effacer le contenu"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "Chargement du graphe...",
|
||||
"emptyState": "Aucun souvenir à afficher",
|
||||
"linkTypeCausal": "Causal ({type})",
|
||||
"linkTypeGeneric": "Lien {type}",
|
||||
"linkTooltipEntity": "Entité :",
|
||||
"linkTooltipWeight": "Poids :",
|
||||
"controlsHint": "Glisser pour déplacer • Défiler pour zoomer • Double-clic sur un nœud pour zoomer • Clic sur le fond pour réinitialiser"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "Défiler pour zoomer · Glisser pour déplacer · Survoler pour explorer · Cliquer pour sélectionner",
|
||||
"hudStats": "{memories} souvenirs · {visible} visibles · {labels} étiquettes · {links} liens · zoom {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "Impossible de mettre à jour le webhook",
|
||||
"delete": "Impossible de supprimer le webhook",
|
||||
"deliveries": "Impossible de récupérer les livraisons du webhook"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "メモリバンク",
|
||||
"expandSidebar": "サイドバーを展開",
|
||||
"collapseSidebar": "サイドバーを折りたたむ",
|
||||
"collapse": "折りたたむ"
|
||||
"collapse": "折りたたむ",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "このメモリバンクの概要統計とバックグラウンド操作。",
|
||||
"operations": "操作",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "すべてのメモリが統合済み(最終:{date})",
|
||||
"pendingConsolidation": "{count}件のメモリが統合待ち",
|
||||
"constellation": "コンステレーション",
|
||||
"graph": "グラフ",
|
||||
"table": "テーブル",
|
||||
"timeline": "タイムライン",
|
||||
"hidePanel": "パネルを非表示",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "リンクの種類",
|
||||
"nodes": "ノード",
|
||||
"links": "リンク",
|
||||
"graphTitle": "グラフ",
|
||||
"linksWithCount": "リンク({count}件)",
|
||||
"clickToFilter": "・クリックでフィルター",
|
||||
"semantic": "セマンティック",
|
||||
"temporal": "時系列",
|
||||
"entity": "エンティティ",
|
||||
"causal": "因果",
|
||||
"displayTitle": "表示",
|
||||
"showLabels": "ラベルを表示",
|
||||
"performanceTitle": "パフォーマンス",
|
||||
"maxNodes": "最大ノード数",
|
||||
"allLinksVisible": "表示中のノード間のすべてのリンクが表示されています。",
|
||||
"limitedTo50Nodes": "⚠️ パフォーマンスのため50ノードに制限されています。合計:{count}",
|
||||
"clickNodeForDetails": "ノードをクリックして詳細を確認",
|
||||
"columnObservation": "観察",
|
||||
"columnMemory": "メモリ",
|
||||
"columnSources": "ソース",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "オブザベーション",
|
||||
"actionClearContent": "内容をクリア"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "グラフを読み込み中...",
|
||||
"emptyState": "表示するメモリがありません",
|
||||
"linkTypeCausal": "因果 ({type})",
|
||||
"linkTypeGeneric": "{type} リンク",
|
||||
"linkTooltipEntity": "エンティティ:",
|
||||
"linkTooltipWeight": "ウェイト:",
|
||||
"controlsHint": "ドラッグで移動 • スクロールでズーム • ノードをダブルクリックでフォーカス • 背景をクリックでリセット"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "スクロールでズーム · ドラッグで移動 · ホバーで探索 · クリックで選択",
|
||||
"hudStats": "{memories} 件のメモリ · {visible} 件表示 · {labels} 件のラベル · {links} 件のリンク · ズーム {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "Webhookの更新に失敗しました",
|
||||
"delete": "Webhookの削除に失敗しました",
|
||||
"deliveries": "Webhook配信の取得に失敗しました"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "메모리 뱅크",
|
||||
"expandSidebar": "사이드바 펼치기",
|
||||
"collapseSidebar": "사이드바 접기",
|
||||
"collapse": "접기"
|
||||
"collapse": "접기",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "이 메모리 뱅크의 개요 통계 및 백그라운드 작업.",
|
||||
"operations": "작업",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "모든 메모리 통합됨 (마지막: {date})",
|
||||
"pendingConsolidation": "{count}개 메모리 통합 대기 중",
|
||||
"constellation": "별자리",
|
||||
"graph": "그래프",
|
||||
"table": "표",
|
||||
"timeline": "타임라인",
|
||||
"hidePanel": "패널 숨기기",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "링크 유형",
|
||||
"nodes": "노드",
|
||||
"links": "링크",
|
||||
"graphTitle": "그래프",
|
||||
"linksWithCount": "링크 ({count})",
|
||||
"clickToFilter": "· 클릭하여 필터링",
|
||||
"semantic": "의미적",
|
||||
"temporal": "시간적",
|
||||
"entity": "엔티티",
|
||||
"causal": "인과적",
|
||||
"displayTitle": "표시",
|
||||
"showLabels": "레이블 표시",
|
||||
"performanceTitle": "성능",
|
||||
"maxNodes": "최대 노드",
|
||||
"allLinksVisible": "표시된 노드 간의 모든 링크가 표시됩니다.",
|
||||
"limitedTo50Nodes": "⚠️ 성능을 위해 50개 노드로 제한됩니다. 전체: {count}",
|
||||
"clickNodeForDetails": "노드를 클릭하면 세부 정보를 볼 수 있습니다",
|
||||
"columnObservation": "관찰",
|
||||
"columnMemory": "메모리",
|
||||
"columnSources": "소스",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "관찰",
|
||||
"actionClearContent": "콘텐츠 지우기"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "그래프 로딩 중...",
|
||||
"emptyState": "표시할 메모리가 없습니다",
|
||||
"linkTypeCausal": "인과 ({type})",
|
||||
"linkTypeGeneric": "{type} 링크",
|
||||
"linkTooltipEntity": "엔티티:",
|
||||
"linkTooltipWeight": "가중치:",
|
||||
"controlsHint": "드래그하여 이동 • 스크롤하여 확대/축소 • 노드 더블클릭으로 포커스 • 배경 클릭으로 초기화"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "스크롤하여 확대/축소 · 드래그하여 이동 · 호버하여 탐색 · 클릭하여 선택",
|
||||
"hudStats": "{memories}개 메모리 · {visible}개 표시 · {labels}개 레이블 · {links}개 링크 · 줌 {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "Webhook을 업데이트하지 못했습니다",
|
||||
"delete": "Webhook을 삭제하지 못했습니다",
|
||||
"deliveries": "Webhook 전달 내역을 가져오지 못했습니다"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "Banco de memória",
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"collapseSidebar": "Recolher barra lateral",
|
||||
"collapse": "Recolher"
|
||||
"collapse": "Recolher",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "Estatísticas gerais e operações em segundo plano para este banco de memória.",
|
||||
"operations": "Operações",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "Todas as memórias consolidadas (última: {date})",
|
||||
"pendingConsolidation": "{count} memórias pendentes de consolidação",
|
||||
"constellation": "Constelação",
|
||||
"graph": "Grafo",
|
||||
"table": "Tabela",
|
||||
"timeline": "Linha do Tempo",
|
||||
"hidePanel": "Ocultar painel",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "Tipos de vínculo",
|
||||
"nodes": "Nós",
|
||||
"links": "Vínculos",
|
||||
"graphTitle": "Grafo",
|
||||
"linksWithCount": "Vínculos ({count})",
|
||||
"clickToFilter": "· clique para filtrar",
|
||||
"semantic": "Semântico",
|
||||
"temporal": "Temporal",
|
||||
"entity": "Entidade",
|
||||
"causal": "Causal",
|
||||
"displayTitle": "Exibição",
|
||||
"showLabels": "Mostrar rótulos",
|
||||
"performanceTitle": "Desempenho",
|
||||
"maxNodes": "Máximo de nós",
|
||||
"allLinksVisible": "Todos os vínculos entre os nós visíveis estão sendo exibidos.",
|
||||
"limitedTo50Nodes": "⚠️ Limitado a 50 nós por desempenho. Total: {count}",
|
||||
"clickNodeForDetails": "Clique em um nó para ver detalhes",
|
||||
"columnObservation": "Observação",
|
||||
"columnMemory": "Memória",
|
||||
"columnSources": "Fontes",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "observação",
|
||||
"actionClearContent": "Limpar conteúdo"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "Carregando grafo...",
|
||||
"emptyState": "Nenhuma memória para exibir",
|
||||
"linkTypeCausal": "Causal ({type})",
|
||||
"linkTypeGeneric": "Link {type}",
|
||||
"linkTooltipEntity": "Entidade:",
|
||||
"linkTooltipWeight": "Peso:",
|
||||
"controlsHint": "Arraste para mover • Scroll para zoom • Duplo clique no nó para focar • Clique no fundo para redefinir"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "Scroll para zoom · Arraste para mover · Passe o mouse para explorar · Clique para selecionar",
|
||||
"hudStats": "{memories} memórias · {visible} visíveis · {labels} rótulos · {links} links · zoom {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "Falha ao atualizar webhook",
|
||||
"delete": "Falha ao excluir webhook",
|
||||
"deliveries": "Falha ao buscar entregas do webhook"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "記憶庫",
|
||||
"expandSidebar": "展開側邊欄",
|
||||
"collapseSidebar": "收起側邊欄",
|
||||
"collapse": "收起"
|
||||
"collapse": "收起",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "檢視此記憶庫的概覽統計和背景作業。",
|
||||
"operations": "操作",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "所有記憶已整合(最近:{date})",
|
||||
"pendingConsolidation": "{count} 條記憶待整合",
|
||||
"constellation": "星座圖",
|
||||
"graph": "圖譜",
|
||||
"table": "表格",
|
||||
"timeline": "時間軸",
|
||||
"hidePanel": "隱藏面板",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "連結類型",
|
||||
"nodes": "節點",
|
||||
"links": "連結",
|
||||
"graphTitle": "圖譜",
|
||||
"linksWithCount": "連結({count})",
|
||||
"clickToFilter": "· 選取篩選",
|
||||
"semantic": "語義",
|
||||
"temporal": "時間",
|
||||
"entity": "實體",
|
||||
"causal": "因果",
|
||||
"displayTitle": "顯示",
|
||||
"showLabels": "顯示標籤",
|
||||
"performanceTitle": "效能",
|
||||
"maxNodes": "最大節點數",
|
||||
"allLinksVisible": "所有可見節點之間的連結均已顯示。",
|
||||
"limitedTo50Nodes": "⚠️ 出於效能限制,最多顯示 50 個節點。總計:{count}",
|
||||
"clickNodeForDetails": "選取節點檢視詳情",
|
||||
"columnObservation": "觀察",
|
||||
"columnMemory": "記憶",
|
||||
"columnSources": "來源",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "觀察",
|
||||
"actionClearContent": "清除內容"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "載入中圖譜...",
|
||||
"emptyState": "目前沒有記憶可顯示",
|
||||
"linkTypeCausal": "因果 ({type})",
|
||||
"linkTypeGeneric": "{type} 連結",
|
||||
"linkTooltipEntity": "實體:",
|
||||
"linkTooltipWeight": "權重:",
|
||||
"controlsHint": "拖曳平移 • 滾動縮放 • 雙擊節點聚焦 • 選取背景重設"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "滾動縮放 · 拖曳平移 · 懸停探索 · 選取項目",
|
||||
"hudStats": "{memories} 條記憶 · {visible} 條可見 · {labels} 個標籤 · {links} 條連結 · 縮放 {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "未能更新 Webhook",
|
||||
"delete": "未能刪除 Webhook",
|
||||
"deliveries": "未能載入 Webhook 投遞"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "记忆库",
|
||||
"expandSidebar": "展开侧边栏",
|
||||
"collapseSidebar": "收起侧边栏",
|
||||
"collapse": "收起"
|
||||
"collapse": "收起",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "查看此记忆库的概览统计和后台操作。",
|
||||
"operations": "操作",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "所有记忆已整合(最近:{date})",
|
||||
"pendingConsolidation": "{count} 条记忆待整合",
|
||||
"constellation": "星座图",
|
||||
"graph": "图谱",
|
||||
"table": "表格",
|
||||
"timeline": "时间线",
|
||||
"hidePanel": "隐藏面板",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "链接类型",
|
||||
"nodes": "节点",
|
||||
"links": "链接",
|
||||
"graphTitle": "图谱",
|
||||
"linksWithCount": "链接({count})",
|
||||
"clickToFilter": "· 点击筛选",
|
||||
"semantic": "语义",
|
||||
"temporal": "时间",
|
||||
"entity": "实体",
|
||||
"causal": "因果",
|
||||
"displayTitle": "显示",
|
||||
"showLabels": "显示标签",
|
||||
"performanceTitle": "性能",
|
||||
"maxNodes": "最大节点数",
|
||||
"allLinksVisible": "所有可见节点之间的链接均已显示。",
|
||||
"limitedTo50Nodes": "⚠️ 出于性能限制,最多显示 50 个节点。总计:{count}",
|
||||
"clickNodeForDetails": "点击节点查看详情",
|
||||
"columnObservation": "观察",
|
||||
"columnMemory": "记忆",
|
||||
"columnSources": "来源",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "观察",
|
||||
"actionClearContent": "清除内容"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "正在加载图谱...",
|
||||
"emptyState": "暂无记忆可显示",
|
||||
"linkTypeCausal": "因果 ({type})",
|
||||
"linkTypeGeneric": "{type} 链接",
|
||||
"linkTooltipEntity": "实体:",
|
||||
"linkTooltipWeight": "权重:",
|
||||
"controlsHint": "拖拽平移 • 滚动缩放 • 双击节点聚焦 • 点击背景重置"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "滚动缩放 · 拖拽平移 · 悬停探索 · 点击选择",
|
||||
"hudStats": "{memories} 条记忆 · {visible} 条可见 · {labels} 个标签 · {links} 条链接 · 缩放 {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "更新 Webhook 失败",
|
||||
"delete": "删除 Webhook 失败",
|
||||
"deliveries": "获取 Webhook 投递失败"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,8 @@
|
||||
"memoryBank": "記憶庫",
|
||||
"expandSidebar": "展開側邊欄",
|
||||
"collapseSidebar": "收起側邊欄",
|
||||
"collapse": "收起"
|
||||
"collapse": "收起",
|
||||
"knowledge": "Knowledge base"
|
||||
},
|
||||
"overviewAndOperations": "檢視此記憶庫的概覽統計和背景作業。",
|
||||
"operations": "操作",
|
||||
@@ -534,6 +535,7 @@
|
||||
"allConsolidatedWithDate": "所有記憶已整合(最近:{date})",
|
||||
"pendingConsolidation": "{count} 條記憶待整合",
|
||||
"constellation": "星座圖",
|
||||
"graph": "圖譜",
|
||||
"table": "表格",
|
||||
"timeline": "時間軸",
|
||||
"hidePanel": "隱藏面板",
|
||||
@@ -547,6 +549,20 @@
|
||||
"linkTypes": "連結型別",
|
||||
"nodes": "節點",
|
||||
"links": "連結",
|
||||
"graphTitle": "圖譜",
|
||||
"linksWithCount": "連結({count})",
|
||||
"clickToFilter": "· 點選篩選",
|
||||
"semantic": "語義",
|
||||
"temporal": "時間",
|
||||
"entity": "實體",
|
||||
"causal": "因果",
|
||||
"displayTitle": "顯示",
|
||||
"showLabels": "顯示標籤",
|
||||
"performanceTitle": "效能",
|
||||
"maxNodes": "最大節點數",
|
||||
"allLinksVisible": "所有可見節點之間的連結均已顯示。",
|
||||
"limitedTo50Nodes": "⚠️ 出於效能限制,最多顯示 50 個節點。總計:{count}",
|
||||
"clickNodeForDetails": "點選節點檢視詳情",
|
||||
"columnObservation": "觀察",
|
||||
"columnMemory": "記憶",
|
||||
"columnSources": "來源",
|
||||
@@ -1438,6 +1454,15 @@
|
||||
"factTypeObservation": "觀察",
|
||||
"actionClearContent": "清除內容"
|
||||
},
|
||||
"graph2d": {
|
||||
"loading": "載入中圖譜...",
|
||||
"emptyState": "尚無記憶可顯示",
|
||||
"linkTypeCausal": "因果 ({type})",
|
||||
"linkTypeGeneric": "{type} 連結",
|
||||
"linkTooltipEntity": "實體:",
|
||||
"linkTooltipWeight": "權重:",
|
||||
"controlsHint": "拖曳平移 • 滾動縮放 • 雙擊節點聚焦 • 按一下背景重設"
|
||||
},
|
||||
"constellation": {
|
||||
"instructions": "滾動縮放 · 拖曳平移 · 懸停探索 · 按一下選取",
|
||||
"hudStats": "{memories} 條記憶 · {visible} 條可見 · {labels} 個標籤 · {links} 條連結 · 縮放 {zoom}x",
|
||||
@@ -1688,7 +1713,55 @@
|
||||
"update": "更新 Webhook 失敗",
|
||||
"delete": "刪除 Webhook 失敗",
|
||||
"deliveries": "取得 Webhook 投遞失敗"
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"tree": "Failed to fetch knowledge base tree",
|
||||
"graph": "Failed to fetch knowledge base graph",
|
||||
"export": "Failed to export knowledge base",
|
||||
"createFolder": "Failed to create folder",
|
||||
"createPage": "Failed to create page",
|
||||
"fetchPage": "Failed to fetch knowledge page",
|
||||
"updateNode": "Failed to update node",
|
||||
"deleteNode": "Failed to delete node"
|
||||
}
|
||||
}
|
||||
},
|
||||
"knowledgeBase": {
|
||||
"title": "Knowledge base",
|
||||
"description": "Organize your knowledge into folders and pages, linked by shared tags.",
|
||||
"loading": "Loading knowledge base…",
|
||||
"loadingPage": "Loading page…",
|
||||
"empty": "No folders or pages yet.",
|
||||
"emptyHint": "Create a folder or a page to get started.",
|
||||
"viewTree": "Tree",
|
||||
"viewGraph": "Graph",
|
||||
"newFolder": "New folder",
|
||||
"newPage": "New page",
|
||||
"exportButton": "Export OKF bundle",
|
||||
"count": "{folders} folders · {pages} pages",
|
||||
"graphCount": "{pages} pages · {links} shared-tag links",
|
||||
"sizeLegendLabel": "shared-tag links",
|
||||
"updatedLabel": "Updated",
|
||||
"close": "Close",
|
||||
"noBody": "This page has no content yet.",
|
||||
"deleteConfirm": "Delete \"{name}\" and everything inside it?",
|
||||
"deleteButton": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create",
|
||||
"creating": "Creating…",
|
||||
"fieldName": "Name",
|
||||
"fieldSourceQuery": "Source query (the question that rebuilds this page)",
|
||||
"fieldParent": "Parent folder",
|
||||
"rootFolder": "Root",
|
||||
"selectPagePrompt": "Select a page to read it.",
|
||||
"createFolderTitle": "New folder",
|
||||
"createPageTitle": "New page",
|
||||
"fieldMission": "Mission (what this folder should collect)",
|
||||
"missionPlaceholder": "e.g. Track all incidents for the payments service",
|
||||
"editMissionTitle": "Edit folder mission",
|
||||
"editMission": "Edit mission",
|
||||
"saveMission": "Save mission",
|
||||
"autoBadge": "auto",
|
||||
"missionNone": "No mission set"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ INTEGRATIONS: dict[str, IntegrationMeta] = {
|
||||
"nemoclaw": IntegrationMeta("@vectorize-io/hindsight-nemoclaw", "NemoClaw"),
|
||||
"strands": IntegrationMeta("hindsight-strands", "Strands"),
|
||||
"claude-code": IntegrationMeta("hindsight-memory", "Claude Code"),
|
||||
"zcode": IntegrationMeta("hindsight-zcode", "ZCode"),
|
||||
"claude-agent-sdk": IntegrationMeta("hindsight-claude-agent-sdk", "Claude Agent SDK"),
|
||||
"llamaindex": IntegrationMeta("hindsight-llamaindex", "LlamaIndex"),
|
||||
"codex": IntegrationMeta("hindsight-codex", "Codex"),
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
---
|
||||
title: "Devin Desktop Persistent Memory (Formerly Windsurf)"
|
||||
authors: [benfrank241]
|
||||
slug: "2026/07/02/devin-desktop-persistent-memory"
|
||||
date: 2026-07-02T13:00
|
||||
tags: [hindsight, devin-desktop, devin, windsurf, codeium, memory, persistent-memory, mcp, tutorial]
|
||||
description: "Add persistent memory to Devin Desktop (formerly Windsurf): a remote MCP server plus one always-on rule that recalls at task start and retains as you work."
|
||||
image: /img/blog/devin-desktop-persistent-memory.png
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||

|
||||
|
||||
[Devin Desktop](https://devin.ai) is the editor Cognition rebranded from Windsurf (formerly Codeium) in June 2026. The name changed; the gap didn't. Devin reads your codebase and holds a plan within a session, but it carries nothing across sessions. Close the editor, reopen it tomorrow, and the agent is a fresh model again, with no memory of the decision you talked through last week or the convention you set on Tuesday.
|
||||
|
||||
The `hindsight-devin-desktop` integration adds persistent long-term memory to Devin. It's worth understanding *how* it gets there, because Devin Desktop doesn't expose lifecycle hooks to third parties. There's no place to bolt a `sessionStart` recall or a `stop` retain. Instead the integration uses two things the editor *does* support: **remote [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers** and **always-on workspace rules**.
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Devin Desktop (formerly Windsurf) has no third-party lifecycle hooks, so memory is wired through MCP plus a rule, not hook scripts.
|
||||
- `hindsight-devin-desktop init` connects the Hindsight **remote MCP server** (Devin gets `recall` / `retain` / `reflect` tools) and writes one **always-on rule** to `.devin/rules/hindsight.md`.
|
||||
- The rule tells Devin to `recall` at the start of each task and `retain` durable facts as it works.
|
||||
- No local daemon, no plugin scripts, no per-turn hooks. The MCP endpoint connects straight to [Hindsight Cloud](https://hindsight.vectorize.io) or your self-hosted server.
|
||||
- This is **model-driven memory**: the rule rides in every request, but the actual recall/retain calls are Devin's decision. That's the main tradeoff versus deterministic hook-based integrations.
|
||||
|
||||
## Why Devin Desktop Needs Persistent Memory
|
||||
|
||||
A new Devin session starts with whatever it can see: your open files, the workspace, and any rules you've written in `.devin/rules/`. What it can't see is the past. The bug you traced through three files yesterday, the library you chose and why, the naming convention you've been holding the line on. None of that survives the session boundary unless you wrote it down somewhere Devin reads.
|
||||
|
||||
You can pin context by hand with rules files, and for stable facts that works. It doesn't help with the things you didn't know to record in advance. Persistent memory closes that gap: durable facts get retained as you work, and the relevant ones come back on their own next time.
|
||||
|
||||
That matters more for an editor you live in all day. A coding agent that reintroduces itself every morning isn't really an assistant. Memory is what turns a fresh-every-session model into one that builds on yesterday.
|
||||
|
||||
## How Devin Desktop Persistent Memory Works
|
||||
|
||||
Devin Desktop gives third parties two integration points, and `hindsight-devin-desktop` uses both.
|
||||
|
||||
**Remote MCP server.** Devin Desktop reads MCP servers from a single global config and supports *remote* servers via `serverUrl` with custom headers, so the integration points Devin straight at the Hindsight MCP endpoint with no local process to manage:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"serverUrl": "https://api.hindsight.vectorize.io/mcp/my-project/",
|
||||
"headers": { "Authorization": "Bearer hsk_..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That gives Devin three tools: `recall` (search memory), `retain` (store a durable fact), and `reflect` (a synthesized, memory-grounded answer). The memory bank is encoded in the endpoint path, so one config line scopes the whole connection to a bank.
|
||||
|
||||
**Always-on rule.** Devin Desktop applies any rule file under `.devin/rules/` whose frontmatter says `trigger: always_on` to every request in the workspace. The integration writes one dedicated file, `.devin/rules/hindsight.md`, telling Devin how and when to use those tools:
|
||||
|
||||
```markdown
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
<!-- Managed by hindsight-devin-desktop -->
|
||||
You have persistent long-term memory through the Hindsight MCP server
|
||||
(`recall`, `retain`, and `reflect` tools).
|
||||
|
||||
- At the start of each task, call `recall` with the user's request to load
|
||||
relevant decisions, preferences, and project context before you act.
|
||||
Use what's relevant and ignore the rest.
|
||||
- When you learn a durable fact, such as an architectural decision, a user
|
||||
preference, a convention, or anything worth remembering across sessions,
|
||||
call `retain` to store it.
|
||||
- Do not mention these memory operations unless the user asks about them.
|
||||
```
|
||||
|
||||
The file carries a sentinel comment (`<!-- Managed by hindsight-devin-desktop -->`) so the integration owns it end to end and can update or remove it idempotently without touching any other rule you've authored. Put together: the MCP server makes memory *available* as tools, and the always-on rule makes Devin *use* them.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install hindsight-devin-desktop
|
||||
cd your-project
|
||||
hindsight-devin-desktop init --api-token YOUR_HINDSIGHT_API_KEY --bank-id my-project
|
||||
```
|
||||
|
||||
`init` merges the `mcpServers` entry into Devin Desktop's global MCP config and writes the rule into `./.devin/rules/hindsight.md`. Reload Devin Desktop (or refresh MCP servers) and the `hindsight` tools are live.
|
||||
|
||||
Three commands cover the lifecycle: `hindsight-devin-desktop init` adds the MCP server and the recall/retain rule, `status` shows whether both are configured, and `uninstall` removes them. If your MCP config isn't plain JSON (comments, or some other tool owns it), `init` won't clobber it. It prints the snippet to paste instead, which you can also get anytime with `hindsight-devin-desktop init --print-only`.
|
||||
|
||||
## Cloud or Self-Hosted
|
||||
|
||||
By default the integration points at Hindsight Cloud (`https://api.hindsight.vectorize.io`), which needs an API key from your dashboard. To run against your own server, pass `--api-url`. If it's an open local server, you can skip the token entirely:
|
||||
|
||||
```bash
|
||||
hindsight-devin-desktop init --api-url http://localhost:8888 --bank-id my-project
|
||||
```
|
||||
|
||||
Settings can also come from the environment: `HINDSIGHT_API_URL` (the API endpoint, defaulting to Cloud), `HINDSIGHT_API_TOKEN` (the bearer token, required for Cloud), and `HINDSIGHT_DEVIN_DESKTOP_BANK_ID` (the bank to scope memory to, defaulting to `devin-desktop`). Point two projects at the same bank to share memory, or give each its own bank for isolation.
|
||||
|
||||
## A Rebrand Detail Worth Knowing
|
||||
|
||||
Because Devin Desktop is a rebrand of Windsurf, a couple of on-disk paths still carry the old name, and the integration handles that so you don't have to. The global MCP config still lives under `~/.codeium/windsurf/` (that's Devin Desktop's data directory, unchanged by the rename), while the workspace rule now lives under `.devin/rules/`, with `.windsurf/rules/` kept as a legacy fallback. If you used the integration back when it was the Windsurf package, your existing rule keeps working and the new path takes precedence going forward.
|
||||
|
||||
## The Tradeoff: Model-Driven, Not Hook-Driven
|
||||
|
||||
This is worth being direct about, because it's the real difference between this integration and the hook-based ones for Claude Code or the Cursor CLI.
|
||||
|
||||
Hook-based integrations are **deterministic**. A `sessionStart` hook recalls before the agent ever sees the prompt; a `stop` hook retains after every task, whether or not the model thought to. The recall and retain happen because the harness fires an event, not because the agent decided to.
|
||||
|
||||
Devin Desktop doesn't offer that surface to third parties, so `hindsight-devin-desktop` is **model-driven**. The always-on rule is injected into every request, so the instruction to use memory is always present, but the actual `recall` and `retain` calls are Devin's decision. In practice modern models follow a short, concrete always-on rule reliably. But it's an instruction, not a guarantee: Devin can skip a `retain` on a task it didn't judge memorable, or answer from context without calling `recall` first. If you want memory pulled for a specific task, you can just ask ("check memory for how we handled auth"), and `reflect` is there to consolidate on demand. The honest framing: Devin Desktop trades the guarantees of hooks for the simplicity of a remote MCP server and one rule file, with no local daemon and nothing to keep running.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
**Is Devin Desktop the same as Windsurf?**
|
||||
Yes. Cognition rebranded the Windsurf editor (formerly Codeium) to Devin Desktop in June 2026. The `hindsight-devin-desktop` package is the maintained integration; it writes its rule to `.devin/rules/` and still reads the MCP config under `~/.codeium/windsurf/`, which is unchanged by the rebrand.
|
||||
|
||||
**Does Devin Desktop have built-in memory across sessions?**
|
||||
No. A new session starts fresh. Persistent memory comes from an integration like `hindsight-devin-desktop` that gives Devin recall and retain over a memory layer.
|
||||
|
||||
**Will memory recall slow Devin down?**
|
||||
Recall is the agent's call, not a per-prompt hook, so there's no fixed overhead on every turn. When Devin does recall, a Hindsight Cloud query is typically well under a second.
|
||||
|
||||
**Does it work with self-hosted Hindsight?**
|
||||
Yes. Pass `--api-url` (or set `HINDSIGHT_API_URL`) to point at your server. For an open local server with no auth, omit the token.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [What is agent memory?](https://vectorize.io/what-is-agent-memory): the foundational concepts behind recall, retention, and memory banks.
|
||||
- [Best AI agent memory systems](https://vectorize.io/articles/best-ai-agent-memory-systems): how the major agent memory frameworks compare.
|
||||
- [Cursor persistent memory](/blog/2026/06/12/cursor-persistent-memory): the hook-based sibling integration for the Cursor editor and CLI.
|
||||
- [One memory for every AI tool](/blog/2026/04/07/one-memory-for-every-ai-tool): point Devin and your other agents at the same bank.
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
title: "From Documents to Decisions: architxt and Hindsight"
|
||||
authors: [garethjcooper]
|
||||
slug: "2026/07/03/architxt-hindsight-temporal-mosaic"
|
||||
date: 2026-07-03T12:00
|
||||
tags: [hindsight, architxt, integration, memory, temporal, enterprise-architecture, community, tutorial]
|
||||
description: "How architxt uses Hindsight as a time-aware agent memory layer to turn fragmented enterprise architecture documents into a queryable, current-state Temporal Mosaic."
|
||||
image: /img/blog/architxt-hindsight.png
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||

|
||||
|
||||
Every enterprise system is described somewhere. The problem is *where*.
|
||||
|
||||
Architecture decisions live in impact assessments buried in SharePoint. Integration details hide in Confluence pages written three years ago. Current-state diagrams sit in slide decks from a programme that was "phase 2'd" into oblivion. When someone asks, "How does billing actually work now?", the answer is never in one place. It is spread across dozens of documents, each written at a different time, for a different audience, with different assumptions about what was "current."
|
||||
|
||||
This is the problem architxt was built to solve. It integrates with [Hindsight](https://github.com/vectorize-io/hindsight), an agent memory system used as a durable, time-aware memory layer for enterprise semantic search and cross-document reasoning.
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
## architxt: turn documents into structured knowledge
|
||||
|
||||
architxt is a document processing pipeline and research framework, accessed via a web UI. You upload documents (PDFs, Word files, PowerPoint decks) and it extracts clean, structured content using Docling, LLM-based denoising, vision analysis for diagrams, and entity detection. The output is combined with key metadata to prepare it for Hindsight. The main components of this are:
|
||||
|
||||
- **Documents** with metadata, tags, and extracted blocks.
|
||||
- **Entities** (systems, services, capabilities) detected and normalised across documents.
|
||||
- **Mental models**: reusable LLM prompts that analyse an entity from a specific angle (capabilities, interfaces, summaries). These allow for quick retrieval of common dimensions per entity.
|
||||
- **A temporal mosaic**: research and query extraction of the best-known state of each entity, regardless of when the source document was written.
|
||||
|
||||

|
||||
|
||||
The key insight is that **knowledge freshness is not document freshness**. A 2020 component design is still valid if that component has not changed, even if the rest of the system was rewritten twice since. architxt tracks which document touched which entity, when, and lets you reason across the whole corpus without pretending there is a single "as-is" document.
|
||||
|
||||
## The architxt process and minimum viable input
|
||||
|
||||
The tool works best when documents arrive with a small amount of consistent data. architxt does not need everything to be perfect; it needs enough structure to know what each document is, when it was produced, and what it is about.
|
||||
|
||||
### Document hygiene
|
||||
|
||||
Each document should carry:
|
||||
|
||||
- **Document ID**: a stable identifier. The same document re-uploaded keeps the same ID.
|
||||
- **Document date**: the publish date, in ISO 8601. This is the anchor for temporal reasoning.
|
||||
- **Context**: a curated value describing the layer or purpose of the document, such as impact assessment, component design, or business capability definition.
|
||||
- **Tags**: consistent filters such as project, domain, system, or data area.
|
||||
- **Source metadata**: where the document came from (Confluence, SharePoint, a file path) and who produced it.
|
||||
|
||||
architxt includes tagging alignment tools that help assign context, tags, source metadata, and entity identifiers. The time saving comes from the bulk change and consistency checking architxt enables across the whole corpus. It aligns information across dozens or hundreds of documents without requiring each one to be manually updated directly through the UI. A document is only useful in the mosaic if you can locate it again, filter by it, and trust its date; architxt uses that externally curated structure to build a reliable, consistent current-state view.
|
||||
|
||||
### Entities are the anchor
|
||||
|
||||
Entities are the central unit. A component, a service, a capability: each becomes a stable reference point that observations from different documents can attach to. Without entities, a document is just a bag of text. With entities, a sentence in a 2020 design and a paragraph in a 2024 migration document can both refer to the same thing, and architxt can keep the latest view of that thing intact.
|
||||
|
||||
In practice, the same entity is rarely called the same thing in every document. One document might say "Billing Engine", another "Billing Service", another "BE". Aliases let architxt map these varied names back to a single entity. The more aliases are known, the more complete the entity timeline becomes.
|
||||
|
||||
To make this work across time, entities are embedded into documents with a stable ID, using a lightweight tagging convention such as `Billing Engine (SYS-001)`. The human name can change ("Billing Engine" might become "Billing Platform" in a later design) but the stable ID survives. architxt then resolves the current name against the stable ID, so references from older documents remain usable even after naming conventions shift.
|
||||
|
||||

|
||||
|
||||
This is why the minimum viable input matters. Good metadata and tags make recall precise. A stable, aliased entity namespace makes composition across documents and across years possible. The rest, mental models, reflections, and the temporal mosaic, builds on top of that foundation.
|
||||
|
||||
## What Hindsight adds
|
||||
|
||||
So, we have the data and have a basic set of metadata. We know there's a goldmine of information, that probably cost thousands or millions of dollars to get written. How do we process and store it in a way that accommodates the variance in source text?
|
||||
|
||||
Hindsight stores the extracted knowledge as a durable, time-aware memory bank. Rather than replacing documents with a single summary, it keeps observations as discrete entries that include when they were captured and where they came from. This matters because the temporal mosaic is only possible when you can ask "what do we most recently know about X?" instead of "what does the latest document say?"
|
||||
|
||||
Two Hindsight primitives make this work:
|
||||
|
||||
- **Retain / Observation**: when architxt extracts facts from a document, they are retained as timestamped, source-referenced facts, and then consolidated into observations. A new document about the same entity does not overwrite the old one; it adds newer facts, and the observation is re-consolidated. The mosaic can then prefer the latest observation per facet while still keeping older ones visible where nothing newer exists.
|
||||
|
||||
- **Reflect / Mental models**: mental models are reusable prompts that run over the current set of memories. They can be refreshed as new documentation is added, so summaries, capability lists, and interface descriptions stay current; auto-refresh after consolidation is opt-in. The output is tied to the observations it was based on, so the generated view remains grounded and traceable.
|
||||
|
||||
Together, retain and reflect mean the mosaic updates incrementally. New documents are ingested, facts are retained, and mental models are re-run. The "current state" is not rebuilt from scratch; it is the latest layer of a continuously updated knowledge stack.
|
||||
|
||||

|
||||
|
||||
## The Temporal Mosaic: current state without a single source of truth
|
||||
|
||||
This combination matters because most architecture tools force one of two models:
|
||||
|
||||
1. **Static models**: draw a diagram once, watch it rot.
|
||||
2. **Designed-vs-delivered reconciliation**: try to maintain two parallel realities and merge them.
|
||||
|
||||
architxt takes a third path, using Hindsight as its durable memory layer. The "current state" is a mosaic: the latest reliable knowledge for each entity, sourced from whichever document last touched it. A component from a 2020 design sits next to a service from a 2024 migration document. Seams (contradictions, outdated interfaces, orphaned dependencies) surface only when a query crosses them.
|
||||
|
||||
This is the **Temporal Mosaic**. It accepts that organisations do not produce one consistent architecture document. They produce a stream of partial, dated, overlapping documents. Rather than flattening them into a single model, architxt uses Hindsight to make them queryable as a composite, with answers grounded in source documents and tagged with entities so they carry provenance rather than relying on model hallucination.
|
||||
|
||||
## What this looks like in practice
|
||||
|
||||
Once the documents are tagged and ingested, the question changes. Instead of "which document might have the answer?", you can ask direct questions against the corpus.
|
||||
|
||||
For example:
|
||||
|
||||
- "What are the integration points between the billing service and the customer platform?"
|
||||
- "Which capabilities does the order processing system support, and which documents describe them?"
|
||||
- "Has the data model for customer records changed since the 2022 platform migration?"
|
||||
|
||||

|
||||
|
||||
Hindsight returns grounded answers. Each claim is tied back to the document and observation it came from, so you can verify it rather than trust a generated summary. If two documents disagree, that disagreement is surfaced rather than smoothed over. This turns document search from a guessing game into a structured query.
|
||||
|
||||
## In short
|
||||
|
||||
The real cost of fragmented architecture knowledge is not the documents themselves. It is the time people spend trying to reconstruct what those documents mean when taken together.
|
||||
|
||||
architxt reduces that cost by turning documents into structured, entity-tagged observations. Hindsight keeps those observations alive over time. The Temporal Mosaic is the result: a current-state view that does not pretend the organisation ever produced a single authoritative description, but still makes the combined knowledge searchable, verifiable, and current.
|
||||
|
||||
That is the shift. Not more documents, or better diagrams, but a way to extract value from the documents already in place and provide a pathway for keeping that state current as new documents get written. The format and scope of future design documents is unknown, but they will contain information worth leveraging. architxt, coupled with Hindsight, is built to make that possible.
|
||||
@@ -2,12 +2,6 @@ hindsight:
|
||||
name: Hindsight Team
|
||||
url: https://github.com/vectorize-io/hindsight
|
||||
|
||||
garethjcooper:
|
||||
name: Gareth Cooper
|
||||
title: Community Contributor
|
||||
url: https://github.com/garethjcooper
|
||||
image_url: https://github.com/garethjcooper.png
|
||||
|
||||
nicoloboschi:
|
||||
name: Nicolò Boschi
|
||||
title: Hindsight Team
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
sidebar_position: 38
|
||||
title: "Eve Agent Memory with Hindsight | Integration"
|
||||
description: "Add automatic long-term memory to Vercel Eve agents with Hindsight. Memory is injected before each turn and retained after — no model tool-calling."
|
||||
description: "Add long-term memory to Vercel Eve agents with Hindsight. A one-line MCP connection gives your agent retain, recall, and reflect across sessions."
|
||||
---
|
||||
|
||||
# Eve
|
||||
|
||||
Automatic long-term memory for [Vercel Eve](https://github.com/vercel/eve) agents using [Hindsight](https://vectorize.io/hindsight). Eve is filesystem-first — an agent gains a capability by dropping a file under `agent/`. The `@vectorize-io/hindsight-eve` package wires two files that call Hindsight's REST API directly, so your agent gets memory that **just works** — relevant memory is injected before every turn and each exchange is retained after — **without the model ever choosing to call a tool.**
|
||||
Long-term memory for [Vercel Eve](https://github.com/vercel/eve) agents using [Hindsight](https://vectorize.io/hindsight). Eve is filesystem-first — an agent gains a capability by dropping a file under `agent/connections/`. The `@vectorize-io/hindsight-eve` package wraps Eve's `defineMcpClientConnection`, so one file gives your agent `retain`, `recall`, and `reflect` over Hindsight's MCP server and it remembers across sessions and deployments.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -18,71 +18,67 @@ npm install @vectorize-io/hindsight-eve
|
||||
|
||||
## Quick Start
|
||||
|
||||
Create two files:
|
||||
Create `agent/connections/hindsight.ts`:
|
||||
|
||||
```ts
|
||||
// agent/instructions/hindsight.ts — recall: inject memory before each turn
|
||||
import { hindsightMemory } from "@vectorize-io/hindsight-eve";
|
||||
import { defineHindsightConnection } from "@vectorize-io/hindsight-eve";
|
||||
|
||||
export default hindsightMemory();
|
||||
export default defineHindsightConnection();
|
||||
```
|
||||
|
||||
```ts
|
||||
// agent/hooks/hindsight.ts — retain: save each exchange after the turn
|
||||
import { hindsightRetainHook } from "@vectorize-io/hindsight-eve";
|
||||
The connection reads its defaults from the environment:
|
||||
|
||||
export default hindsightRetainHook();
|
||||
```
|
||||
| Env var | Purpose |
|
||||
| ----------------------- | ---------------------------------------------------------------- |
|
||||
| `HINDSIGHT_API_KEY` | Bearer token sent as `Authorization: Bearer <key>` |
|
||||
| `HINDSIGHT_MCP_URL` | MCP endpoint (defaults to Hindsight Cloud) |
|
||||
| `HINDSIGHT_MCP_BANK_ID` | Optional bank to scope memory to, sent as the `X-Bank-Id` header |
|
||||
|
||||
Both read their config from the environment:
|
||||
|
||||
| Env var | Purpose |
|
||||
| ------------------- | -------------------------------------------------------------- |
|
||||
| `HINDSIGHT_API_KEY` | Bearer token sent as `Authorization: Bearer <key>` |
|
||||
| `HINDSIGHT_API_URL` | Hindsight REST base (defaults to Hindsight Cloud) |
|
||||
| `HINDSIGHT_BANK_ID` | Bank to scope memory to (defaults to `default`; auto-created) |
|
||||
The model discovers the tools via Eve's `connection__search` and calls them as `connection__hindsight__recall`, `connection__hindsight__retain`, and `connection__hindsight__reflect`. The connection's URL and token never reach the model.
|
||||
|
||||
### Hindsight Cloud
|
||||
|
||||
Set `HINDSIGHT_API_KEY` from your [Hindsight Cloud](https://hindsight.vectorize.io) dashboard. `HINDSIGHT_API_URL` defaults to `https://api.hindsight.vectorize.io`, so no URL is needed.
|
||||
Set `HINDSIGHT_API_KEY` from your [Hindsight Cloud](https://hindsight.vectorize.io) dashboard. The connection defaults to `https://api.hindsight.vectorize.io/mcp`, so no URL is needed.
|
||||
|
||||
### Self-hosted
|
||||
|
||||
```ts
|
||||
import { hindsightMemory } from "@vectorize-io/hindsight-eve";
|
||||
Point at your own server, optionally scoping to a bank. Use `apiKey: null` for a no-auth local server:
|
||||
|
||||
// A local server with no auth:
|
||||
export default hindsightMemory({ apiUrl: "http://localhost:8000", apiKey: null });
|
||||
```ts
|
||||
import { defineHindsightConnection } from "@vectorize-io/hindsight-eve";
|
||||
|
||||
export default defineHindsightConnection({
|
||||
url: "http://localhost:8000/mcp",
|
||||
apiKey: null,
|
||||
});
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Both factories accept the same options (each falls back to its env var):
|
||||
|
||||
```ts
|
||||
hindsightMemory({
|
||||
apiUrl, // REST base; defaults to HINDSIGHT_API_URL, then Cloud
|
||||
defineHindsightConnection({
|
||||
url, // MCP endpoint; defaults to HINDSIGHT_MCP_URL, then Cloud
|
||||
apiKey, // bearer token; null = no auth (local dev)
|
||||
bankId, // bank to scope memory to
|
||||
recallQuery, // the broad query used for recall (see below)
|
||||
budget, // "low" | "mid" | "high" — recall result budget (default "mid")
|
||||
maxTokens, // recall token budget (default 1024)
|
||||
context, // `context` tag written on retained items (default "eve")
|
||||
includeAssistantReply, // also retain the assistant's reply (default false — user message only)
|
||||
timeoutMs, // HTTP timeout (default 15000)
|
||||
onError, // (err, phase) => void — failures degrade silently (default console.warn)
|
||||
bankId, // scope memory to a bank (X-Bank-Id header)
|
||||
description, // override the model-facing description
|
||||
tools, // { allow } | { block } — narrow which Hindsight tools the model sees
|
||||
approval, // human-in-the-loop policy, e.g. once() from "eve/tools/approval"
|
||||
});
|
||||
```
|
||||
|
||||
## Recall is profile-based, not per-message
|
||||
Restrict the agent to read-only recall and require approval the first time:
|
||||
|
||||
Eve's instruction resolver runs at the start of a turn and **cannot see the live user message**, so recall uses a fixed broad query (default: `"user preferences, identity, and working context"`) to surface the user's ambient profile/context each turn. This is ideal for "the agent knows you" — preferences, identity, ongoing context — and is fully deterministic. Tune it with `recallQuery`. Per-message, query-specific retrieval inherently needs a tool the model calls and is out of scope here.
|
||||
```ts
|
||||
import { defineHindsightConnection } from "@vectorize-io/hindsight-eve";
|
||||
import { once } from "eve/tools/approval";
|
||||
|
||||
## Verify
|
||||
|
||||
Run your agent. Tell it a durable preference in one chat ("whenever you write me code, use Python with full type hints and no comments"). Start a **fresh** chat and ask for something — the agent applies the remembered preference, because the memory was injected before the model ran, with no tool call.
|
||||
export default defineHindsightConnection({
|
||||
tools: { allow: ["recall", "reflect"] },
|
||||
approval: once(),
|
||||
});
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Hindsight docs](https://hindsight.vectorize.io)
|
||||
- [Eve hooks](https://github.com/vercel/eve/blob/main/docs/guides/hooks.md) · [Eve dynamic capabilities](https://github.com/vercel/eve/blob/main/docs/guides/dynamic-capabilities.md)
|
||||
- [Eve connections](https://github.com/vercel/eve/blob/main/docs/connections.mdx)
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
---
|
||||
sidebar_position: 40
|
||||
title: "ZCode Persistent Memory with Hindsight | Integration Guide"
|
||||
description: "Add persistent long-term memory to ZCode (Z.ai's GLM desktop coding agent) with Hindsight. Python hooks automatically recall context before each prompt and retain conversations — no MCP, no workflow changes."
|
||||
---
|
||||
|
||||
# ZCode
|
||||
|
||||
Persistent memory for [ZCode](https://zcode.z.ai) — Z.ai's GLM desktop coding agent — using [Hindsight](https://vectorize.io/hindsight). ZCode embeds the Claude Code agent runtime, so Python hook scripts automatically recall relevant context before each prompt and retain conversations after each turn. No MCP server, no changes to your ZCode workflow.
|
||||
|
||||
## Quick Start
|
||||
|
||||
:::tip Recommended: Hindsight Cloud
|
||||
[Sign up free](https://ui.hindsight.vectorize.io/signup) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
|
||||
:::
|
||||
|
||||
```bash
|
||||
# Install the CLI
|
||||
pip install hindsight-zcode
|
||||
|
||||
# Install the hooks (defaults to Hindsight Cloud)
|
||||
hindsight-zcode install --api-url https://api.hindsight.vectorize.io --api-token your-api-key
|
||||
|
||||
# Restart ZCode — memory is live
|
||||
```
|
||||
|
||||
The installer copies the hook scripts to `~/.zcode/hooks/hindsight/`, registers them in `~/.zcode/cli/config.json` (merged with any existing hooks), and creates `~/.hindsight/zcode.json` for your personal config. It never touches your Claude Code config at `~/.claude/settings.json`.
|
||||
|
||||
**Self-hosting alternative** — connect to a local `hindsight-embed` daemon by omitting the flags:
|
||||
|
||||
```bash
|
||||
hindsight-zcode install
|
||||
```
|
||||
|
||||
To uninstall:
|
||||
|
||||
```bash
|
||||
hindsight-zcode uninstall
|
||||
```
|
||||
|
||||
### Alternative: install as a ZCode plugin
|
||||
|
||||
ZCode can install Hindsight directly from a plugin marketplace — no `pip` step. The same hook scripts ship as a hooks-only Claude Code plugin (`hindsight-zcode`) in the Hindsight marketplace:
|
||||
|
||||
```
|
||||
# In ZCode: add the Hindsight marketplace, then install the plugin
|
||||
zcode plugins add-marketplace vectorize-io/hindsight
|
||||
zcode plugins install hindsight-zcode
|
||||
```
|
||||
|
||||
When installed this way, ZCode registers the hooks automatically (no config-file edit). Provide your Hindsight credentials via environment variables (`HINDSIGHT_API_URL`, `HINDSIGHT_API_TOKEN`) or by creating `~/.hindsight/zcode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
|
||||
"hindsightApiToken": "hsk_your_token"
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Auto-recall** — before each prompt, queries Hindsight for relevant memories and injects them as additional context (visible to the model, not the transcript)
|
||||
- **Auto-retain** — after each response, stores the turn to Hindsight for future recall
|
||||
- **No MCP required** — plain Python hook scripts calling Hindsight's REST API; nothing to run alongside ZCode
|
||||
- **Cross-tool memory** — the same Hindsight bank is shared across Claude Code, Cursor, and other Hindsight integrations, so memory follows you between tools
|
||||
- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory
|
||||
- **Zero runtime dependencies** — the hook scripts are pure Python stdlib; the `pip install` only ships the one-time installer
|
||||
|
||||
## Architecture
|
||||
|
||||
ZCode embeds the Claude Code agent runtime and reads the standard Claude Code hook schema from its own config namespace, `~/.zcode/cli/config.json` (with `hooks.enabled: true`). The plugin wires three hook events:
|
||||
|
||||
| Hook | Event | Purpose |
|
||||
|------|-------|---------|
|
||||
| `session_start.py` | `SessionStart` | Warm up — verify Hindsight is reachable |
|
||||
| `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` |
|
||||
| `retain.py` | `Stop` | **Auto-retain** — assemble the turn, POST to Hindsight |
|
||||
|
||||
On `UserPromptSubmit`, the hook reads the prompt, queries Hindsight for the most relevant memories, and emits a context block that ZCode injects before sending the turn to the model:
|
||||
|
||||
```
|
||||
<hindsight_memories>
|
||||
Relevant memories from past conversations...
|
||||
Current time - 2026-03-27 09:14
|
||||
|
||||
- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26)
|
||||
- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26)
|
||||
</hindsight_memories>
|
||||
```
|
||||
|
||||
On `Stop`, the hook pairs the user prompt (captured at `UserPromptSubmit`) with the agent's response and POSTs the turn to Hindsight. ZCode does not provide a `SessionEnd` hook event, so retention rides `Stop` — every turn is stored as it completes.
|
||||
|
||||
## Connection Modes
|
||||
|
||||
### 1. External API (recommended)
|
||||
|
||||
Connect to a running Hindsight server (cloud or self-hosted) via `~/.hindsight/zcode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "https://api.hindsight.vectorize.io",
|
||||
"hindsightApiToken": "hsk_your_token"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Local Daemon
|
||||
|
||||
Run `hindsight-embed` locally. The `session_start.py` hook detects it on `apiPort` (default `9077`). The daemon is not auto-started by the plugin — start it separately:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed
|
||||
```
|
||||
|
||||
Then leave `hindsightApiUrl` empty in your config and the plugin connects to `http://localhost:9077`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Default config ships in `~/.zcode/hooks/hindsight/settings.json`. For personal overrides that survive updates, create `~/.hindsight/zcode.json`. Most settings can also be overridden via environment variable.
|
||||
|
||||
**Loading order** (later entries win):
|
||||
|
||||
1. Built-in defaults
|
||||
2. Plugin `settings.json` (at `~/.zcode/hooks/hindsight/settings.json`)
|
||||
3. User config (`~/.hindsight/zcode.json`)
|
||||
4. Environment variables
|
||||
|
||||
---
|
||||
|
||||
### Connection
|
||||
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Empty = local daemon. |
|
||||
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. |
|
||||
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. |
|
||||
|
||||
---
|
||||
|
||||
### Memory Bank
|
||||
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `bankId` | `HINDSIGHT_BANK_ID` | `"zcode"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. |
|
||||
| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. |
|
||||
| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. |
|
||||
| `agentName` | `HINDSIGHT_AGENT_NAME` | `"zcode"` | Agent name used in dynamic bank ID derivation. |
|
||||
|
||||
---
|
||||
|
||||
### Auto-Recall
|
||||
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. |
|
||||
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). |
|
||||
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Token budget for the injected memory block. |
|
||||
|
||||
---
|
||||
|
||||
### Auto-Retain
|
||||
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. |
|
||||
| `retainEveryNTurns` | `HINDSIGHT_RETAIN_EVERY_N_TURNS` | `1` | Retain every N turns. Default `1` stores every turn on `Stop`. |
|
||||
|
||||
## Relationship to ZCode's built-in memory
|
||||
|
||||
ZCode ships its own local, per-project memory (`~/.zcode/cli/memories/`). Hindsight is complementary: it stores memory in a **cloud (or self-hosted) bank that is shared across tools** — the same bank powers Claude Code, Cursor, and other Hindsight integrations — so your context follows you between agents and machines rather than staying local to one ZCode project.
|
||||
@@ -140,16 +140,6 @@
|
||||
"link": "/sdks/integrations/claude-code",
|
||||
"icon": "/img/icons/claude-code.png"
|
||||
},
|
||||
{
|
||||
"id": "zcode",
|
||||
"name": "ZCode",
|
||||
"description": "No-MCP long-term memory for ZCode (Z.ai's GLM desktop coding agent) via Hindsight hooks. Recalls relevant context before each prompt and retains conversations after each turn.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "tool",
|
||||
"link": "/sdks/integrations/zcode",
|
||||
"icon": "/img/icons/zcode.svg"
|
||||
},
|
||||
{
|
||||
"id": "claude-agent-sdk",
|
||||
"name": "Claude Agent SDK",
|
||||
@@ -383,7 +373,7 @@
|
||||
{
|
||||
"id": "eve",
|
||||
"name": "Eve",
|
||||
"description": "Automatic long-term memory for Vercel Eve agents. Memory is injected before each turn and retained after, with no model tool-calling.",
|
||||
"description": "Long-term memory for Vercel Eve agents. A one-line MCP connection exposing retain, recall, and reflect.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "framework",
|
||||
|
||||
@@ -10,12 +10,6 @@ For the source code, see [`hindsight-integrations/eve`](https://github.com/vecto
|
||||
|
||||
← [Back to main changelog](/changelog)
|
||||
|
||||
## [0.2.0](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.2.0)
|
||||
|
||||
**Features**
|
||||
|
||||
- Added an auto-memory mode that works without model tool-calling, enabling Eve to capture memories automatically.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}><img src="https://github.com/benfrank241.png?size=40" alt="@benfrank241" width="18" height="18" style={{borderRadius: "50%"}} />@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/dd7e25245" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>dd7e25245</a>
|
||||
|
||||
## [0.1.0](https://github.com/vectorize-io/hindsight/tree/integrations/eve/v0.1.0)
|
||||
|
||||
**Features**
|
||||
|
||||
|
Before Width: | Height: | Size: 612 KiB |
|
Before Width: | Height: | Size: 589 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 325 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="zcodeA" x1="8" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#4F7CFF"/>
|
||||
<stop offset="1" stop-color="#1BC7B4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="6" y="6" width="52" height="52" rx="13" fill="url(#zcodeA)"/>
|
||||
<path d="M22 22h20v5.2L28.4 42H42v6H22v-5.2L35.6 28H22z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
@@ -2010,6 +2010,531 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/tree": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Get the knowledge-base tree",
|
||||
"description": "Return the knowledge base as a nested tree of folders and pages.",
|
||||
"operationId": "get_knowledge_base_tree",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgeTreeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/folders": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Create a knowledge-base folder",
|
||||
"description": "Create a folder, optionally nested under a parent folder.",
|
||||
"operationId": "create_knowledge_folder",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateFolderRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgeNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/pages": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Create a knowledge-base page",
|
||||
"description": "Create a page (a mental model + tree node). Content is generated asynchronously; use the returned operation_id to track completion.",
|
||||
"operationId": "create_knowledge_page",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreatePageRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateKnowledgePageResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/graph": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Knowledge-base constellation graph",
|
||||
"description": "Return pages as nodes linked by shared tags, for the constellation view.",
|
||||
"operationId": "get_knowledge_base_graph",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgePageGraphResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/export": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Export the knowledge base as an OKF bundle",
|
||||
"description": "Return a portable OKF bundle: a nested index.md, one <id>.md per page, and history logs.",
|
||||
"operationId": "export_knowledge_base",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgePageBundleResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Get a knowledge-base page",
|
||||
"description": "Return a single page as an OKF document (frontmatter + markdown body).",
|
||||
"operationId": "get_knowledge_page",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Page Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgePageResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}": {
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Rename or move a knowledge-base node",
|
||||
"description": "Rename a node (set `name`) and/or move it under another folder (set `parent_id`, null for the root).",
|
||||
"operationId": "update_knowledge_node",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "node_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Node Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateNodeRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgeNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Knowledge Base"
|
||||
],
|
||||
"summary": "Delete a knowledge-base node",
|
||||
"description": "Delete a folder or page and its whole subtree (pages' mental models are removed too).",
|
||||
"operationId": "delete_knowledge_node",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "node_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Node Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/directives": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -7744,6 +8269,61 @@
|
||||
"title": "CreateDirectiveRequest",
|
||||
"description": "Request model for creating a directive."
|
||||
},
|
||||
"CreateFolderRequest": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"parent_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Parent Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "CreateFolderRequest",
|
||||
"description": "Create a folder under an optional parent folder."
|
||||
},
|
||||
"CreateKnowledgePageResponse": {
|
||||
"properties": {
|
||||
"page_id": {
|
||||
"type": "string",
|
||||
"title": "Page Id"
|
||||
},
|
||||
"mental_model_id": {
|
||||
"type": "string",
|
||||
"title": "Mental Model Id"
|
||||
},
|
||||
"operation_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Operation Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"page_id",
|
||||
"mental_model_id"
|
||||
],
|
||||
"title": "CreateKnowledgePageResponse",
|
||||
"description": "Result of creating a page: the node id, its mental model, and the refresh op."
|
||||
},
|
||||
"CreateMentalModelRequest": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -7838,6 +8418,71 @@
|
||||
"title": "CreateMentalModelResponse",
|
||||
"description": "Response model for mental model creation."
|
||||
},
|
||||
"CreatePageRequest": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"source_query": {
|
||||
"type": "string",
|
||||
"title": "Source Query"
|
||||
},
|
||||
"parent_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Parent Id"
|
||||
},
|
||||
"tags": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Tags"
|
||||
},
|
||||
"max_tokens": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Max Tokens"
|
||||
},
|
||||
"trigger": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/MentalModelTrigger-Input"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"source_query"
|
||||
],
|
||||
"title": "CreatePageRequest",
|
||||
"description": "Create a page (a mental model + tree node) under an optional parent folder."
|
||||
},
|
||||
"CreateWebhookRequest": {
|
||||
"properties": {
|
||||
"url": {
|
||||
@@ -9064,6 +9709,268 @@
|
||||
"title": "IncludeOptions",
|
||||
"description": "Options for including additional data in recall results."
|
||||
},
|
||||
"KnowledgeNode": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"title": "Id"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"folder",
|
||||
"page"
|
||||
],
|
||||
"title": "Kind"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"parent_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Parent Id"
|
||||
},
|
||||
"mental_model_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Mental Model Id",
|
||||
"description": "Backing mental model id (pages only)."
|
||||
},
|
||||
"managed": {
|
||||
"type": "boolean",
|
||||
"title": "Managed",
|
||||
"description": "Client-set flag: true = system-owned, false = hand-authored.",
|
||||
"default": false
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Description",
|
||||
"description": "Page source query (OKF `description`)."
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Tags",
|
||||
"default": []
|
||||
},
|
||||
"timestamp": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Timestamp",
|
||||
"description": "Last refresh (page) or last update (folder)."
|
||||
},
|
||||
"children": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/KnowledgeNode"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Children",
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind",
|
||||
"name"
|
||||
],
|
||||
"title": "KnowledgeNode",
|
||||
"description": "A node in the knowledge-base tree \u2014 a folder or a page.\n\nPages carry ``description``/``tags`` from their backing mental model. The\nknowledge base is client-managed (CRUD); ``managed`` lets a client tag a node\nas system-owned vs. hand-authored."
|
||||
},
|
||||
"KnowledgePageBundleFile": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"title": "Path"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"title": "Content"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
],
|
||||
"title": "KnowledgePageBundleFile",
|
||||
"description": "One file in a portable OKF bundle."
|
||||
},
|
||||
"KnowledgePageBundleResponse": {
|
||||
"properties": {
|
||||
"files": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/KnowledgePageBundleFile"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"files"
|
||||
],
|
||||
"title": "KnowledgePageBundleResponse",
|
||||
"description": "A portable OKF bundle \u2014 a flat set of markdown files (index + pages + logs)."
|
||||
},
|
||||
"KnowledgePageGraphResponse": {
|
||||
"properties": {
|
||||
"nodes": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Nodes"
|
||||
},
|
||||
"edges": {
|
||||
"items": {
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Edges"
|
||||
},
|
||||
"total_pages": {
|
||||
"type": "integer",
|
||||
"title": "Total Pages"
|
||||
},
|
||||
"total_edges": {
|
||||
"type": "integer",
|
||||
"title": "Total Edges"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"nodes",
|
||||
"edges",
|
||||
"total_pages",
|
||||
"total_edges"
|
||||
],
|
||||
"title": "KnowledgePageGraphResponse",
|
||||
"description": "Constellation graph of knowledge pages linked by shared tags."
|
||||
},
|
||||
"KnowledgePageResponse": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"title": "Id"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "Type",
|
||||
"description": "OKF document type \u2014 from a `type:<x>` tag, else 'knowledge-page'."
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Description",
|
||||
"description": "The source query that rebuilds the page."
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Tags",
|
||||
"default": []
|
||||
},
|
||||
"timestamp": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Timestamp",
|
||||
"description": "Last refresh time (falls back to creation)."
|
||||
},
|
||||
"body": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Body",
|
||||
"description": "The page's synthesized markdown body."
|
||||
},
|
||||
"markdown": {
|
||||
"type": "string",
|
||||
"title": "Markdown",
|
||||
"description": "The full OKF document: YAML frontmatter + markdown body."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"markdown"
|
||||
],
|
||||
"title": "KnowledgePageResponse",
|
||||
"description": "A knowledge page rendered as an OKF document."
|
||||
},
|
||||
"KnowledgeTreeResponse": {
|
||||
"properties": {
|
||||
"roots": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/KnowledgeNode"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Roots"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"roots"
|
||||
],
|
||||
"title": "KnowledgeTreeResponse",
|
||||
"description": "The knowledge base as a nested folder/page tree."
|
||||
},
|
||||
"LLMRequestEntry": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -12797,6 +13704,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateNodeRequest": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Name"
|
||||
},
|
||||
"parent_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Parent Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "UpdateNodeRequest",
|
||||
"description": "Rename and/or move a node. Each field applies only when present."
|
||||
},
|
||||
"UpdateWebhookRequest": {
|
||||
"properties": {
|
||||
"url": {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
# Hindsight for Eve
|
||||
|
||||
Automatic long-term memory for [Vercel Eve](https://github.com/vercel/eve) agents, powered by
|
||||
[Hindsight](https://vectorize.io/hindsight). Two files give your agent memory that **just
|
||||
works** — relevant memory is injected before every turn, and each exchange is saved after —
|
||||
**without the model ever choosing to call a tool.**
|
||||
Long-term memory for [Vercel Eve](https://github.com/vercel/eve) agents, powered by
|
||||
[Hindsight](https://vectorize.io/hindsight). One file gives your agent `retain`, `recall`,
|
||||
and `reflect` over [Hindsight's MCP server](https://hindsight.vectorize.io) — so it
|
||||
remembers facts across sessions and deployments instead of starting cold every time.
|
||||
|
||||
## How it works
|
||||
|
||||
Eve is filesystem-first. This package wires two authored files that call Hindsight's REST API
|
||||
directly, so memory never depends on the LLM deciding to call a tool:
|
||||
|
||||
- **`agent/instructions/hindsight.ts`** — a dynamic instructions resolver that, before each
|
||||
turn, recalls the user's stored memory from Hindsight and injects it as a system message.
|
||||
- **`agent/hooks/hindsight.ts`** — a hook that, after each turn, retains the user message and
|
||||
the assistant's answer to Hindsight.
|
||||
Eve is filesystem-first: an agent gains a capability by dropping a file under
|
||||
`agent/connections/`. This package wraps eve's `defineMcpClientConnection`, pre-filling the
|
||||
Hindsight MCP endpoint, a model-facing description, and bearer auth. The model discovers the
|
||||
tools through `connection__search` and calls them as `connection__hindsight__recall`,
|
||||
`connection__hindsight__retain`, and `connection__hindsight__reflect`. The connection's URL
|
||||
and token never reach the model.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -25,97 +24,81 @@ npm install @vectorize-io/hindsight-eve
|
||||
|
||||
## Quick start
|
||||
|
||||
Create two files:
|
||||
Create `agent/connections/hindsight.ts`:
|
||||
|
||||
```ts
|
||||
// agent/instructions/hindsight.ts
|
||||
import { hindsightMemory } from "@vectorize-io/hindsight-eve";
|
||||
import { defineHindsightConnection } from "@vectorize-io/hindsight-eve";
|
||||
|
||||
export default hindsightMemory();
|
||||
export default defineHindsightConnection();
|
||||
```
|
||||
|
||||
```ts
|
||||
// agent/hooks/hindsight.ts
|
||||
import { hindsightRetainHook } from "@vectorize-io/hindsight-eve";
|
||||
That's it. By default the connection reads:
|
||||
|
||||
export default hindsightRetainHook();
|
||||
```
|
||||
|
||||
That's it. Both read their config from the environment:
|
||||
|
||||
| Env var | Purpose |
|
||||
| ------------------- | ------------------------------------------------------------- |
|
||||
| `HINDSIGHT_API_KEY` | Bearer token sent as `Authorization: Bearer <key>` |
|
||||
| `HINDSIGHT_API_URL` | Hindsight REST base (defaults to Hindsight Cloud) |
|
||||
| `HINDSIGHT_BANK_ID` | Bank to scope memory to (defaults to `default`; auto-created) |
|
||||
| Env var | Purpose |
|
||||
| ----------------------- | ---------------------------------------------------------------- |
|
||||
| `HINDSIGHT_API_KEY` | Bearer token sent as `Authorization: Bearer <key>` |
|
||||
| `HINDSIGHT_MCP_URL` | MCP endpoint (defaults to Hindsight Cloud) |
|
||||
| `HINDSIGHT_MCP_BANK_ID` | Optional bank to scope memory to, sent as the `X-Bank-Id` header |
|
||||
|
||||
### Hindsight Cloud
|
||||
|
||||
Set `HINDSIGHT_API_KEY` to a key from your [Hindsight Cloud](https://hindsight.vectorize.io)
|
||||
dashboard. `HINDSIGHT_API_URL` defaults to `https://api.hindsight.vectorize.io`, so no URL is
|
||||
dashboard. The connection defaults to `https://api.hindsight.vectorize.io/mcp`, so no URL is
|
||||
needed.
|
||||
|
||||
### Self-hosted
|
||||
|
||||
Point at your own server and (optionally) pick a bank:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_URL="http://localhost:8000"
|
||||
export HINDSIGHT_BANK_ID="my-project"
|
||||
export HINDSIGHT_API_KEY="…" # or pass apiKey: null below for a no-auth server
|
||||
export HINDSIGHT_MCP_URL="http://localhost:8000/mcp"
|
||||
export HINDSIGHT_MCP_BANK_ID="my-project"
|
||||
export HINDSIGHT_API_KEY="…" # or omit and pass apiKey: null below for a no-auth server
|
||||
```
|
||||
|
||||
```ts
|
||||
import { hindsightMemory } from "@vectorize-io/hindsight-eve";
|
||||
import { defineHindsightConnection } from "@vectorize-io/hindsight-eve";
|
||||
|
||||
export default hindsightMemory({ apiUrl: "http://localhost:8000", apiKey: null });
|
||||
// A local server with no auth:
|
||||
export default defineHindsightConnection({
|
||||
url: "http://localhost:8000/mcp",
|
||||
apiKey: null,
|
||||
});
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Both factories accept the same options (each falls back to its env var):
|
||||
|
||||
```ts
|
||||
hindsightMemory({
|
||||
apiUrl, // string — REST base; defaults to HINDSIGHT_API_URL, then Cloud
|
||||
defineHindsightConnection({
|
||||
url, // string — MCP endpoint; defaults to HINDSIGHT_MCP_URL, then Cloud
|
||||
apiKey, // string | null — bearer token; null = no auth (local dev)
|
||||
bankId, // string — bank to scope memory to
|
||||
recallQuery, // string — the broad query used for recall (see below)
|
||||
budget, // "low" | "mid" | "high" — recall result budget (default "mid")
|
||||
maxTokens, // number — recall token budget (default 1024)
|
||||
context, // string — `context` tag written on retained items (default "eve")
|
||||
includeAssistantReply, // boolean — also retain the assistant's reply (default false)
|
||||
timeoutMs, // number — HTTP timeout (default 15000)
|
||||
onError, // (err, phase) => void — failures degrade silently via this (default console.warn)
|
||||
bankId, // string — scope memory to a bank (X-Bank-Id header)
|
||||
description, // string — override the model-facing description
|
||||
tools, // { allow } | { block } — narrow which Hindsight tools the model sees
|
||||
approval, // human-in-the-loop policy, e.g. once() from "eve/tools/approval"
|
||||
});
|
||||
```
|
||||
|
||||
## Recall is profile-based, not per-message
|
||||
Restrict the agent to read-only recall, and require approval the first time:
|
||||
|
||||
Eve's instruction resolver runs at the start of a turn and **cannot see the live user
|
||||
message**, so recall uses a fixed broad query (default:
|
||||
`"user preferences, identity, and working context"`) to surface the user's ambient
|
||||
profile/context each turn. This is ideal for "the agent knows you" — preferences, identity,
|
||||
ongoing context — and is deterministic. Tune it with `recallQuery`. (Per-message, query-
|
||||
specific retrieval inherently needs a tool the model calls; that's out of scope here.)
|
||||
```ts
|
||||
import { defineHindsightConnection } from "@vectorize-io/hindsight-eve";
|
||||
import { once } from "eve/tools/approval";
|
||||
|
||||
## Notes
|
||||
|
||||
- Memory is scoped to a **bank** (one isolated store, e.g. per user). Point both files at the
|
||||
same `HINDSIGHT_BANK_ID`.
|
||||
- By default only the **user's** message is retained (the durable signal) — set
|
||||
`includeAssistantReply: true` to also store the assistant's reply.
|
||||
- Retains run asynchronously and never block a turn; failures degrade via `onError`.
|
||||
- The recall block injected into context is fenced with a sentinel so recalled facts are never
|
||||
re-retained.
|
||||
export default defineHindsightConnection({
|
||||
tools: { allow: ["recall", "reflect"] },
|
||||
approval: once(),
|
||||
});
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
Run your agent. Tell it a durable preference in one chat ("whenever you write me code, use
|
||||
Python with full type hints and no comments"). Start a **fresh** chat and ask for something —
|
||||
the agent applies the remembered preference, because the memory was injected before the model
|
||||
ran, with no tool call.
|
||||
With the connection in place, run your agent and ask it something it would need to look up
|
||||
("what did we decide about X last week?"). Eve's `connection__search` surfaces the Hindsight
|
||||
tools and the model calls `connection__hindsight__recall`. To seed memory, have the agent
|
||||
`retain` a fact in one session and `recall` it in the next.
|
||||
|
||||
## Links
|
||||
|
||||
- [Hindsight docs](https://hindsight.vectorize.io)
|
||||
- [Eve hooks](https://github.com/vercel/eve/blob/main/docs/guides/hooks.md) ·
|
||||
[Eve dynamic capabilities](https://github.com/vercel/eve/blob/main/docs/guides/dynamic-capabilities.md)
|
||||
- [Eve connections](https://github.com/vercel/eve/blob/main/docs/connections.mdx)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-eve",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vectorize-io/hindsight-eve",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"vitest": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eve": ">=0.11.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-eve",
|
||||
"version": "0.2.0",
|
||||
"description": "Automatic long-term memory for Vercel Eve agents — Hindsight memory injected before each turn and retained after, with no model tool-calling",
|
||||
"version": "0.1.0",
|
||||
"description": "Hindsight long-term memory for Vercel Eve agents - a one-line MCP connection exposing retain, recall, and reflect",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
@@ -15,7 +15,7 @@
|
||||
"eve",
|
||||
"vercel",
|
||||
"agents",
|
||||
"hooks",
|
||||
"mcp",
|
||||
"memory",
|
||||
"hindsight",
|
||||
"llm",
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { hindsightAutoRecall, hindsightRetainHook } from "./auto-memory";
|
||||
import { SENTINEL_OPEN } from "./client";
|
||||
|
||||
const OPTS = { apiUrl: "http://test", apiKey: "k", bankId: "b" };
|
||||
const CTX = { session: { id: "s1" }, channel: { kind: "web" } } as unknown;
|
||||
|
||||
/** Mock fetch, routing by URL; returns recall results or a retain ack. */
|
||||
function mockFetch(recallResults: unknown[] = []): ReturnType<typeof vi.fn> {
|
||||
const fn = vi.fn(async (url: string) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => (url.includes("/recall") ? { results: recallResults } : { success: true }),
|
||||
text: async () => "",
|
||||
}));
|
||||
vi.stubGlobal("fetch", fn);
|
||||
return fn;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handlers = (def: { events: unknown }): any => def.events;
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("hindsightRetainHook", () => {
|
||||
it("retains the user's message on turn.completed (user-only by default)", async () => {
|
||||
const fetchFn = mockFetch();
|
||||
const ev = handlers(hindsightRetainHook(OPTS));
|
||||
|
||||
ev["message.received"]({ data: { turnId: "t1", message: "I prefer tabs" } });
|
||||
ev["message.completed"]({ data: { turnId: "t1", message: "Got it.", finishReason: "stop" } });
|
||||
await ev["turn.completed"]({ data: { turnId: "t1" } }, CTX);
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetchFn.mock.calls[0];
|
||||
expect(url).toBe("http://test/v1/default/banks/b/memories");
|
||||
const body = JSON.parse(init.body);
|
||||
expect(body.async).toBe(true);
|
||||
expect(body.items[0].content).toBe("User: I prefer tabs");
|
||||
expect(body.items[0].context).toBe("eve");
|
||||
expect(body.items[0].metadata).toMatchObject({ sessionId: "s1", turnId: "t1", channel: "web" });
|
||||
});
|
||||
|
||||
it("includes the assistant reply when includeAssistantReply is set", async () => {
|
||||
const fetchFn = mockFetch();
|
||||
const ev = handlers(hindsightRetainHook({ ...OPTS, includeAssistantReply: true }));
|
||||
ev["message.received"]({ data: { turnId: "t1", message: "I prefer tabs" } });
|
||||
ev["message.completed"]({ data: { turnId: "t1", message: "Got it.", finishReason: "stop" } });
|
||||
await ev["turn.completed"]({ data: { turnId: "t1" } }, CTX);
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body).items[0].content).toBe(
|
||||
"User: I prefer tabs\n\nAssistant: Got it."
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores non-terminal assistant steps (finishReason !== 'stop')", async () => {
|
||||
const fetchFn = mockFetch();
|
||||
const ev = handlers(hindsightRetainHook(OPTS));
|
||||
ev["message.received"]({ data: { turnId: "t1", message: "hi" } });
|
||||
ev["message.completed"]({
|
||||
data: { turnId: "t1", message: "calling tool", finishReason: "tool-calls" },
|
||||
});
|
||||
await ev["turn.completed"]({ data: { turnId: "t1" } }, CTX);
|
||||
// still retains (user text present), but content has no assistant half
|
||||
expect(JSON.parse(fetchFn.mock.calls[0][1].body).items[0].content).toBe("User: hi");
|
||||
});
|
||||
|
||||
it("does not retain a turn with no user message", async () => {
|
||||
const fetchFn = mockFetch();
|
||||
const ev = handlers(hindsightRetainHook(OPTS));
|
||||
ev["message.completed"]({ data: { turnId: "t1", message: "orphan", finishReason: "stop" } });
|
||||
await ev["turn.completed"]({ data: { turnId: "t1" } }, CTX);
|
||||
expect(fetchFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("never throws on a retain failure (degrades via onError)", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({}),
|
||||
text: async () => "boom",
|
||||
}))
|
||||
);
|
||||
const onError = vi.fn();
|
||||
const ev = handlers(hindsightRetainHook({ ...OPTS, onError }));
|
||||
ev["message.received"]({ data: { turnId: "t1", message: "x" } });
|
||||
await expect(ev["turn.completed"]({ data: { turnId: "t1" } }, CTX)).resolves.toBeUndefined();
|
||||
expect(onError).toHaveBeenCalledWith(expect.anything(), "retain");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hindsightAutoRecall", () => {
|
||||
it("recalls and returns injected instructions containing the memories", async () => {
|
||||
const fetchFn = mockFetch([{ id: "1", text: "prefers Python" }]);
|
||||
const ev = handlers(hindsightAutoRecall(OPTS));
|
||||
const result = await ev["turn.started"]({ data: { turnId: "t1" } }, CTX);
|
||||
|
||||
const [url, init] = fetchFn.mock.calls[0];
|
||||
expect(url).toBe("http://test/v1/default/banks/b/memories/recall");
|
||||
expect(JSON.parse(init.body).query).toBe("user preferences, identity, and working context");
|
||||
expect(result.markdown).toContain(SENTINEL_OPEN);
|
||||
expect(result.markdown).toContain("- prefers Python");
|
||||
});
|
||||
|
||||
it("returns undefined when there is nothing to recall", async () => {
|
||||
mockFetch([]);
|
||||
const ev = handlers(hindsightAutoRecall(OPTS));
|
||||
expect(await ev["turn.started"]({ data: { turnId: "t1" } }, CTX)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined and reports onError on a recall failure", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => ({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({}),
|
||||
text: async () => "boom",
|
||||
}))
|
||||
);
|
||||
const onError = vi.fn();
|
||||
const ev = handlers(hindsightAutoRecall({ ...OPTS, onError }));
|
||||
expect(await ev["turn.started"]({ data: { turnId: "t1" } }, CTX)).toBeUndefined();
|
||||
expect(onError).toHaveBeenCalledWith(expect.anything(), "recall");
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* Automatic, no-tool long-term memory for Vercel Eve agents, backed by
|
||||
* Hindsight's REST API. Two authored files give an agent memory that works
|
||||
* without the model ever choosing to call a tool:
|
||||
*
|
||||
* ```ts
|
||||
* // agent/instructions/hindsight.ts — recall: inject memory before each turn
|
||||
* import { hindsightMemory } from "@vectorize-io/hindsight-eve";
|
||||
* export default hindsightMemory();
|
||||
*
|
||||
* // agent/hooks/hindsight.ts — retain: save each exchange after the turn
|
||||
* import { hindsightRetainHook } from "@vectorize-io/hindsight-eve";
|
||||
* export default hindsightRetainHook();
|
||||
* ```
|
||||
*
|
||||
* This module is the only one that imports `eve`. The HTTP client and config
|
||||
* resolution are kept pure (in `./client` and `./config`) so they unit-test
|
||||
* without the framework.
|
||||
*/
|
||||
import { defineHook, type HookDefinition } from "eve/hooks";
|
||||
import { defineDynamic, defineInstructions, type DynamicSentinel } from "eve/instructions";
|
||||
|
||||
import { HindsightRestClient, buildRecallMarkdown } from "./client.js";
|
||||
import {
|
||||
buildRetainContent,
|
||||
recordAssistantMessage,
|
||||
recordUserMessage,
|
||||
resolveAutoMemory,
|
||||
takeTurn,
|
||||
type AutoMemoryOptions,
|
||||
type TurnBuffer,
|
||||
} from "./config.js";
|
||||
|
||||
export type { AutoMemoryOptions } from "./config.js";
|
||||
|
||||
/**
|
||||
* Inject the user's stored memory as a system message before each turn.
|
||||
* Drop the returned value as the default export of `agent/instructions/hindsight.ts`.
|
||||
*
|
||||
* Recall uses a fixed broad query (not the live message — eve's instruction
|
||||
* resolver can't see it), which surfaces the user's ambient profile/context.
|
||||
* Tune it with `recallQuery`.
|
||||
*/
|
||||
export function hindsightAutoRecall(options: AutoMemoryOptions = {}): DynamicSentinel {
|
||||
const cfg = resolveAutoMemory(options);
|
||||
const client = new HindsightRestClient(cfg.apiUrl, cfg.apiKey, cfg.timeoutMs);
|
||||
|
||||
return defineDynamic({
|
||||
events: {
|
||||
"turn.started": async (): Promise<unknown> => {
|
||||
try {
|
||||
const { results } = await client.recall(cfg.bankId, cfg.recallQuery, {
|
||||
budget: cfg.budget,
|
||||
maxTokens: cfg.maxTokens,
|
||||
});
|
||||
if (results.length === 0) return undefined;
|
||||
return defineInstructions({ markdown: buildRecallMarkdown(results) });
|
||||
} catch (error) {
|
||||
cfg.onError(error, "recall");
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Primary name for {@link hindsightAutoRecall} — the memory-injection half. */
|
||||
export const hindsightMemory = hindsightAutoRecall;
|
||||
|
||||
/**
|
||||
* Retain each completed exchange to Hindsight. Drop the returned value as the
|
||||
* default export of `agent/hooks/hindsight.ts`.
|
||||
*
|
||||
* Pairs the user message (`message.received`) with the final assistant answer
|
||||
* (`message.completed` where `finishReason === "stop"`) by `turnId`, then
|
||||
* retains on `turn.completed`. All side effects are guarded — a failure warns
|
||||
* via `onError` and never breaks the turn.
|
||||
*/
|
||||
export function hindsightRetainHook(options: AutoMemoryOptions = {}): HookDefinition {
|
||||
const cfg = resolveAutoMemory(options);
|
||||
const client = new HindsightRestClient(cfg.apiUrl, cfg.apiKey, cfg.timeoutMs);
|
||||
const buffer: TurnBuffer = new Map();
|
||||
|
||||
return defineHook({
|
||||
events: {
|
||||
"message.received": (event) => {
|
||||
recordUserMessage(buffer, event.data.turnId, event.data.message);
|
||||
},
|
||||
"message.completed": (event) => {
|
||||
// Only the terminal assistant text; intermediate steps end in "tool-calls".
|
||||
if (event.data.finishReason === "stop" && event.data.message) {
|
||||
recordAssistantMessage(buffer, event.data.turnId, event.data.message);
|
||||
}
|
||||
},
|
||||
"turn.completed": async (event, ctx) => {
|
||||
try {
|
||||
const content = buildRetainContent(
|
||||
takeTurn(buffer, event.data.turnId),
|
||||
cfg.includeAssistantReply
|
||||
);
|
||||
if (content === null) return;
|
||||
const metadata: Record<string, string> = {
|
||||
sessionId: ctx.session.id,
|
||||
turnId: event.data.turnId,
|
||||
};
|
||||
if (ctx.channel.kind) metadata.channel = ctx.channel.kind;
|
||||
await client.retain(
|
||||
cfg.bankId,
|
||||
[{ content, context: cfg.context, metadata, timestamp: new Date().toISOString() }],
|
||||
{ async: true }
|
||||
);
|
||||
} catch (error) {
|
||||
cfg.onError(error, "retain");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||