From 18f9e510aa0410d7903c3517adf8249a64df2537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Mon, 29 Jun 2026 11:31:35 +0200 Subject: [PATCH 1/6] feat(knowledge-base): self-curating knowledge base (OKF pages + folder missions) Server-side knowledge base: a hierarchy of folders and pages over mental models, projected to the Open Knowledge Format, with a mission-driven curator that maintains pages automatically after each consolidation. - knowledge_pages table (PG + Oracle): parent_id tree, kind folder/page, mission, managed, last_curated_at; partial unique index on (folder, name) for concurrency-safe dedup; added to BACKUP_TABLES. - api/okf.py: OKF serializer (frontmatter + body, index/log, constellation graph). - engine/knowledge_curator.py: folder curator (LLM op plan + safe apply); reads new memories since last curation (delta, not recall); ops create/merge/delete page + spawn sub-folder (bounded depth<=3, <=8). Runs as an async curate_folder task on folder/mission create and after consolidation. Curator pages use an observation-only delta trigger with exclude_mental_models. - MemoryEngine: folder/page CRUD, tree, curate, async submit + worker handler. - /v1/default/banks/{bank}/knowledge-base/* endpoints. - Control plane: knowledge-base tree view + constellation toggle, missions, OKF page panel + bundle export; proxies, client, sidebar, i18n. - Tests: okf unit, knowledge-base HTTP, curator apply + dedup guard, hs_llm_core e2e. - Regenerated OpenAPI + SDK clients + docs-skill. --- hindsight-api-slim/hindsight_api/admin/cli.py | 1 + ...7d8e9f0_knowledge_pages_mission_managed.py | 58 + .../a9b8c7d6e5f4_add_knowledge_pages.py | 110 + ...3e4f5a6_knowledge_pages_last_curated_at.py | 52 + ...f6a7b8_knowledge_pages_unique_page_name.py | 71 + hindsight-api-slim/hindsight_api/api/http.py | 500 ++++ hindsight-api-slim/hindsight_api/api/okf.py | 263 ++ .../engine/consolidation/consolidator.py | 11 + .../hindsight_api/engine/knowledge_curator.py | 375 +++ .../hindsight_api/engine/memory_engine.py | 479 ++++ .../tests/test_knowledge_base.py | 215 ++ .../tests/test_knowledge_curator.py | 207 ++ .../tests/test_knowledge_curator_e2e.py | 81 + .../tests/test_knowledge_curator_llm.py | 53 + hindsight-api-slim/tests/test_okf.py | 166 ++ hindsight-clients/go/api/openapi.yaml | 707 +++++ hindsight-clients/go/api_knowledge_base.go | 1045 +++++++ hindsight-clients/go/client.go | 3 + .../go/model_create_folder_request.go | 250 ++ .../model_create_knowledge_page_response.go | 232 ++ .../go/model_create_page_request.go | 361 +++ hindsight-clients/go/model_knowledge_node.go | 557 ++++ .../go/model_knowledge_page_bundle_file.go | 186 ++ .../model_knowledge_page_bundle_response.go | 158 ++ .../go/model_knowledge_page_graph_response.go | 242 ++ .../go/model_knowledge_page_response.go | 418 +++ .../go/model_knowledge_tree_response.go | 158 ++ .../go/model_update_node_request.go | 228 ++ .../python/.openapi-generator/FILES | 11 + .../python/hindsight_client_api/__init__.py | 11 + .../hindsight_client_api/api/__init__.py | 1 + .../api/knowledge_base_api.py | 2399 +++++++++++++++++ .../hindsight_client_api/models/__init__.py | 10 + .../models/create_folder_request.py | 101 + .../models/create_knowledge_page_response.py | 96 + .../models/create_page_request.py | 121 + .../models/knowledge_node.py | 148 + .../models/knowledge_page_bundle_file.py | 89 + .../models/knowledge_page_bundle_response.py | 95 + .../models/knowledge_page_graph_response.py | 93 + .../models/knowledge_page_response.py | 116 + .../models/knowledge_tree_response.py | 95 + .../models/update_node_request.py | 106 + .../typescript/generated/sdk.gen.ts | 156 ++ .../typescript/generated/types.gen.ts | 582 ++++ .../src/app/[locale]/banks/[bankId]/page.tsx | 10 +- .../app/api/knowledge-base/export/route.ts | 35 + .../app/api/knowledge-base/folders/route.ts | 38 + .../src/app/api/knowledge-base/graph/route.ts | 35 + .../knowledge-base/nodes/[nodeId]/route.ts | 92 + .../knowledge-base/pages/[pageId]/route.ts | 43 + .../src/app/api/knowledge-base/pages/route.ts | 38 + .../src/app/api/knowledge-base/tree/route.ts | 35 + .../src/components/knowledge-base-view.tsx | 772 ++++++ .../src/components/sidebar.tsx | 4 +- hindsight-control-plane/src/lib/api.ts | 123 + hindsight-control-plane/src/messages/de.json | 51 +- hindsight-control-plane/src/messages/en.json | 51 +- hindsight-control-plane/src/messages/es.json | 51 +- hindsight-control-plane/src/messages/fr.json | 51 +- hindsight-control-plane/src/messages/ja.json | 51 +- hindsight-control-plane/src/messages/ko.json | 51 +- hindsight-control-plane/src/messages/pt.json | 51 +- .../src/messages/yue-Hant.json | 51 +- .../src/messages/zh-CN.json | 51 +- .../src/messages/zh-TW.json | 51 +- hindsight-docs/static/openapi.json | 977 ++++++- skills/hindsight-docs/references/openapi.json | 977 ++++++- 68 files changed, 15088 insertions(+), 18 deletions(-) create mode 100644 hindsight-api-slim/hindsight_api/alembic/versions/a5b6c7d8e9f0_knowledge_pages_mission_managed.py create mode 100644 hindsight-api-slim/hindsight_api/alembic/versions/a9b8c7d6e5f4_add_knowledge_pages.py create mode 100644 hindsight-api-slim/hindsight_api/alembic/versions/b1c2d3e4f5a6_knowledge_pages_last_curated_at.py create mode 100644 hindsight-api-slim/hindsight_api/alembic/versions/c3d4e5f6a7b8_knowledge_pages_unique_page_name.py create mode 100644 hindsight-api-slim/hindsight_api/api/okf.py create mode 100644 hindsight-api-slim/hindsight_api/engine/knowledge_curator.py create mode 100644 hindsight-api-slim/tests/test_knowledge_base.py create mode 100644 hindsight-api-slim/tests/test_knowledge_curator.py create mode 100644 hindsight-api-slim/tests/test_knowledge_curator_e2e.py create mode 100644 hindsight-api-slim/tests/test_knowledge_curator_llm.py create mode 100644 hindsight-api-slim/tests/test_okf.py create mode 100644 hindsight-clients/go/api_knowledge_base.go create mode 100644 hindsight-clients/go/model_create_folder_request.go create mode 100644 hindsight-clients/go/model_create_knowledge_page_response.go create mode 100644 hindsight-clients/go/model_create_page_request.go create mode 100644 hindsight-clients/go/model_knowledge_node.go create mode 100644 hindsight-clients/go/model_knowledge_page_bundle_file.go create mode 100644 hindsight-clients/go/model_knowledge_page_bundle_response.go create mode 100644 hindsight-clients/go/model_knowledge_page_graph_response.go create mode 100644 hindsight-clients/go/model_knowledge_page_response.go create mode 100644 hindsight-clients/go/model_knowledge_tree_response.go create mode 100644 hindsight-clients/go/model_update_node_request.go create mode 100644 hindsight-clients/python/hindsight_client_api/api/knowledge_base_api.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/create_folder_request.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/create_knowledge_page_response.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/create_page_request.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/knowledge_node.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_file.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_response.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/knowledge_page_graph_response.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/knowledge_page_response.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/knowledge_tree_response.py create mode 100644 hindsight-clients/python/hindsight_client_api/models/update_node_request.py create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/export/route.ts create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/folders/route.ts create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/graph/route.ts create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/nodes/[nodeId]/route.ts create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/pages/[pageId]/route.ts create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/pages/route.ts create mode 100644 hindsight-control-plane/src/app/api/knowledge-base/tree/route.ts create mode 100644 hindsight-control-plane/src/components/knowledge-base-view.tsx diff --git a/hindsight-api-slim/hindsight_api/admin/cli.py b/hindsight-api-slim/hindsight_api/admin/cli.py index 8403c1102e..49c1e900ba 100644 --- a/hindsight-api-slim/hindsight_api/admin/cli.py +++ b/hindsight-api-slim/hindsight_api/admin/cli.py @@ -56,6 +56,7 @@ "observation_history", "mental_models", "mental_model_history", + "knowledge_pages", "directives", "async_operations", "webhooks", diff --git a/hindsight-api-slim/hindsight_api/alembic/versions/a5b6c7d8e9f0_knowledge_pages_mission_managed.py b/hindsight-api-slim/hindsight_api/alembic/versions/a5b6c7d8e9f0_knowledge_pages_mission_managed.py new file mode 100644 index 0000000000..ae10736782 --- /dev/null +++ b/hindsight-api-slim/hindsight_api/alembic/versions/a5b6c7d8e9f0_knowledge_pages_mission_managed.py @@ -0,0 +1,58 @@ +"""Add mission + managed to knowledge_pages (folder curator). + +Folders gain a ``mission`` — the steering prompt a curator uses after each +consolidation to decide which pages should exist under that folder. Pages gain a +``managed`` flag: curator-created pages are ``managed = true`` (the curator may +merge/delete them), human-created pages stay ``managed = false`` (pinned — the +curator never touches them). + +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 mission TEXT") + 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") + op.execute(f"ALTER TABLE {schema}knowledge_pages DROP COLUMN IF EXISTS mission") + + +def _oracle_upgrade() -> None: + op.execute("ALTER TABLE knowledge_pages ADD (mission CLOB)") + 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") + op.execute("ALTER TABLE knowledge_pages DROP COLUMN mission") + + +def upgrade() -> None: + run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade) + + +def downgrade() -> None: + run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade) diff --git a/hindsight-api-slim/hindsight_api/alembic/versions/a9b8c7d6e5f4_add_knowledge_pages.py b/hindsight-api-slim/hindsight_api/alembic/versions/a9b8c7d6e5f4_add_knowledge_pages.py new file mode 100644 index 0000000000..6a744f3c21 --- /dev/null +++ b/hindsight-api-slim/hindsight_api/alembic/versions/a9b8c7d6e5f4_add_knowledge_pages.py @@ -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) diff --git a/hindsight-api-slim/hindsight_api/alembic/versions/b1c2d3e4f5a6_knowledge_pages_last_curated_at.py b/hindsight-api-slim/hindsight_api/alembic/versions/b1c2d3e4f5a6_knowledge_pages_last_curated_at.py new file mode 100644 index 0000000000..3a51c73e44 --- /dev/null +++ b/hindsight-api-slim/hindsight_api/alembic/versions/b1c2d3e4f5a6_knowledge_pages_last_curated_at.py @@ -0,0 +1,52 @@ +"""Add last_curated_at watermark to knowledge_pages folders. + +The folder curator looks at the memories created since it last ran (a delta, +like the mental-model refresh's ``created_after`` scope) rather than a semantic +recall. ``last_curated_at`` is that per-folder watermark. + +Revision ID: b1c2d3e4f5a6 +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 = "b1c2d3e4f5a6" +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() + op.execute(f"ALTER TABLE {schema}knowledge_pages ADD COLUMN IF NOT EXISTS last_curated_at TIMESTAMP WITH TIME ZONE") + + +def _pg_downgrade() -> None: + schema = _pg_schema_prefix() + op.execute(f"ALTER TABLE {schema}knowledge_pages DROP COLUMN IF EXISTS last_curated_at") + + +def _oracle_upgrade() -> None: + op.execute("ALTER TABLE knowledge_pages ADD (last_curated_at TIMESTAMP WITH TIME ZONE)") + + +def _oracle_downgrade() -> None: + op.execute("ALTER TABLE knowledge_pages DROP COLUMN last_curated_at") + + +def upgrade() -> None: + run_for_dialect(pg=_pg_upgrade, oracle=_oracle_upgrade) + + +def downgrade() -> None: + run_for_dialect(pg=_pg_downgrade, oracle=_oracle_downgrade) diff --git a/hindsight-api-slim/hindsight_api/alembic/versions/c3d4e5f6a7b8_knowledge_pages_unique_page_name.py b/hindsight-api-slim/hindsight_api/alembic/versions/c3d4e5f6a7b8_knowledge_pages_unique_page_name.py new file mode 100644 index 0000000000..223b94a11c --- /dev/null +++ b/hindsight-api-slim/hindsight_api/alembic/versions/c3d4e5f6a7b8_knowledge_pages_unique_page_name.py @@ -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: b1c2d3e4f5a6 +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 = "b1c2d3e4f5a6" +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) diff --git a/hindsight-api-slim/hindsight_api/api/http.py b/hindsight-api-slim/hindsight_api/api/http.py index ca12c4c070..40839d6694 100644 --- a/hindsight-api-slim/hindsight_api/api/http.py +++ b/hindsight-api-slim/hindsight_api/api/http.py @@ -18,6 +18,7 @@ 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,154 @@ 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. + + Folders carry a ``mission`` (the curator's steering prompt); pages carry + ``description``/``tags`` from their backing mental model and a ``managed`` + flag (true = curator-managed, false = pinned/human). + """ + + 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).") + mission: str | None = Field(default=None, description="Curator mission (folders only).") + managed: bool = Field(default=False, description="True for curator-managed nodes; false for pinned/human.") + 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 + mission: str | None = Field(default=None, description="Curator mission for the folder.") + + +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, move, and/or set a folder's mission. Each field applies only when present.""" + + name: str | None = None + parent_id: str | None = None + mission: 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:` 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"), + mission=node.get("mission"), + 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 +4930,357 @@ async def api_delete_mental_model( 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, + mission=body.mission, + request_context=request_context, + ) + # A new folder with a mission curates itself in the background, so its + # pages populate from existing memories without waiting for the next + # consolidation. Best-effort — scheduling must not fail folder creation. + if node.get("mission"): + try: + await app.state.memory.submit_async_curate_folder( + bank_id=bank_id, folder_id=node["id"], request_context=request_context + ) + except Exception as curate_err: + logger.warning(f"Failed to schedule curation for new folder {node['id']}: {curate_err}") + 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 .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 "mission" in body.model_fields_set: + did_change = True + updated = await app.state.memory.set_folder_mission( + bank_id=bank_id, folder_id=node_id, mission=body.mission, request_context=request_context + ) + # Setting/changing a mission re-curates the folder in the background. + if updated and updated.get("mission"): + try: + await app.state.memory.submit_async_curate_folder( + bank_id=bank_id, folder_id=node_id, request_context=request_context + ) + except Exception as curate_err: + logger.warning(f"Failed to schedule curation for folder {node_id}: {curate_err}") + if not did_change: + raise HTTPException(status_code=400, detail="Provide name, parent_id, and/or mission 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 # ========================================================================= diff --git a/hindsight-api-slim/hindsight_api/api/okf.py b/hindsight-api-slim/hindsight_api/api/okf.py new file mode 100644 index 0000000000..66068391a6 --- /dev/null +++ b/hindsight-api-slim/hindsight_api/api/okf.py @@ -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:`` 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:`` 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) diff --git a/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py b/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py index c525253fe3..53c90bead5 100644 --- a/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py +++ b/hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py @@ -1352,6 +1352,17 @@ def _fmt(key: str) -> str: ) stats["mental_models_refreshed"] = mental_models_refreshed + # Knowledge-base folders re-curate themselves after consolidation, on the + # same hook as the mental-model refresh: each mission-bearing folder gets + # its own background curate_folder task. Banks without knowledge-base + # folders no-op. Best-effort — scheduling must never break consolidation. + try: + stats["folders_curation_scheduled"] = await memory_engine.submit_async_curate_bank_folders( + bank_id=bank_id, request_context=request_context + ) + except Exception as e: + logger.warning("Knowledge-base curation scheduling failed for %s: %s", bank_id, e) + perf.flush() return {"status": "completed", "bank_id": bank_id, **stats} diff --git a/hindsight-api-slim/hindsight_api/engine/knowledge_curator.py b/hindsight-api-slim/hindsight_api/engine/knowledge_curator.py new file mode 100644 index 0000000000..6ea3e225f0 --- /dev/null +++ b/hindsight-api-slim/hindsight_api/engine/knowledge_curator.py @@ -0,0 +1,375 @@ +"""Folder curator: mission-driven page maintenance for the knowledge base. + +After a consolidation, a folder that has a ``mission`` recalls the memories +relevant to that mission, looks at its current pages, and asks the LLM to emit a +small set of ops — create a page, merge pages, delete a page, or spawn a +sub-folder. Curators are **independent**: each folder pulls over the bank's +memories on its own (the tree is for organization, not routing), so a memory can +surface in more than one folder. + +Safety: only ``managed`` pages (curator-created) are ever merged or deleted; +human-authored (pinned) pages are never touched. Sub-folder spawning is bounded +by depth and count. Page *content* is still synthesized by the normal mental- +model refresh — the curator only decides which pages should exist. +""" + +from __future__ import annotations + +import json +import logging +from datetime import datetime +from typing import TYPE_CHECKING, Any, Literal + +from pydantic import BaseModel, Field + +if TYPE_CHECKING: + from ..models import RequestContext + from .memory_engine import MemoryEngine + +logger = logging.getLogger(__name__) + +# Bounds (kept conservative; the curator should converge, not churn). +MAX_DEPTH = 3 +MAX_SUBFOLDERS = 8 +DEFAULT_MAX_OPS = 8 +# How many new memories to feed the curator per run (newest first). The curator +# reads the delta — memories created since the folder's last curation — not a +# semantic recall, so this caps the LLM context, not relevance. +CURATOR_MAX_MEMORIES = 200 +MAX_FOLDERS_PER_RUN = 20 + +# Trigger for curator-created pages: each page is a living document synthesized +# from the consolidated **observations** (not raw facts), refreshed incrementally +# (delta) after each consolidation, and excluding other mental models so a page +# never reflects on its siblings (which produced the "based_on: mental-models" +# / "no information" pollution we saw). +CURATOR_PAGE_TRIGGER = { + "mode": "delta", + "fact_types": ["observation"], + "exclude_mental_models": True, + "refresh_after_consolidation": True, +} + +CURATOR_SYSTEM_PROMPT = """\ +You curate ONE folder of a knowledge base. The folder has a MISSION that defines exactly +what belongs in it. You are given the folder's CURRENT PAGES (which already exist) and the +NEW MEMORIES added since the last curation. Output the minimal set of operations. + +Work in two steps: +1. FILTER: keep only the new memories that clearly advance THIS folder's mission. Discard + everything else — a memory about another topic is NOT this folder's job, even if it is + interesting. If a memory does not fit the mission, ignore it completely. +2. For the kept memories, choose operations. + +Operations (emit JSON {{"operations": [...]}}; at most {max_ops}): +- create_page: a mission-relevant topic that is NOT already one of the CURRENT PAGES. + Provide "name" and "source_query" (the question that rebuilds the page). +- merge_pages: two CURRENT PAGES cover the same topic. Provide "page_ids" + the "name" + and "source_query" of the single replacement. +- delete_page: a CURRENT PAGE no longer fits the mission. Provide "page_id" + "reason". +- create_subfolder: the topics split into a clear sub-theme. Provide "name" + "mission". + +Hard rules: +- ON MISSION ONLY. Do NOT create a page for a memory that doesn't fit the mission. Creating + an off-topic page is the worst mistake you can make. +- NO DUPLICATES. Before creating, check CURRENT PAGES — if the topic is already there, + do NOT recreate it. Never emit two create_page ops for the same subject in one response. +- One page = one topic. Merge overlapping topics instead of adding near-duplicates. +- Be conservative: when unsure whether something fits or is already covered, do nothing. +- Ground every page in the given memories; never invent facts. Use the exact page ids shown. +- Return {{"operations": []}} when the folder already reflects the new memories. +""" + + +def _parse_dt(value: Any) -> datetime | None: + """Parse an ISO timestamp (or pass through a datetime); None on failure/empty.""" + if not value: + return None + if isinstance(value, datetime): + return value + if isinstance(value, str): + try: + return datetime.fromisoformat(value) + except ValueError: + return None + return None + + +class CuratorOp(BaseModel): + """One curator operation. Flat schema (no discriminated union) for provider portability.""" + + action: Literal["create_page", "merge_pages", "delete_page", "create_subfolder"] + name: str | None = Field(default=None, description="Title for create_page / merge result / create_subfolder") + source_query: str | None = Field(default=None, description="Question that rebuilds a page") + mission: str | None = Field(default=None, description="Mission for a spawned sub-folder") + page_ids: list[str] = Field(default_factory=list, description="Pages to merge") + page_id: str | None = Field(default=None, description="Page to delete") + reason: str | None = Field(default=None, description="Short justification") + + +class CuratorPlan(BaseModel): + """The LLM's proposed set of operations for a folder.""" + + operations: list[CuratorOp] = Field(default_factory=list) + + +class CuratorAction(BaseModel): + """An applied or skipped op, for the result audit trail.""" + + action: str + target: str | None = None + detail: str | None = None + + +class CuratorResult(BaseModel): + """Outcome of curating one folder.""" + + folder_id: str + mission: str | None = None + applied: list[CuratorAction] = Field(default_factory=list) + skipped: list[CuratorAction] = Field(default_factory=list) + + +def parse_plan(raw: Any) -> CuratorPlan: + """Coerce an LLM response (model / dict / JSON string) into a CuratorPlan.""" + if isinstance(raw, CuratorPlan): + return raw + if isinstance(raw, dict): + try: + return CuratorPlan.model_validate(raw) + except Exception: + return CuratorPlan() + if isinstance(raw, str): + try: + return CuratorPlan.model_validate(json.loads(raw)) + except Exception: + return CuratorPlan() + return CuratorPlan() + + +def _depth(folder_id: str, by_id: dict[str, dict[str, Any]]) -> int: + """Depth of a node from the root (root nodes are depth 0).""" + depth = 0 + cursor = by_id.get(folder_id, {}).get("parent_id") + while cursor is not None: + depth += 1 + cursor = by_id.get(cursor, {}).get("parent_id") + return depth + + +async def apply_curator_plan( + engine: "MemoryEngine", + bank_id: str, + folder_id: str, + plan: CuratorPlan, + *, + request_context: "RequestContext", + max_ops: int = DEFAULT_MAX_OPS, + dry_run: bool = False, +) -> CuratorResult: + """Apply a curator plan to a folder, enforcing safety + bounds. + + Re-reads the folder's current state so the same logic serves both the live + curator and deterministic tests (hand-built plans). Only ``managed`` pages are + merged/deleted; sub-folder spawning is capped by depth and count. + """ + nodes = await engine.list_knowledge_nodes(bank_id, request_context=request_context) + by_id = {n["id"]: n for n in nodes} + folder = by_id.get(folder_id) + if folder is None or folder.get("kind") != "folder": + raise ValueError(f"Folder '{folder_id}' not found") + + managed_page_ids = { + n["id"] for n in nodes if n.get("parent_id") == folder_id and n["kind"] == "page" and n.get("managed") + } + child_folder_count = sum(1 for n in nodes if n.get("parent_id") == folder_id and n["kind"] == "folder") + depth = _depth(folder_id, by_id) + + # Deterministic dedup guard: small models routinely emit near-duplicate + # create_page ops (and re-create existing pages) despite the prompt, so we + # drop any create whose title collides with an existing page in this folder + # or one already created in this run. Titles are matched case-insensitively. + existing_titles = { + (n["name"] or "").strip().lower() for n in nodes if n.get("parent_id") == folder_id and n["kind"] == "page" + } + + result = CuratorResult(folder_id=folder_id, mission=folder.get("mission")) + spawned = 0 + + async def _new_page(name: str, source_query: str) -> bool: + """Create a managed page; return False if it already exists (dup name).""" + node = await engine.create_knowledge_page( + bank_id, + name, + source_query, + "Generating content...", + parent_id=folder_id, + managed=True, + trigger=dict(CURATOR_PAGE_TRIGGER), + request_context=request_context, + ) + if node is None: + # A concurrent run already created this page (DB uniqueness guard). + return False + # Generate the content in the background, like the manual create path. + # Best-effort: the page already exists; a refresh hiccup must not abort the op. + try: + await engine.submit_async_refresh_mental_model( + bank_id, node["mental_model_id"], request_context=request_context + ) + except Exception as e: + logger.warning("Refresh scheduling failed for new page %s: %s", node["id"], e) + return True + + for op in plan.operations: + if len(result.applied) >= max_ops: + result.skipped.append(CuratorAction(action=op.action, detail="max ops reached")) + continue + + if op.action == "create_page": + if not op.name or not op.source_query: + result.skipped.append(CuratorAction(action="create_page", detail="missing name/source_query")) + continue + title = op.name.strip().lower() + if title in existing_titles: + result.skipped.append(CuratorAction(action="create_page", target=op.name, detail="duplicate title")) + continue + existing_titles.add(title) + if not dry_run and not await _new_page(op.name, op.source_query): + # DB uniqueness guard rejected it (a concurrent run won the race). + result.skipped.append(CuratorAction(action="create_page", target=op.name, detail="already exists")) + continue + result.applied.append(CuratorAction(action="create_page", target=op.name, detail=op.source_query)) + + elif op.action == "delete_page": + if op.page_id not in managed_page_ids: + result.skipped.append( + CuratorAction(action="delete_page", target=op.page_id, detail="not a managed page") + ) + continue + if not dry_run: + await engine.delete_knowledge_node(bank_id, op.page_id, request_context=request_context) + managed_page_ids.discard(op.page_id) + result.applied.append(CuratorAction(action="delete_page", target=op.page_id, detail=op.reason)) + + elif op.action == "merge_pages": + mergeable = [pid for pid in op.page_ids if pid in managed_page_ids] + if len(mergeable) < 2 or not op.name or not op.source_query: + result.skipped.append( + CuratorAction(action="merge_pages", detail="need >=2 managed pages + name/source_query") + ) + continue + if not dry_run: + if not await _new_page(op.name, op.source_query): + result.skipped.append(CuratorAction(action="merge_pages", target=op.name, detail="already exists")) + continue + for pid in mergeable: + await engine.delete_knowledge_node(bank_id, pid, request_context=request_context) + for pid in mergeable: + managed_page_ids.discard(pid) + result.applied.append(CuratorAction(action="merge_pages", target=op.name, detail=",".join(mergeable))) + + elif op.action == "create_subfolder": + if not op.name or not op.mission: + result.skipped.append(CuratorAction(action="create_subfolder", detail="missing name/mission")) + continue + if depth >= MAX_DEPTH or (child_folder_count + spawned) >= MAX_SUBFOLDERS: + result.skipped.append(CuratorAction(action="create_subfolder", target=op.name, detail="bounds reached")) + continue + if not dry_run: + await engine.create_knowledge_folder( + bank_id, + op.name, + parent_id=folder_id, + mission=op.mission, + managed=True, + request_context=request_context, + ) + spawned += 1 + result.applied.append(CuratorAction(action="create_subfolder", target=op.name, detail=op.mission)) + + return result + + +async def _ask_llm_for_plan( + engine: "MemoryEngine", + bank_id: str, + mission: str, + folder_name: str, + current_pages: list[dict[str, Any]], + memories: list[str], + max_ops: int, + request_context: "RequestContext", +) -> CuratorPlan: + """Build the curator prompt and ask the bank's configured LLM for a plan.""" + resolved = await engine._config_resolver.resolve_full_config(bank_id, request_context) + llm = engine._reflect_llm_config.with_config(resolved, bank_id=bank_id, operation="curate_folder") + + pages_block = ( + "\n".join(f"- id={p['id']} | {p['name']} | source_query={p.get('source_query')}" for p in current_pages) + or "(none)" + ) + memories_block = "\n".join(f"- {m}" for m in memories) or "(no new memories)" + user = ( + f"FOLDER: {folder_name}\n" + f"MISSION (only content matching this belongs here): {mission}\n\n" + f"CURRENT PAGES — these already exist, do NOT recreate them:\n{pages_block}\n\n" + f"NEW MEMORIES — filter to the ones matching the mission, then act:\n{memories_block}\n" + ) + raw, _usage = await llm.call( + messages=[ + {"role": "system", "content": CURATOR_SYSTEM_PROMPT.format(max_ops=max_ops)}, + {"role": "user", "content": user}, + ], + response_format=CuratorPlan, + scope="curate_folder", + temperature=0.1, + max_completion_tokens=2048, + skip_validation=True, + return_usage=True, + ) + return parse_plan(raw) + + +async def curate_folder( + engine: "MemoryEngine", + bank_id: str, + folder_id: str, + *, + request_context: "RequestContext", + max_ops: int = DEFAULT_MAX_OPS, + dry_run: bool = False, +) -> CuratorResult: + """Run the curator for one folder: read new memories → LLM plan → apply. + + The curator looks at the memories created since this folder was last curated + (the delta), like the mental-model refresh — NOT a semantic recall. If nothing + new has arrived, it is a no-op. + """ + nodes = await engine.list_knowledge_nodes(bank_id, request_context=request_context) + folder = next((n for n in nodes if n["id"] == folder_id and n.get("kind") == "folder"), None) + if folder is None: + raise ValueError(f"Folder '{folder_id}' not found") + mission = folder.get("mission") + if not mission: + # No mission → nothing to curate. + return CuratorResult(folder_id=folder_id, mission=None) + + current_pages = [n for n in nodes if n.get("parent_id") == folder_id and n["kind"] == "page"] + since = _parse_dt(folder.get("last_curated_at")) + memories = await engine.list_new_memory_texts( + bank_id, since=since, limit=CURATOR_MAX_MEMORIES, request_context=request_context + ) + if not memories: + # No new memories since the last run → nothing to do. + return CuratorResult(folder_id=folder_id, mission=mission) + + plan = await _ask_llm_for_plan( + engine, bank_id, mission, folder["name"], current_pages, memories, max_ops, request_context + ) + result = await apply_curator_plan( + engine, bank_id, folder_id, plan, request_context=request_context, max_ops=max_ops, dry_run=dry_run + ) + if not dry_run: + await engine.mark_folder_curated(bank_id, folder_id, request_context=request_context) + return result diff --git a/hindsight-api-slim/hindsight_api/engine/memory_engine.py b/hindsight-api-slim/hindsight_api/engine/memory_engine.py index 9ad7ab0b54..2a5dbb1711 100644 --- a/hindsight-api-slim/hindsight_api/engine/memory_engine.py +++ b/hindsight-api-slim/hindsight_api/engine/memory_engine.py @@ -1871,6 +1871,28 @@ async def _handle_refresh_mental_model(self, task_dict: dict[str, Any]): logger.info(f"[REFRESH_MENTAL_MODEL_TASK] Completed for bank_id={bank_id}, mental_model_id={mental_model_id}") + async def _handle_curate_folder(self, task_dict: dict[str, Any]): + """Handler for curate_folder tasks — delegates to curate_folder. + + Runs the mission-driven curator for one folder in the background (queued + on folder creation and after each consolidation), mirroring how + refresh_mental_model tasks run the async refresh. + """ + bank_id = task_dict.get("bank_id") + folder_id = task_dict.get("folder_id") + if not bank_id or not folder_id: + raise ValueError("bank_id and folder_id are required for curate_folder task") + + from hindsight_api.models import RequestContext + + internal_context = RequestContext( + internal=True, + tenant_id=task_dict.get("_tenant_id"), + api_key_id=task_dict.get("_api_key_id"), + retry_count=task_dict.get("_retry_count", 0), + ) + await self.curate_folder(bank_id=bank_id, folder_id=folder_id, request_context=internal_context) + @_bind_bank_id("task_dict", key="bank_id") async def execute_task(self, task_dict: dict[str, Any]): """ @@ -1930,6 +1952,8 @@ async def execute_task(self, task_dict: dict[str, Any]): await self._handle_graph_maintenance(task_dict) elif task_type == "refresh_mental_model": await self._handle_refresh_mental_model(task_dict) + elif task_type == "curate_folder": + await self._handle_curate_folder(task_dict) elif task_type == "webhook_delivery": await self._handle_webhook_delivery(task_dict) else: @@ -11139,6 +11163,412 @@ async def delete_mental_model( 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. + + @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"], + "mission": row["mission"] if "mission" in row else None, + "managed": (row["managed"] if "managed" in row else False), + "last_curated_at": ( + row["last_curated_at"].isoformat() if "last_curated_at" in row and row["last_curated_at"] else None + ), + "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, mission, managed, " + "last_curated_at, 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.mission, kp.managed, kp.last_curated_at, 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, + mission: str | None = None, + managed: bool = False, + request_context: "RequestContext", + ) -> dict[str, Any]: + """Create a folder (a container node) in the knowledge base. + + ``mission`` is the curator's steering prompt for the folder; ``managed`` + marks folders the curator spawned itself (vs. human-created). + """ + 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, mission, managed) + VALUES ($1, $2, $3, 'folder', $4, $5, $6) + RETURNING {self._KP_COLUMNS} + """, + folder_id, + bank_id, + parent_id, + name, + mission, + 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=True`` marks a curator-created page the curator may later + merge/delete; human-created pages default to ``managed=False`` (pinned). + + Returns ``None`` when a page with the same name already exists in the same + folder (a uniqueness violation, e.g. two concurrent curator runs creating + the same page) — the caller should treat that as "already exists". + """ + 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, + trigger=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 set_folder_mission( + self, bank_id: str, folder_id: str, mission: str | None, *, request_context: "RequestContext" + ) -> dict[str, Any] | None: + """Set (or clear) a folder's curator mission.""" + 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 mission = $3, updated_at = now() + WHERE bank_id = $1 AND id = $2 AND kind = 'folder' + RETURNING {self._KP_COLUMNS} + """, + bank_id, + folder_id, + mission, + ) + return self._row_to_knowledge_node(row) if row else None + + async def list_new_memory_texts( + self, bank_id: str, *, since: datetime | None, limit: int, request_context: "RequestContext" + ) -> list[str]: + """Return source-memory texts (world/experience) created since ``since``. + + The folder curator uses this instead of a semantic recall — it sees the + new memories (the delta), like the mental-model refresh's ``created_after`` + scope. Newest first, capped at ``limit``. ``since=None`` returns the most + recent ``limit`` memories (first curation of a folder). + """ + await self._authenticate_tenant(request_context) + backend = await self._get_backend() + table = fq_table("memory_units") + async with acquire_with_retry(backend) as conn: + if since is None: + rows = await conn.fetch( + f"SELECT text FROM {table} WHERE bank_id = $1 AND fact_type IN ('world', 'experience') " + f"ORDER BY created_at DESC LIMIT $2", + bank_id, + limit, + ) + else: + rows = await conn.fetch( + f"SELECT text FROM {table} WHERE bank_id = $1 AND fact_type IN ('world', 'experience') " + f"AND created_at > $2 ORDER BY created_at DESC LIMIT $3", + bank_id, + since, + limit, + ) + return [r["text"] for r in rows] + + async def mark_folder_curated(self, bank_id: str, folder_id: str, *, request_context: "RequestContext") -> None: + """Stamp a folder's ``last_curated_at`` watermark to now (after a curation run).""" + await self._authenticate_tenant(request_context) + backend = await self._get_backend() + async with acquire_with_retry(backend) as conn: + await conn.execute( + f"UPDATE {fq_table('knowledge_pages')} SET last_curated_at = now() WHERE bank_id = $1 AND id = $2", + bank_id, + folder_id, + ) + + async def curate_folder( + self, bank_id: str, folder_id: str, *, request_context: "RequestContext", dry_run: bool = False + ): + """Run the mission-driven curator for a single folder (see knowledge_curator). + + This is the synchronous worker-side entry point (called by the + ``curate_folder`` task handler). Callers that want it to run in the + background should use :meth:`submit_async_curate_folder` instead. + + Serialized per folder: the folder-create trigger and the post-consolidation + sweep can both fire for the same folder, and without this lock they race — + both read ``last_curated_at`` before either advances it and create the same + pages twice. The lock makes the second run see the advanced watermark (and + the pages the first run created), so it no-ops. In-process only; a multi- + worker deployment would also need a DB advisory lock. + """ + await self._authenticate_tenant(request_context) + from .knowledge_curator import curate_folder as _curate_folder + + if not hasattr(self, "_curate_locks"): + self._curate_locks: dict[tuple[str, str], asyncio.Lock] = {} + lock = self._curate_locks.setdefault((bank_id, folder_id), asyncio.Lock()) + async with lock: + return await _curate_folder(self, bank_id, folder_id, request_context=request_context, dry_run=dry_run) + async def compute_mental_model_is_stale( self, conn, @@ -12867,3 +13297,52 @@ async def submit_async_refresh_mental_model( result_metadata={"mental_model_id": mental_model_id, "name": mental_model["name"]}, dedupe_by_bank=False, ) + + async def submit_async_curate_folder( + self, bank_id: str, folder_id: str, *, request_context: "RequestContext" + ) -> dict[str, Any]: + """Schedule a background curation run for one knowledge-base folder. + + Returns ``{"operation_id": None}`` without scheduling when no LLM provider + is configured (curation needs the LLM, and this path is auto-triggered, so + a missing provider should be a silent no-op rather than an error). + """ + if self._llm_config.provider == "none": + return {"operation_id": None} + + await self._authenticate_tenant(request_context) + task_payload: dict[str, Any] = {"folder_id": folder_id} + if request_context.tenant_id: + task_payload["_tenant_id"] = request_context.tenant_id + if request_context.api_key_id: + task_payload["_api_key_id"] = request_context.api_key_id + + return await self._submit_async_operation( + bank_id=bank_id, + operation_type="curate_folder", + task_type="curate_folder", + task_payload=task_payload, + result_metadata={"folder_id": folder_id}, + dedupe_by_bank=False, + ) + + async def submit_async_curate_bank_folders(self, bank_id: str, *, request_context: "RequestContext") -> int: + """Schedule background curation for every mission-bearing folder (capped). + + Used by the post-consolidation hook: each folder with a mission gets its + own ``curate_folder`` task so they run independently in the background. + Returns the number of curation tasks submitted. + """ + from .knowledge_curator import MAX_FOLDERS_PER_RUN + + await self._authenticate_tenant(request_context) + nodes = await self.list_knowledge_nodes(bank_id, request_context=request_context) + folders = [n for n in nodes if n.get("kind") == "folder" and n.get("mission")][:MAX_FOLDERS_PER_RUN] + submitted = 0 + for folder in folders: + try: + await self.submit_async_curate_folder(bank_id, folder["id"], request_context=request_context) + submitted += 1 + except Exception as e: + logger.warning("Failed to schedule curation for %s/%s: %s", bank_id, folder["id"], e) + return submitted diff --git a/hindsight-api-slim/tests/test_knowledge_base.py b/hindsight-api-slim/tests/test_knowledge_base.py new file mode 100644 index 0000000000..54eccad522 --- /dev/null +++ b/hindsight-api-slim/tests/test_knowledge_base.py @@ -0,0 +1,215 @@ +"""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 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 + + async def test_create_folder_with_mission_schedules_curation(self, api_client, kb_bank): + bank_id, ids = kb_bank + # Creating a folder with a mission triggers a background curation; with the + # sync task backend it runs inline and must not fail the create. + resp = await api_client.post( + f"/v1/default/banks/{_enc(bank_id)}/knowledge-base/folders", + json={"name": "Auto", "mission": "Collect everything about orders"}, + ) + assert resp.status_code == 201, resp.text + assert resp.json()["mission"] == "Collect everything about orders" + + +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 diff --git a/hindsight-api-slim/tests/test_knowledge_curator.py b/hindsight-api-slim/tests/test_knowledge_curator.py new file mode 100644 index 0000000000..8e6b49582c --- /dev/null +++ b/hindsight-api-slim/tests/test_knowledge_curator.py @@ -0,0 +1,207 @@ +"""Tests for the knowledge-base folder curator. + +The op-application logic is deterministic and tested directly with hand-built +plans (no LLM). The model's *decision* (which ops to emit) is covered by a +separate hs_llm_core judge test in test_knowledge_curator_llm.py. +""" + +import uuid + +import pytest_asyncio + +from hindsight_api.engine import knowledge_curator as kc +from hindsight_api.engine.knowledge_curator import CuratorOp, CuratorPlan +from hindsight_api.engine.memory_engine import MemoryEngine + + +@pytest_asyncio.fixture +async def folder_bank(memory: MemoryEngine, request_context): + """A bank with one folder containing a managed page and a pinned page.""" + bank_id = f"test-kc-{uuid.uuid4().hex[:8]}" + folder = await memory.create_knowledge_folder( + bank_id, "Incidents", mission="Track payment incidents", request_context=request_context + ) + managed = await memory.create_knowledge_page( + bank_id, + "Old outage", + "What happened in the old outage?", + "# Old\n\nstale", + parent_id=folder["id"], + managed=True, + request_context=request_context, + ) + pinned = await memory.create_knowledge_page( + bank_id, + "Runbook", + "How to recover?", + "# Runbook\n\npinned", + parent_id=folder["id"], + managed=False, + request_context=request_context, + ) + yield memory, bank_id, folder["id"], managed["id"], pinned["id"] + await memory.delete_bank(bank_id, request_context=request_context) + + +def _names(nodes, parent_id): + return {n["name"] for n in nodes if n.get("parent_id") == parent_id and n["kind"] == "page"} + + +class TestApplyPlan: + async def test_create_page(self, folder_bank, request_context): + memory, bank_id, folder_id, _managed, _pinned = folder_bank + plan = CuratorPlan( + operations=[CuratorOp(action="create_page", name="Checkout outage", source_query="What happened?")] + ) + result = await kc.apply_curator_plan(memory, bank_id, folder_id, plan, request_context=request_context) + assert [a.action for a in result.applied] == ["create_page"] + nodes = await memory.list_knowledge_nodes(bank_id, request_context=request_context) + created = next(n for n in nodes if n["name"] == "Checkout outage") + assert created["managed"] is True + + async def test_delete_only_managed(self, folder_bank, request_context): + memory, bank_id, folder_id, managed_id, pinned_id = folder_bank + plan = CuratorPlan( + operations=[ + CuratorOp(action="delete_page", page_id=managed_id, reason="stale"), + CuratorOp(action="delete_page", page_id=pinned_id, reason="should be skipped"), + ] + ) + result = await kc.apply_curator_plan(memory, bank_id, folder_id, plan, request_context=request_context) + assert [a.action for a in result.applied] == ["delete_page"] + assert [a.action for a in result.skipped] == ["delete_page"] + nodes = await memory.list_knowledge_nodes(bank_id, request_context=request_context) + names = _names(nodes, folder_id) + assert "Old outage" not in names # managed → deleted + assert "Runbook" in names # pinned → untouched + + async def test_merge_pages(self, folder_bank, request_context): + memory, bank_id, folder_id, managed_id, _pinned = folder_bank + # add a second managed page so there are two to merge + second = await memory.create_knowledge_page( + bank_id, + "Old outage dup", + "dup?", + "# dup", + parent_id=folder_id, + managed=True, + request_context=request_context, + ) + plan = CuratorPlan( + operations=[ + CuratorOp( + action="merge_pages", + page_ids=[managed_id, second["id"]], + name="Outage summary", + source_query="Summarize the outages.", + ) + ] + ) + result = await kc.apply_curator_plan(memory, bank_id, folder_id, plan, request_context=request_context) + assert [a.action for a in result.applied] == ["merge_pages"] + nodes = await memory.list_knowledge_nodes(bank_id, request_context=request_context) + names = _names(nodes, folder_id) + assert "Outage summary" in names + assert "Old outage" not in names and "Old outage dup" not in names + + async def test_create_subfolder(self, folder_bank, request_context): + memory, bank_id, folder_id, _managed, _pinned = folder_bank + plan = CuratorPlan( + operations=[CuratorOp(action="create_subfolder", name="Checkout", mission="Checkout incidents only")] + ) + result = await kc.apply_curator_plan(memory, bank_id, folder_id, plan, request_context=request_context) + assert [a.action for a in result.applied] == ["create_subfolder"] + nodes = await memory.list_knowledge_nodes(bank_id, request_context=request_context) + sub = next(n for n in nodes if n["name"] == "Checkout" and n["kind"] == "folder") + assert sub["parent_id"] == folder_id + assert sub["mission"] == "Checkout incidents only" + assert sub["managed"] is True + + async def test_subfolder_depth_bound(self, folder_bank, request_context): + memory, bank_id, folder_id, _managed, _pinned = folder_bank + # Build a chain to MAX_DEPTH so a spawn at the bottom is rejected. + parent = folder_id + for i in range(kc.MAX_DEPTH): + child = await memory.create_knowledge_folder( + bank_id, f"L{i}", parent_id=parent, request_context=request_context + ) + parent = child["id"] + plan = CuratorPlan(operations=[CuratorOp(action="create_subfolder", name="TooDeep", mission="x")]) + result = await kc.apply_curator_plan(memory, bank_id, parent, plan, request_context=request_context) + assert result.applied == [] + assert any("bounds" in (a.detail or "") for a in result.skipped) + + async def test_dry_run_applies_nothing(self, folder_bank, request_context): + memory, bank_id, folder_id, managed_id, _pinned = folder_bank + plan = CuratorPlan(operations=[CuratorOp(action="delete_page", page_id=managed_id)]) + result = await kc.apply_curator_plan( + memory, bank_id, folder_id, plan, request_context=request_context, dry_run=True + ) + assert [a.action for a in result.applied] == ["delete_page"] + nodes = await memory.list_knowledge_nodes(bank_id, request_context=request_context) + assert "Old outage" in _names(nodes, folder_id) # not actually deleted + + +class TestCurateGating: + async def test_no_mission_is_noop(self, memory: MemoryEngine, request_context): + bank_id = f"test-kc-nm-{uuid.uuid4().hex[:8]}" + folder = await memory.create_knowledge_folder(bank_id, "NoMission", request_context=request_context) + result = await memory.curate_folder(bank_id, folder["id"], request_context=request_context) + assert result.applied == [] and result.skipped == [] + assert result.mission is None + await memory.delete_bank(bank_id, request_context=request_context) + + async def test_set_folder_mission(self, memory: MemoryEngine, request_context): + bank_id = f"test-kc-sm-{uuid.uuid4().hex[:8]}" + folder = await memory.create_knowledge_folder(bank_id, "F", request_context=request_context) + updated = await memory.set_folder_mission( + bank_id, folder["id"], "Collect release notes", request_context=request_context + ) + assert updated["mission"] == "Collect release notes" + await memory.delete_bank(bank_id, request_context=request_context) + + +class TestDuplicatePageGuard: + async def test_duplicate_page_name_blocked(self, memory: MemoryEngine, request_context): + """Two pages with the same name in one folder: the second is rejected (None). + + This is the deterministic guard that stops concurrent curator runs from + creating duplicate pages (e.g. two 'Anna' pages), independent of the LLM. + """ + bank_id = f"test-kc-dup-{uuid.uuid4().hex[:8]}" + folder = await memory.create_knowledge_folder( + bank_id, "people", mission="track all people", request_context=request_context + ) + first = await memory.create_knowledge_page( + bank_id, + "Anna", + "Who is Anna?", + "# Anna", + parent_id=folder["id"], + managed=True, + request_context=request_context, + ) + assert first is not None + # Same name (any case) in the same folder → blocked. + dup = await memory.create_knowledge_page( + bank_id, + "anna", + "Who is anna?", + "# anna", + parent_id=folder["id"], + managed=True, + request_context=request_context, + ) + assert dup is None, "duplicate page name should be rejected" + + nodes = await memory.list_knowledge_nodes(bank_id, request_context=request_context) + annas = [n for n in nodes if n["kind"] == "page" and n["name"].lower() == "anna"] + assert len(annas) == 1, f"expected 1 Anna page, got {len(annas)}" + + # A page with the same name in a DIFFERENT folder is allowed. + other = await memory.create_knowledge_folder(bank_id, "other", request_context=request_context) + ok = await memory.create_knowledge_page( + bank_id, "Anna", "q", "# a", parent_id=other["id"], managed=True, request_context=request_context + ) + assert ok is not None + await memory.delete_bank(bank_id, request_context=request_context) diff --git a/hindsight-api-slim/tests/test_knowledge_curator_e2e.py b/hindsight-api-slim/tests/test_knowledge_curator_e2e.py new file mode 100644 index 0000000000..3c2bf115aa --- /dev/null +++ b/hindsight-api-slim/tests/test_knowledge_curator_e2e.py @@ -0,0 +1,81 @@ +"""End-to-end curator test through the real async pipeline (no direct curate call). + +Creates a 'people' folder with a mission, retains a document, and lets the real +triggers run (folder-create curation + retain → consolidation → curation). Asserts +the resulting memory, observation, and the curator-built pages. +""" + +import uuid + +import httpx +import pytest + +from hindsight_api.api import create_app +from hindsight_api.engine.memory_engine import MemoryEngine + +pytestmark = pytest.mark.hs_llm_core + + +async def test_people_folder_builds_one_page_per_person(memory_real_llm: MemoryEngine, request_context): + memory = memory_real_llm + bank_id = f"test-people-{uuid.uuid4().hex[:8]}" + app = create_app(memory, initialize_memory=False) + transport = httpx.ASGITransport(app=app) + + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + base = f"/v1/default/banks/{bank_id}" + + # 1. Folder with a mission (this alone schedules a curation, but there are + # no memories yet). + r = await client.post( + f"{base}/knowledge-base/folders", + json={"name": "people", "mission": "track all people"}, + ) + assert r.status_code == 201, r.text + + # 2. Retain a document → extraction + consolidation + curation, all async. + r = await client.post( + f"{base}/memories", + json={"items": [{"content": "marco loves anna", "document_id": "doc1"}]}, + ) + assert r.status_code in (200, 201, 202), r.text + await memory.wait_for_background_tasks() + + # --- inspect what the pipeline produced ------------------------------- + world = await memory.list_memory_units(bank_id, fact_type="world", request_context=request_context) + experience = await memory.list_memory_units(bank_id, fact_type="experience", request_context=request_context) + observations = await memory.list_memory_units(bank_id, fact_type="observation", request_context=request_context) + tree = (await client.get(f"{base}/knowledge-base/tree")).json() + people = next(n for n in tree["roots"] if n["name"] == "people") + pages = people.get("children", []) + + print("\n--- MEMORIES (world) ---") + for m in world["items"]: + print(" ", m["text"]) + print("--- MEMORIES (experience) ---") + for m in experience["items"]: + print(" ", m["text"]) + print("--- OBSERVATIONS ---") + for m in observations["items"]: + print(" ", m["text"]) + print("--- PAGES under 'people' ---") + page_bodies = {} + for p in pages: + doc = (await client.get(f"{base}/knowledge-base/pages/{p['id']}")).json() + page_bodies[p["name"]] = doc.get("body") or "" + print(f" [{p['name']}] managed={p.get('managed')}\n {page_bodies[p['name']][:160]}") + + # --- assertions ------------------------------------------------------- + source_count = world["total"] + experience["total"] + assert source_count == 1, f"expected 1 memory, got {source_count}" + assert observations["total"] == 1, f"expected 1 observation, got {observations['total']}" + assert len(pages) == 2, f"expected 2 pages (marco, anna), got {len(pages)}: {[p['name'] for p in pages]}" + + joined_names = " ".join(p["name"].lower() for p in pages) + assert "marco" in joined_names, f"no Marco page: {[p['name'] for p in pages]}" + assert "anna" in joined_names, f"no Anna page: {[p['name'] for p in pages]}" + for name, body in page_bodies.items(): + assert body.strip(), f"page '{name}' has empty content" + assert "Generating content" not in body, f"page '{name}' content never synthesized: {body[:80]!r}" + + await memory.delete_bank(bank_id, request_context=request_context) diff --git a/hindsight-api-slim/tests/test_knowledge_curator_llm.py b/hindsight-api-slim/tests/test_knowledge_curator_llm.py new file mode 100644 index 0000000000..e25dda7b65 --- /dev/null +++ b/hindsight-api-slim/tests/test_knowledge_curator_llm.py @@ -0,0 +1,53 @@ +"""LLM-behaviour test for the folder curator's decisions. + +The curator's *mechanics* (applying ops) are covered deterministically in +test_knowledge_curator.py. This test exercises how the model interprets a folder +mission + memories and decides which pages to create — non-deterministic, so it +runs the real pipeline and judges the outcome rather than string-matching. +""" + +import uuid + +import pytest + +from hindsight_api.engine.memory_engine import MemoryEngine +from tests.llm_judge import assert_meets_criteria + +pytestmark = pytest.mark.hs_llm_core + + +async def test_curator_creates_page_for_mission_topic(memory_real_llm: MemoryEngine, request_context): + memory = memory_real_llm + bank_id = f"test-kc-llm-{uuid.uuid4().hex[:8]}" + + # Seed memories the curator should turn into a page. + await memory.retain_batch_async( + bank_id=bank_id, + contents=[ + {"content": "I adopted a golden retriever named Rex in March."}, + {"content": "Rex loves swimming and is great with kids."}, + {"content": "My favorite programming language is Rust."}, + ], + request_context=request_context, + ) + await memory.wait_for_background_tasks() + + folder = await memory.create_knowledge_folder( + bank_id, "Pets", mission="Everything about the user's pets", request_context=request_context + ) + + result = await memory.curate_folder(bank_id, folder["id"], request_context=request_context) + + assert result.applied, "curator should have created at least one page for a clear mission topic" + + summary = "\n".join(f"- {a.action}: {a.target} ({a.detail})" for a in result.applied) + await assert_meets_criteria( + response=summary, + criteria=( + "At least one created page is about the user's pet/dog Rex (the folder's mission). " + "A page about the Rust programming language would be off-mission and should NOT appear." + ), + context="Folder mission: 'Everything about the user's pets'. Memories included a dog named Rex and an unrelated note about Rust.", + ) + + await memory.delete_bank(bank_id, request_context=request_context) diff --git a/hindsight-api-slim/tests/test_okf.py b/hindsight-api-slim/tests/test_okf.py new file mode 100644 index 0000000000..f4fd31ea22 --- /dev/null +++ b/hindsight-api-slim/tests/test_okf.py @@ -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(), []) diff --git a/hindsight-clients/go/api/openapi.yaml b/hindsight-clients/go/api/openapi.yaml index ca0873f849..a54479f939 100644 --- a/hindsight-clients/go/api/openapi.yaml +++ b/hindsight-clients/go/api/openapi.yaml @@ -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 .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,46 @@ components: - content - name title: CreateDirectiveRequest + CreateFolderRequest: + description: Create a folder under an optional parent folder. + example: + mission: mission + parent_id: parent_id + name: name + properties: + name: + title: Name + type: string + parent_id: + nullable: true + type: string + mission: + 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 +5535,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 +6396,232 @@ components: source_facts: $ref: '#/components/schemas/SourceFactsIncludeOptions' title: IncludeOptions + KnowledgeNode: + description: |- + A node in the knowledge-base tree — a folder or a page. + + Folders carry a ``mission`` (the curator's steering prompt); pages carry + ``description``/``tags`` from their backing mental model and a ``managed`` + flag (true = curator-managed, false = pinned/human). + example: + mission: mission + 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 + mission: + nullable: true + type: string + managed: + default: false + description: True for curator-managed nodes; false for pinned/human. + 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:` 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: + - mission: mission + 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 + - mission: mission + 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 +7407,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 +8803,24 @@ components: trigger: $ref: '#/components/schemas/MentalModelTrigger-Input' title: UpdateMentalModelRequest + UpdateNodeRequest: + description: "Rename, move, and/or set a folder's mission. Each field applies\ + \ only when present." + example: + mission: mission + parent_id: parent_id + name: name + properties: + name: + nullable: true + type: string + parent_id: + nullable: true + type: string + mission: + nullable: true + type: string + title: UpdateNodeRequest UpdateWebhookRequest: description: Request model for updating a webhook. Only provided fields are updated. diff --git a/hindsight-clients/go/api_knowledge_base.go b/hindsight-clients/go/api_knowledge_base.go new file mode 100644 index 0000000000..e1c59ba469 --- /dev/null +++ b/hindsight-clients/go/api_knowledge_base.go @@ -0,0 +1,1045 @@ +/* +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 ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + + +// KnowledgeBaseAPIService KnowledgeBaseAPI service +type KnowledgeBaseAPIService service + +type ApiCreateKnowledgeFolderRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + createFolderRequest *CreateFolderRequest + authorization *string +} + +func (r ApiCreateKnowledgeFolderRequest) CreateFolderRequest(createFolderRequest CreateFolderRequest) ApiCreateKnowledgeFolderRequest { + r.createFolderRequest = &createFolderRequest + return r +} + +func (r ApiCreateKnowledgeFolderRequest) Authorization(authorization string) ApiCreateKnowledgeFolderRequest { + r.authorization = &authorization + return r +} + +func (r ApiCreateKnowledgeFolderRequest) Execute() (*KnowledgeNode, *http.Response, error) { + return r.ApiService.CreateKnowledgeFolderExecute(r) +} + +/* +CreateKnowledgeFolder Create a knowledge-base folder + +Create a folder, optionally nested under a parent folder. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @return ApiCreateKnowledgeFolderRequest +*/ +func (a *KnowledgeBaseAPIService) CreateKnowledgeFolder(ctx context.Context, bankId string) ApiCreateKnowledgeFolderRequest { + return ApiCreateKnowledgeFolderRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + } +} + +// Execute executes the request +// @return KnowledgeNode +func (a *KnowledgeBaseAPIService) CreateKnowledgeFolderExecute(r ApiCreateKnowledgeFolderRequest) (*KnowledgeNode, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *KnowledgeNode + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.CreateKnowledgeFolder") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/folders" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.createFolderRequest == nil { + return localVarReturnValue, nil, reportError("createFolderRequest is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + // body params + localVarPostBody = r.createFolderRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiCreateKnowledgePageRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + createPageRequest *CreatePageRequest + authorization *string +} + +func (r ApiCreateKnowledgePageRequest) CreatePageRequest(createPageRequest CreatePageRequest) ApiCreateKnowledgePageRequest { + r.createPageRequest = &createPageRequest + return r +} + +func (r ApiCreateKnowledgePageRequest) Authorization(authorization string) ApiCreateKnowledgePageRequest { + r.authorization = &authorization + return r +} + +func (r ApiCreateKnowledgePageRequest) Execute() (*CreateKnowledgePageResponse, *http.Response, error) { + return r.ApiService.CreateKnowledgePageExecute(r) +} + +/* +CreateKnowledgePage 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. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @return ApiCreateKnowledgePageRequest +*/ +func (a *KnowledgeBaseAPIService) CreateKnowledgePage(ctx context.Context, bankId string) ApiCreateKnowledgePageRequest { + return ApiCreateKnowledgePageRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + } +} + +// Execute executes the request +// @return CreateKnowledgePageResponse +func (a *KnowledgeBaseAPIService) CreateKnowledgePageExecute(r ApiCreateKnowledgePageRequest) (*CreateKnowledgePageResponse, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *CreateKnowledgePageResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.CreateKnowledgePage") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/pages" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.createPageRequest == nil { + return localVarReturnValue, nil, reportError("createPageRequest is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + // body params + localVarPostBody = r.createPageRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiDeleteKnowledgeNodeRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + nodeId string + authorization *string +} + +func (r ApiDeleteKnowledgeNodeRequest) Authorization(authorization string) ApiDeleteKnowledgeNodeRequest { + r.authorization = &authorization + return r +} + +func (r ApiDeleteKnowledgeNodeRequest) Execute() (interface{}, *http.Response, error) { + return r.ApiService.DeleteKnowledgeNodeExecute(r) +} + +/* +DeleteKnowledgeNode Delete a knowledge-base node + +Delete a folder or page and its whole subtree (pages' mental models are removed too). + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @param nodeId + @return ApiDeleteKnowledgeNodeRequest +*/ +func (a *KnowledgeBaseAPIService) DeleteKnowledgeNode(ctx context.Context, bankId string, nodeId string) ApiDeleteKnowledgeNodeRequest { + return ApiDeleteKnowledgeNodeRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + nodeId: nodeId, + } +} + +// Execute executes the request +// @return interface{} +func (a *KnowledgeBaseAPIService) DeleteKnowledgeNodeExecute(r ApiDeleteKnowledgeNodeRequest) (interface{}, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue interface{} + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.DeleteKnowledgeNode") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"node_id"+"}", url.PathEscape(parameterValueToString(r.nodeId, "nodeId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiExportKnowledgeBaseRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + authorization *string +} + +func (r ApiExportKnowledgeBaseRequest) Authorization(authorization string) ApiExportKnowledgeBaseRequest { + r.authorization = &authorization + return r +} + +func (r ApiExportKnowledgeBaseRequest) Execute() (*KnowledgePageBundleResponse, *http.Response, error) { + return r.ApiService.ExportKnowledgeBaseExecute(r) +} + +/* +ExportKnowledgeBase Export the knowledge base as an OKF bundle + +Return a portable OKF bundle: a nested index.md, one .md per page, and history logs. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @return ApiExportKnowledgeBaseRequest +*/ +func (a *KnowledgeBaseAPIService) ExportKnowledgeBase(ctx context.Context, bankId string) ApiExportKnowledgeBaseRequest { + return ApiExportKnowledgeBaseRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + } +} + +// Execute executes the request +// @return KnowledgePageBundleResponse +func (a *KnowledgeBaseAPIService) ExportKnowledgeBaseExecute(r ApiExportKnowledgeBaseRequest) (*KnowledgePageBundleResponse, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *KnowledgePageBundleResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.ExportKnowledgeBase") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/export" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiGetKnowledgeBaseGraphRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + authorization *string +} + +func (r ApiGetKnowledgeBaseGraphRequest) Authorization(authorization string) ApiGetKnowledgeBaseGraphRequest { + r.authorization = &authorization + return r +} + +func (r ApiGetKnowledgeBaseGraphRequest) Execute() (*KnowledgePageGraphResponse, *http.Response, error) { + return r.ApiService.GetKnowledgeBaseGraphExecute(r) +} + +/* +GetKnowledgeBaseGraph Knowledge-base constellation graph + +Return pages as nodes linked by shared tags, for the constellation view. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @return ApiGetKnowledgeBaseGraphRequest +*/ +func (a *KnowledgeBaseAPIService) GetKnowledgeBaseGraph(ctx context.Context, bankId string) ApiGetKnowledgeBaseGraphRequest { + return ApiGetKnowledgeBaseGraphRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + } +} + +// Execute executes the request +// @return KnowledgePageGraphResponse +func (a *KnowledgeBaseAPIService) GetKnowledgeBaseGraphExecute(r ApiGetKnowledgeBaseGraphRequest) (*KnowledgePageGraphResponse, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *KnowledgePageGraphResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.GetKnowledgeBaseGraph") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/graph" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiGetKnowledgeBaseTreeRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + authorization *string +} + +func (r ApiGetKnowledgeBaseTreeRequest) Authorization(authorization string) ApiGetKnowledgeBaseTreeRequest { + r.authorization = &authorization + return r +} + +func (r ApiGetKnowledgeBaseTreeRequest) Execute() (*KnowledgeTreeResponse, *http.Response, error) { + return r.ApiService.GetKnowledgeBaseTreeExecute(r) +} + +/* +GetKnowledgeBaseTree Get the knowledge-base tree + +Return the knowledge base as a nested tree of folders and pages. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @return ApiGetKnowledgeBaseTreeRequest +*/ +func (a *KnowledgeBaseAPIService) GetKnowledgeBaseTree(ctx context.Context, bankId string) ApiGetKnowledgeBaseTreeRequest { + return ApiGetKnowledgeBaseTreeRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + } +} + +// Execute executes the request +// @return KnowledgeTreeResponse +func (a *KnowledgeBaseAPIService) GetKnowledgeBaseTreeExecute(r ApiGetKnowledgeBaseTreeRequest) (*KnowledgeTreeResponse, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *KnowledgeTreeResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.GetKnowledgeBaseTree") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/tree" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiGetKnowledgePageRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + pageId string + authorization *string +} + +func (r ApiGetKnowledgePageRequest) Authorization(authorization string) ApiGetKnowledgePageRequest { + r.authorization = &authorization + return r +} + +func (r ApiGetKnowledgePageRequest) Execute() (*KnowledgePageResponse, *http.Response, error) { + return r.ApiService.GetKnowledgePageExecute(r) +} + +/* +GetKnowledgePage Get a knowledge-base page + +Return a single page as an OKF document (frontmatter + markdown body). + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @param pageId + @return ApiGetKnowledgePageRequest +*/ +func (a *KnowledgeBaseAPIService) GetKnowledgePage(ctx context.Context, bankId string, pageId string) ApiGetKnowledgePageRequest { + return ApiGetKnowledgePageRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + pageId: pageId, + } +} + +// Execute executes the request +// @return KnowledgePageResponse +func (a *KnowledgeBaseAPIService) GetKnowledgePageExecute(r ApiGetKnowledgePageRequest) (*KnowledgePageResponse, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *KnowledgePageResponse + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.GetKnowledgePage") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"page_id"+"}", url.PathEscape(parameterValueToString(r.pageId, "pageId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiUpdateKnowledgeNodeRequest struct { + ctx context.Context + ApiService *KnowledgeBaseAPIService + bankId string + nodeId string + updateNodeRequest *UpdateNodeRequest + authorization *string +} + +func (r ApiUpdateKnowledgeNodeRequest) UpdateNodeRequest(updateNodeRequest UpdateNodeRequest) ApiUpdateKnowledgeNodeRequest { + r.updateNodeRequest = &updateNodeRequest + return r +} + +func (r ApiUpdateKnowledgeNodeRequest) Authorization(authorization string) ApiUpdateKnowledgeNodeRequest { + r.authorization = &authorization + return r +} + +func (r ApiUpdateKnowledgeNodeRequest) Execute() (*KnowledgeNode, *http.Response, error) { + return r.ApiService.UpdateKnowledgeNodeExecute(r) +} + +/* +UpdateKnowledgeNode 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). + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param bankId + @param nodeId + @return ApiUpdateKnowledgeNodeRequest +*/ +func (a *KnowledgeBaseAPIService) UpdateKnowledgeNode(ctx context.Context, bankId string, nodeId string) ApiUpdateKnowledgeNodeRequest { + return ApiUpdateKnowledgeNodeRequest{ + ApiService: a, + ctx: ctx, + bankId: bankId, + nodeId: nodeId, + } +} + +// Execute executes the request +// @return KnowledgeNode +func (a *KnowledgeBaseAPIService) UpdateKnowledgeNodeExecute(r ApiUpdateKnowledgeNodeRequest) (*KnowledgeNode, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPatch + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *KnowledgeNode + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "KnowledgeBaseAPIService.UpdateKnowledgeNode") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}" + localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"node_id"+"}", url.PathEscape(parameterValueToString(r.nodeId, "nodeId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.updateNodeRequest == nil { + return localVarReturnValue, nil, reportError("updateNodeRequest is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.authorization != nil { + parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "") + } + // body params + localVarPostBody = r.updateNodeRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 422 { + var v HTTPValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/hindsight-clients/go/client.go b/hindsight-clients/go/client.go index 8fd1031144..e326af8ccd 100644 --- a/hindsight-clients/go/client.go +++ b/hindsight-clients/go/client.go @@ -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) diff --git a/hindsight-clients/go/model_create_folder_request.go b/hindsight-clients/go/model_create_folder_request.go new file mode 100644 index 0000000000..b7bb6c5514 --- /dev/null +++ b/hindsight-clients/go/model_create_folder_request.go @@ -0,0 +1,250 @@ +/* +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"` + Mission NullableString `json:"mission,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() +} + +// GetMission returns the Mission field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateFolderRequest) GetMission() string { + if o == nil || IsNil(o.Mission.Get()) { + var ret string + return ret + } + return *o.Mission.Get() +} + +// GetMissionOk returns a tuple with the Mission 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) GetMissionOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Mission.Get(), o.Mission.IsSet() +} + +// HasMission returns a boolean if a field has been set. +func (o *CreateFolderRequest) HasMission() bool { + if o != nil && o.Mission.IsSet() { + return true + } + + return false +} + +// SetMission gets a reference to the given NullableString and assigns it to the Mission field. +func (o *CreateFolderRequest) SetMission(v string) { + o.Mission.Set(&v) +} +// SetMissionNil sets the value for Mission to be an explicit nil +func (o *CreateFolderRequest) SetMissionNil() { + o.Mission.Set(nil) +} + +// UnsetMission ensures that no value is present for Mission, not even an explicit nil +func (o *CreateFolderRequest) UnsetMission() { + o.Mission.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() + } + if o.Mission.IsSet() { + toSerialize["mission"] = o.Mission.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) +} + + diff --git a/hindsight-clients/go/model_create_knowledge_page_response.go b/hindsight-clients/go/model_create_knowledge_page_response.go new file mode 100644 index 0000000000..ca2be4cbea --- /dev/null +++ b/hindsight-clients/go/model_create_knowledge_page_response.go @@ -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) +} + + diff --git a/hindsight-clients/go/model_create_page_request.go b/hindsight-clients/go/model_create_page_request.go new file mode 100644 index 0000000000..906585f831 --- /dev/null +++ b/hindsight-clients/go/model_create_page_request.go @@ -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) +} + + diff --git a/hindsight-clients/go/model_knowledge_node.go b/hindsight-clients/go/model_knowledge_node.go new file mode 100644 index 0000000000..8d99870a8e --- /dev/null +++ b/hindsight-clients/go/model_knowledge_node.go @@ -0,0 +1,557 @@ +/* +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. Folders carry a ``mission`` (the curator's steering prompt); pages carry ``description``/``tags`` from their backing mental model and a ``managed`` flag (true = curator-managed, false = pinned/human). +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"` + Mission NullableString `json:"mission,omitempty"` + // True for curator-managed nodes; false for pinned/human. + 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() +} + +// GetMission returns the Mission field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *KnowledgeNode) GetMission() string { + if o == nil || IsNil(o.Mission.Get()) { + var ret string + return ret + } + return *o.Mission.Get() +} + +// GetMissionOk returns a tuple with the Mission 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) GetMissionOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Mission.Get(), o.Mission.IsSet() +} + +// HasMission returns a boolean if a field has been set. +func (o *KnowledgeNode) HasMission() bool { + if o != nil && o.Mission.IsSet() { + return true + } + + return false +} + +// SetMission gets a reference to the given NullableString and assigns it to the Mission field. +func (o *KnowledgeNode) SetMission(v string) { + o.Mission.Set(&v) +} +// SetMissionNil sets the value for Mission to be an explicit nil +func (o *KnowledgeNode) SetMissionNil() { + o.Mission.Set(nil) +} + +// UnsetMission ensures that no value is present for Mission, not even an explicit nil +func (o *KnowledgeNode) UnsetMission() { + o.Mission.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 o.Mission.IsSet() { + toSerialize["mission"] = o.Mission.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) +} + + diff --git a/hindsight-clients/go/model_knowledge_page_bundle_file.go b/hindsight-clients/go/model_knowledge_page_bundle_file.go new file mode 100644 index 0000000000..4ed79dac30 --- /dev/null +++ b/hindsight-clients/go/model_knowledge_page_bundle_file.go @@ -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) +} + + diff --git a/hindsight-clients/go/model_knowledge_page_bundle_response.go b/hindsight-clients/go/model_knowledge_page_bundle_response.go new file mode 100644 index 0000000000..f13830e8fa --- /dev/null +++ b/hindsight-clients/go/model_knowledge_page_bundle_response.go @@ -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) +} + + diff --git a/hindsight-clients/go/model_knowledge_page_graph_response.go b/hindsight-clients/go/model_knowledge_page_graph_response.go new file mode 100644 index 0000000000..d77503ce6e --- /dev/null +++ b/hindsight-clients/go/model_knowledge_page_graph_response.go @@ -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) +} + + diff --git a/hindsight-clients/go/model_knowledge_page_response.go b/hindsight-clients/go/model_knowledge_page_response.go new file mode 100644 index 0000000000..08b8e0982b --- /dev/null +++ b/hindsight-clients/go/model_knowledge_page_response.go @@ -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:` 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) +} + + diff --git a/hindsight-clients/go/model_knowledge_tree_response.go b/hindsight-clients/go/model_knowledge_tree_response.go new file mode 100644 index 0000000000..66585072d1 --- /dev/null +++ b/hindsight-clients/go/model_knowledge_tree_response.go @@ -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) +} + + diff --git a/hindsight-clients/go/model_update_node_request.go b/hindsight-clients/go/model_update_node_request.go new file mode 100644 index 0000000000..f6f372c648 --- /dev/null +++ b/hindsight-clients/go/model_update_node_request.go @@ -0,0 +1,228 @@ +/* +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, move, and/or set a folder's mission. Each field applies only when present. +type UpdateNodeRequest struct { + Name NullableString `json:"name,omitempty"` + ParentId NullableString `json:"parent_id,omitempty"` + Mission NullableString `json:"mission,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() +} + +// GetMission returns the Mission field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *UpdateNodeRequest) GetMission() string { + if o == nil || IsNil(o.Mission.Get()) { + var ret string + return ret + } + return *o.Mission.Get() +} + +// GetMissionOk returns a tuple with the Mission 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) GetMissionOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.Mission.Get(), o.Mission.IsSet() +} + +// HasMission returns a boolean if a field has been set. +func (o *UpdateNodeRequest) HasMission() bool { + if o != nil && o.Mission.IsSet() { + return true + } + + return false +} + +// SetMission gets a reference to the given NullableString and assigns it to the Mission field. +func (o *UpdateNodeRequest) SetMission(v string) { + o.Mission.Set(&v) +} +// SetMissionNil sets the value for Mission to be an explicit nil +func (o *UpdateNodeRequest) SetMissionNil() { + o.Mission.Set(nil) +} + +// UnsetMission ensures that no value is present for Mission, not even an explicit nil +func (o *UpdateNodeRequest) UnsetMission() { + o.Mission.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() + } + if o.Mission.IsSet() { + toSerialize["mission"] = o.Mission.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) +} + + diff --git a/hindsight-clients/python/.openapi-generator/FILES b/hindsight-clients/python/.openapi-generator/FILES index 83b5ba67a0..101127ade2 100644 --- a/hindsight-clients/python/.openapi-generator/FILES +++ b/hindsight-clients/python/.openapi-generator/FILES @@ -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 diff --git a/hindsight-clients/python/hindsight_client_api/__init__.py b/hindsight-clients/python/hindsight_client_api/__init__.py index 7338c19665..6d6743fdf5 100644 --- a/hindsight-clients/python/hindsight_client_api/__init__.py +++ b/hindsight-clients/python/hindsight_client_api/__init__.py @@ -25,6 +25,7 @@ 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_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.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_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 diff --git a/hindsight-clients/python/hindsight_client_api/api/__init__.py b/hindsight-clients/python/hindsight_client_api/api/__init__.py index 5c8b5cea3d..56b5a0a910 100644 --- a/hindsight-clients/python/hindsight_client_api/api/__init__.py +++ b/hindsight-clients/python/hindsight_client_api/api/__init__.py @@ -9,6 +9,7 @@ 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 diff --git a/hindsight-clients/python/hindsight_client_api/api/knowledge_base_api.py b/hindsight-clients/python/hindsight_client_api/api/knowledge_base_api.py new file mode 100644 index 0000000000..efe1c43694 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/api/knowledge_base_api.py @@ -0,0 +1,2399 @@ +# 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 + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from pydantic import StrictStr +from typing import Any, Optional +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_page_request import CreatePageRequest +from hindsight_client_api.models.knowledge_node import KnowledgeNode +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.update_node_request import UpdateNodeRequest + +from hindsight_client_api.api_client import ApiClient, RequestSerialized +from hindsight_client_api.api_response import ApiResponse +from hindsight_client_api.rest import RESTResponseType + + +class KnowledgeBaseApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + async def create_knowledge_folder( + self, + bank_id: StrictStr, + create_folder_request: CreateFolderRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> KnowledgeNode: + """Create a knowledge-base folder + + Create a folder, optionally nested under a parent folder. + + :param bank_id: (required) + :type bank_id: str + :param create_folder_request: (required) + :type create_folder_request: CreateFolderRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_knowledge_folder_serialize( + bank_id=bank_id, + create_folder_request=create_folder_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "KnowledgeNode", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def create_knowledge_folder_with_http_info( + self, + bank_id: StrictStr, + create_folder_request: CreateFolderRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[KnowledgeNode]: + """Create a knowledge-base folder + + Create a folder, optionally nested under a parent folder. + + :param bank_id: (required) + :type bank_id: str + :param create_folder_request: (required) + :type create_folder_request: CreateFolderRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_knowledge_folder_serialize( + bank_id=bank_id, + create_folder_request=create_folder_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "KnowledgeNode", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def create_knowledge_folder_without_preload_content( + self, + bank_id: StrictStr, + create_folder_request: CreateFolderRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Create a knowledge-base folder + + Create a folder, optionally nested under a parent folder. + + :param bank_id: (required) + :type bank_id: str + :param create_folder_request: (required) + :type create_folder_request: CreateFolderRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_knowledge_folder_serialize( + bank_id=bank_id, + create_folder_request=create_folder_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "KnowledgeNode", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_knowledge_folder_serialize( + self, + bank_id, + create_folder_request, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + if create_folder_request is not None: + _body_params = create_folder_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/folders', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def create_knowledge_page( + self, + bank_id: StrictStr, + create_page_request: CreatePageRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CreateKnowledgePageResponse: + """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. + + :param bank_id: (required) + :type bank_id: str + :param create_page_request: (required) + :type create_page_request: CreatePageRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_knowledge_page_serialize( + bank_id=bank_id, + create_page_request=create_page_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "CreateKnowledgePageResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def create_knowledge_page_with_http_info( + self, + bank_id: StrictStr, + create_page_request: CreatePageRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CreateKnowledgePageResponse]: + """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. + + :param bank_id: (required) + :type bank_id: str + :param create_page_request: (required) + :type create_page_request: CreatePageRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_knowledge_page_serialize( + bank_id=bank_id, + create_page_request=create_page_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "CreateKnowledgePageResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def create_knowledge_page_without_preload_content( + self, + bank_id: StrictStr, + create_page_request: CreatePageRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """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. + + :param bank_id: (required) + :type bank_id: str + :param create_page_request: (required) + :type create_page_request: CreatePageRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._create_knowledge_page_serialize( + bank_id=bank_id, + create_page_request=create_page_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '201': "CreateKnowledgePageResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _create_knowledge_page_serialize( + self, + bank_id, + create_page_request, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + if create_page_request is not None: + _body_params = create_page_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/pages', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def delete_knowledge_node( + self, + bank_id: StrictStr, + node_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> object: + """Delete a knowledge-base node + + Delete a folder or page and its whole subtree (pages' mental models are removed too). + + :param bank_id: (required) + :type bank_id: str + :param node_id: (required) + :type node_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_knowledge_node_serialize( + bank_id=bank_id, + node_id=node_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "object", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def delete_knowledge_node_with_http_info( + self, + bank_id: StrictStr, + node_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[object]: + """Delete a knowledge-base node + + Delete a folder or page and its whole subtree (pages' mental models are removed too). + + :param bank_id: (required) + :type bank_id: str + :param node_id: (required) + :type node_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_knowledge_node_serialize( + bank_id=bank_id, + node_id=node_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "object", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def delete_knowledge_node_without_preload_content( + self, + bank_id: StrictStr, + node_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Delete a knowledge-base node + + Delete a folder or page and its whole subtree (pages' mental models are removed too). + + :param bank_id: (required) + :type bank_id: str + :param node_id: (required) + :type node_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._delete_knowledge_node_serialize( + bank_id=bank_id, + node_id=node_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "object", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _delete_knowledge_node_serialize( + self, + bank_id, + node_id, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + if node_id is not None: + _path_params['node_id'] = node_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='DELETE', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def export_knowledge_base( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> KnowledgePageBundleResponse: + """Export the knowledge base as an OKF bundle + + Return a portable OKF bundle: a nested index.md, one .md per page, and history logs. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._export_knowledge_base_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageBundleResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def export_knowledge_base_with_http_info( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[KnowledgePageBundleResponse]: + """Export the knowledge base as an OKF bundle + + Return a portable OKF bundle: a nested index.md, one .md per page, and history logs. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._export_knowledge_base_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageBundleResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def export_knowledge_base_without_preload_content( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Export the knowledge base as an OKF bundle + + Return a portable OKF bundle: a nested index.md, one .md per page, and history logs. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._export_knowledge_base_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageBundleResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _export_knowledge_base_serialize( + self, + bank_id, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/export', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def get_knowledge_base_graph( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> KnowledgePageGraphResponse: + """Knowledge-base constellation graph + + Return pages as nodes linked by shared tags, for the constellation view. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_base_graph_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageGraphResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def get_knowledge_base_graph_with_http_info( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[KnowledgePageGraphResponse]: + """Knowledge-base constellation graph + + Return pages as nodes linked by shared tags, for the constellation view. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_base_graph_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageGraphResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def get_knowledge_base_graph_without_preload_content( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Knowledge-base constellation graph + + Return pages as nodes linked by shared tags, for the constellation view. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_base_graph_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageGraphResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_knowledge_base_graph_serialize( + self, + bank_id, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/graph', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def get_knowledge_base_tree( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> KnowledgeTreeResponse: + """Get the knowledge-base tree + + Return the knowledge base as a nested tree of folders and pages. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_base_tree_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgeTreeResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def get_knowledge_base_tree_with_http_info( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[KnowledgeTreeResponse]: + """Get the knowledge-base tree + + Return the knowledge base as a nested tree of folders and pages. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_base_tree_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgeTreeResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def get_knowledge_base_tree_without_preload_content( + self, + bank_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Get the knowledge-base tree + + Return the knowledge base as a nested tree of folders and pages. + + :param bank_id: (required) + :type bank_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_base_tree_serialize( + bank_id=bank_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgeTreeResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_knowledge_base_tree_serialize( + self, + bank_id, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/tree', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def get_knowledge_page( + self, + bank_id: StrictStr, + page_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> KnowledgePageResponse: + """Get a knowledge-base page + + Return a single page as an OKF document (frontmatter + markdown body). + + :param bank_id: (required) + :type bank_id: str + :param page_id: (required) + :type page_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_page_serialize( + bank_id=bank_id, + page_id=page_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def get_knowledge_page_with_http_info( + self, + bank_id: StrictStr, + page_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[KnowledgePageResponse]: + """Get a knowledge-base page + + Return a single page as an OKF document (frontmatter + markdown body). + + :param bank_id: (required) + :type bank_id: str + :param page_id: (required) + :type page_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_page_serialize( + bank_id=bank_id, + page_id=page_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def get_knowledge_page_without_preload_content( + self, + bank_id: StrictStr, + page_id: StrictStr, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Get a knowledge-base page + + Return a single page as an OKF document (frontmatter + markdown body). + + :param bank_id: (required) + :type bank_id: str + :param page_id: (required) + :type page_id: str + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._get_knowledge_page_serialize( + bank_id=bank_id, + page_id=page_id, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgePageResponse", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _get_knowledge_page_serialize( + self, + bank_id, + page_id, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + if page_id is not None: + _path_params['page_id'] = page_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/pages/{page_id}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def update_knowledge_node( + self, + bank_id: StrictStr, + node_id: StrictStr, + update_node_request: UpdateNodeRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> KnowledgeNode: + """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). + + :param bank_id: (required) + :type bank_id: str + :param node_id: (required) + :type node_id: str + :param update_node_request: (required) + :type update_node_request: UpdateNodeRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_knowledge_node_serialize( + bank_id=bank_id, + node_id=node_id, + update_node_request=update_node_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgeNode", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def update_knowledge_node_with_http_info( + self, + bank_id: StrictStr, + node_id: StrictStr, + update_node_request: UpdateNodeRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[KnowledgeNode]: + """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). + + :param bank_id: (required) + :type bank_id: str + :param node_id: (required) + :type node_id: str + :param update_node_request: (required) + :type update_node_request: UpdateNodeRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_knowledge_node_serialize( + bank_id=bank_id, + node_id=node_id, + update_node_request=update_node_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgeNode", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def update_knowledge_node_without_preload_content( + self, + bank_id: StrictStr, + node_id: StrictStr, + update_node_request: UpdateNodeRequest, + authorization: Optional[StrictStr] = None, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """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). + + :param bank_id: (required) + :type bank_id: str + :param node_id: (required) + :type node_id: str + :param update_node_request: (required) + :type update_node_request: UpdateNodeRequest + :param authorization: + :type authorization: str + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._update_knowledge_node_serialize( + bank_id=bank_id, + node_id=node_id, + update_node_request=update_node_request, + authorization=authorization, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "KnowledgeNode", + '422': "HTTPValidationError", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_knowledge_node_serialize( + self, + bank_id, + node_id, + update_node_request, + authorization, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + if bank_id is not None: + _path_params['bank_id'] = bank_id + if node_id is not None: + _path_params['node_id'] = node_id + # process the query parameters + # process the header parameters + if authorization is not None: + _header_params['authorization'] = authorization + # process the form parameters + # process the body parameter + if update_node_request is not None: + _body_params = update_node_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + ] + + return self.api_client.param_serialize( + method='PATCH', + resource_path='/v1/default/banks/{bank_id}/knowledge-base/nodes/{node_id}', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/hindsight-clients/python/hindsight_client_api/models/__init__.py b/hindsight-clients/python/hindsight_client_api/models/__init__.py index 9b397a7f25..99592e2cd0 100644 --- a/hindsight-clients/python/hindsight_client_api/models/__init__.py +++ b/hindsight-clients/python/hindsight_client_api/models/__init__.py @@ -44,8 +44,11 @@ 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.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_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 diff --git a/hindsight-clients/python/hindsight_client_api/models/create_folder_request.py b/hindsight-clients/python/hindsight_client_api/models/create_folder_request.py new file mode 100644 index 0000000000..8d91151058 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/create_folder_request.py @@ -0,0 +1,101 @@ +# 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 + mission: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["name", "parent_id", "mission"] + + 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 + + # set to None if mission (nullable) is None + # and model_fields_set contains the field + if self.mission is None and "mission" in self.model_fields_set: + _dict['mission'] = 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"), + "mission": obj.get("mission") + }) + return _obj + + diff --git a/hindsight-clients/python/hindsight_client_api/models/create_knowledge_page_response.py b/hindsight-clients/python/hindsight_client_api/models/create_knowledge_page_response.py new file mode 100644 index 0000000000..aceaf20a92 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/create_knowledge_page_response.py @@ -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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/create_page_request.py b/hindsight-clients/python/hindsight_client_api/models/create_page_request.py new file mode 100644 index 0000000000..b4079a9528 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/create_page_request.py @@ -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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/knowledge_node.py b/hindsight-clients/python/hindsight_client_api/models/knowledge_node.py new file mode 100644 index 0000000000..7d9c2e261e --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/knowledge_node.py @@ -0,0 +1,148 @@ +# 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. Folders carry a ``mission`` (the curator's steering prompt); pages carry ``description``/``tags`` from their backing mental model and a ``managed`` flag (true = curator-managed, false = pinned/human). + """ # noqa: E501 + id: StrictStr + kind: StrictStr + name: StrictStr + parent_id: Optional[StrictStr] = None + mental_model_id: Optional[StrictStr] = None + mission: Optional[StrictStr] = None + managed: Optional[StrictBool] = Field(default=False, description="True for curator-managed nodes; false for pinned/human.") + 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", "mission", "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 mission (nullable) is None + # and model_fields_set contains the field + if self.mission is None and "mission" in self.model_fields_set: + _dict['mission'] = 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"), + "mission": obj.get("mission"), + "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) + diff --git a/hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_file.py b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_file.py new file mode 100644 index 0000000000..a09728e1d9 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_file.py @@ -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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_response.py b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_response.py new file mode 100644 index 0000000000..e0490594e9 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_bundle_response.py @@ -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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/knowledge_page_graph_response.py b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_graph_response.py new file mode 100644 index 0000000000..d5869b6d6f --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_graph_response.py @@ -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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/knowledge_page_response.py b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_response.py new file mode 100644 index 0000000000..4766acc512 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/knowledge_page_response.py @@ -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:` 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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/knowledge_tree_response.py b/hindsight-clients/python/hindsight_client_api/models/knowledge_tree_response.py new file mode 100644 index 0000000000..263d608bbd --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/knowledge_tree_response.py @@ -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 + + diff --git a/hindsight-clients/python/hindsight_client_api/models/update_node_request.py b/hindsight-clients/python/hindsight_client_api/models/update_node_request.py new file mode 100644 index 0000000000..e141334697 --- /dev/null +++ b/hindsight-clients/python/hindsight_client_api/models/update_node_request.py @@ -0,0 +1,106 @@ +# 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, move, and/or set a folder's mission. Each field applies only when present. + """ # noqa: E501 + name: Optional[StrictStr] = None + parent_id: Optional[StrictStr] = None + mission: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["name", "parent_id", "mission"] + + 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 + + # set to None if mission (nullable) is None + # and model_fields_set contains the field + if self.mission is None and "mission" in self.model_fields_set: + _dict['mission'] = 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"), + "mission": obj.get("mission") + }) + return _obj + + diff --git a/hindsight-clients/typescript/generated/sdk.gen.ts b/hindsight-clients/typescript/generated/sdk.gen.ts index 6705f158c8..00446c03f1 100644 --- a/hindsight-clients/typescript/generated/sdk.gen.ts +++ b/hindsight-clients/typescript/generated/sdk.gen.ts @@ -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 = ( ...options, }); +/** + * Get the knowledge-base tree + * + * Return the knowledge base as a nested tree of folders and pages. + */ +export const getKnowledgeBaseTree = ( + options: Options +) => + (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 = ( + options: Options +) => + (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 = ( + options: Options +) => + (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 = ( + options: Options +) => + (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 .md per page, and history logs. + */ +export const exportKnowledgeBase = ( + options: Options +) => + (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 = ( + options: Options +) => + (options.client ?? client).get({ + 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 = ( + options: Options +) => + (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 = ( + options: Options +) => + (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 * diff --git a/hindsight-clients/typescript/generated/types.gen.ts b/hindsight-clients/typescript/generated/types.gen.ts index 3766fb764e..756a49841f 100644 --- a/hindsight-clients/typescript/generated/types.gen.ts +++ b/hindsight-clients/typescript/generated/types.gen.ts @@ -1153,6 +1153,48 @@ export type CreateDirectiveRequest = { tags?: Array; }; +/** + * CreateFolderRequest + * + * Create a folder under an optional parent folder. + */ +export type CreateFolderRequest = { + /** + * Name + */ + name: string; + /** + * Parent Id + */ + parent_id?: string | null; + /** + * Mission + * + * Curator mission for the folder. + */ + mission?: 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 +1257,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 | null; + /** + * Max Tokens + */ + max_tokens?: number | null; + trigger?: MentalModelTriggerInput | null; +}; + /** * CreateWebhookRequest * @@ -1956,6 +2027,190 @@ export type IncludeOptions = { source_facts?: SourceFactsIncludeOptions | null; }; +/** + * KnowledgeNode + * + * A node in the knowledge-base tree — a folder or a page. + * + * Folders carry a ``mission`` (the curator's steering prompt); pages carry + * ``description``/``tags`` from their backing mental model and a ``managed`` + * flag (true = curator-managed, false = pinned/human). + */ +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; + /** + * Mission + * + * Curator mission (folders only). + */ + mission?: string | null; + /** + * Managed + * + * True for curator-managed nodes; false for pinned/human. + */ + managed?: boolean; + /** + * Description + * + * Page source query (OKF `description`). + */ + description?: string | null; + /** + * Tags + */ + tags?: Array; + /** + * Timestamp + * + * Last refresh (page) or last update (folder). + */ + timestamp?: string | null; + /** + * Children + */ + children?: Array; +}; + +/** + * 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; +}; + +/** + * 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:` tag, else 'knowledge-page'. + */ + type: string; + /** + * Description + * + * The source query that rebuilds the page. + */ + description?: string | null; + /** + * Tags + */ + tags?: Array; + /** + * 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; +}; + /** * LLMRequestEntry * @@ -3938,6 +4193,26 @@ export type UpdateMentalModelRequest = { trigger?: MentalModelTriggerInput | null; }; +/** + * UpdateNodeRequest + * + * Rename, move, and/or set a folder's mission. Each field applies only when present. + */ +export type UpdateNodeRequest = { + /** + * Name + */ + name?: string | null; + /** + * Parent Id + */ + parent_id?: string | null; + /** + * Mission + */ + mission?: string | null; +}; + /** * UpdateWebhookRequest * @@ -5271,6 +5546,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?: { diff --git a/hindsight-control-plane/src/app/[locale]/banks/[bankId]/page.tsx b/hindsight-control-plane/src/app/[locale]/banks/[bankId]/page.tsx index faad0a8ae4..182b4d6278 100644 --- a/hindsight-control-plane/src/app/[locale]/banks/[bankId]/page.tsx +++ b/hindsight-control-plane/src/app/[locale]/banks/[bankId]/page.tsx @@ -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() { )} + + {/* Knowledge base Tab — KnowledgeBaseView renders its own header. */} + {view === "knowledge" && ( +
+ +
+ )} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/export/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/export/route.ts new file mode 100644 index 0000000000..58c8dff5c8 --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/export/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/folders/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/folders/route.ts new file mode 100644 index 0000000000..97a2d80992 --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/folders/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/graph/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/graph/route.ts new file mode 100644 index 0000000000..e89af549a2 --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/graph/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/nodes/[nodeId]/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/nodes/[nodeId]/route.ts new file mode 100644 index 0000000000..1d07d220f9 --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/nodes/[nodeId]/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/pages/[pageId]/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/pages/[pageId]/route.ts new file mode 100644 index 0000000000..4f6d32ca15 --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/pages/[pageId]/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/pages/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/pages/route.ts new file mode 100644 index 0000000000..70b537587f --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/pages/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/app/api/knowledge-base/tree/route.ts b/hindsight-control-plane/src/app/api/knowledge-base/tree/route.ts new file mode 100644 index 0000000000..511665a730 --- /dev/null +++ b/hindsight-control-plane/src/app/api/knowledge-base/tree/route.ts @@ -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 } + ); + } +} diff --git a/hindsight-control-plane/src/components/knowledge-base-view.tsx b/hindsight-control-plane/src/components/knowledge-base-view.tsx new file mode 100644 index 0000000000..672f87cc53 --- /dev/null +++ b/hindsight-control-plane/src/components/knowledge-base-view.tsx @@ -0,0 +1,772 @@ +"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, + Pencil, + 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>; +type PageDetail = Awaited>; + +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([]); + const [loading, setLoading] = useState(false); + const [view, setView] = useState("tree"); + const [expanded, setExpanded] = useState>(new Set()); + + const [selected, setSelected] = useState(null); + const [loadingDetail, setLoadingDetail] = useState(false); + + const [graph, setGraph] = useState(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: "", mission: "" }); + const [creating, setCreating] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleting, setDeleting] = useState(false); + const [missionFolder, setMissionFolder] = useState(null); + const [missionDraft, setMissionDraft] = useState(""); + const [savingMission, setSavingMission] = 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, mission: "" }); + 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, + mission: form.mission.trim() || null, + }); + } 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 openMission = (folder: KnowledgeNode) => { + setMissionDraft(folder.mission ?? ""); + setMissionFolder(folder); + }; + + const handleSaveMission = async () => { + if (!currentBank || !missionFolder) return; + setSavingMission(true); + try { + await client.updateKnowledgeNode(currentBank, missionFolder.id, { + mission: missionDraft.trim() || null, + }); + setMissionFolder(null); + await loadTree(); + } catch { + // toast handled by interceptor + } finally { + setSavingMission(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(); + for (const n of graph?.nodes ?? []) colors.set(n.data.type, n.data.color); + return colors; + }, [graph]); + + const constellationData = useMemo(() => { + 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(); + 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 ( +
+
+
+

{t("title")}

+

{t("description")}

+
+
+ + + +
+
+ +
+
+ {view === "graph" + ? t("graphCount", { pages: graph?.total_pages ?? 0, links: graph?.total_edges ?? 0 }) + : t("count", { folders: folderCount, pages: pageCount })} +
+
+ + +
+
+ + {view === "tree" ? ( +
+ {loading ? ( +
+ +
+ ) : roots.length > 0 ? ( +
    + {roots.map((node) => ( + + ))} +
+ ) : ( +
+
+ +
{t("empty")}
+
{t("emptyHint")}
+
+
+ )} +
+ ) : ( +
+ {graphLoading ? ( +
+ +
+ ) : constellationData.nodes.length > 0 ? ( + openPage(node.id)} + nodeSizeFn={nodeSizeFn} + clusterKeyFn={(node) => node.group ?? null} + clusterColorFn={(key) => typeColors.get(key) || FALLBACK_COLOR} + clusterLabelFn={(key) => key} + sizeLegendLabel={t("sizeLegendLabel")} + compactLabels + /> + ) : ( +
+
{t("empty")}
+
+ )} +
+ )} + + {/* Page detail panel */} + {(selected || loadingDetail) && ( +
+
+
+
+

+ {selected?.name ?? t("loadingPage")} +

+ {selected?.description && ( +

+ “{selected.description}” +

+ )} +
+ +
+ {loadingDetail && !selected ? ( +
+ +
+ ) : selected ? ( +
+ {selected.tags.length > 0 && ( +
+ {selected.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + {selected.timestamp && ( +
+ {t("updatedLabel")} {formatRelativeTime(selected.timestamp)} +
+ )} + {selected.body ? ( +
+ {selected.body} +
+ ) : ( +

+ {t("noBody")} +

+ )} +
+ ) : null} +
+
+ )} + + {/* Create folder/page dialog */} + !o && setCreateKind(null)}> + + + + {createKind === "folder" ? t("createFolderTitle") : t("createPageTitle")} + + {t("description")} + +
+
+ + setForm({ ...form, name: e.target.value })} + autoFocus + /> +
+ {createKind === "folder" && ( +
+ +