* fix: prevent silent memory loss on consolidation LLM failure
When all LLM retries are exhausted during consolidation, memories were
being marked consolidated_at unconditionally, permanently excluding them
from future consolidation runs without producing any observations.
Fix with two complementary mechanisms:
- Adaptive batch splitting: on LLM failure, the batch is halved and
retried recursively down to batch_size=1, recovering most transient
failures (rate limits, Pydantic validation on long prompts) without
operator intervention
- consolidation_failed_at column: only single-memory batches that still
fail after all retries are marked here instead of consolidated_at, so
they remain visible and retryable
- New API endpoint POST /v1/default/banks/{bank_id}/consolidation/retry-failed
resets these memories for the next consolidation run
* chore: regenerate OpenAPI spec
* fix: rename consolidation endpoint from /retry-failed to /recover
* fix: add consolidation_failed_at column, adaptive batch splitting, and recovery API
- Migration a3b4c5d6e7f8: add consolidation_failed_at TIMESTAMPTZ column to
memory_units with an index for efficient failure queries; properly chains off
g7h8i9j0k1l2 (backsweep_orphan_observations)
- Consolidator: filter pending memories with consolidation_failed_at IS NULL
so failed memories are not re-fetched in an infinite loop
- Consolidator: adaptive batch splitting — when a batch exhausts all 3 LLM
retries, halve it and retry sub-batches recursively; only single-memory
batches that also exhaust all retries get consolidation_failed_at set
- New tests (9 total) covering: adaptive splitting recovers all memories,
larger batch splitting, single-memory permanent failure, exclusion from
next run, partial batch failure, recover resets columns, recover returns
0 when none failed, recover-then-consolidate succeeds, HTTP endpoint
* chore: regenerate Go, Python, TypeScript clients with recover consolidation endpoint
* feat: add Recover Consolidation action to bank Actions dropdown
* style: apply ruff formatting to http.py and config.py
* fix: handle consolidation scope in large batch test mock LLM
The mock LLM was returning {"facts": ...} for ALL calls including consolidation.
Consolidation doesn't use skip_validation=True so it expects a _ConsolidationBatchResponse
instance, not a raw dict. Before this PR consolidation silently swallowed the AttributeError
(failed=False was returned); now failed=True triggers adaptive splitting and timeouts.
Fix: return _ConsolidationBatchResponse() when scope=="consolidation".
* fix: restrict claude-agent-sdk to macOS platform only (no Linux wheel available)
Also fix pre-existing type errors: use setattr for XLM-RoBERTa monkey-patch
and add missing reranker_local_fp16/bucket_batching/batch_size fields to main.py config constructor.
* fix: add UV_INDEX_STRATEGY=unsafe-best-match to fix markupsafe cp314 wheel conflict
PyTorch CPU index serves markupsafe==3.0.3 with only cp314 wheels.
uv's default first-index strategy stops at the first index with any version
even if no compatible wheel exists. unsafe-best-match searches all indices
for the best compatible wheel, falling back to PyPI for markupsafe.
* fix: use explicit pytorch index to prevent markupsafe wheel conflict
Configure the pytorch CPU index as explicit=true in pyproject.toml so it is
ONLY used for torch (via [tool.uv.sources]). All other packages (including
markupsafe) are resolved exclusively from PyPI, preventing the pytorch index
from serving incompatible cp314-only wheels for non-pytorch packages.
Remove UV_INDEX and UV_INDEX_STRATEGY from CI workflow (no longer needed
since the index is now configured in pyproject.toml).
* ci: trigger CI run
* ci: retry trigger
* ci: trigger after remote URL fix
* ci: add workflow_dispatch to unblock manual trigger
* fix: remove empty env blocks left after UV_INDEX removal
* fix: add type: ignore for optional claude_agent_sdk imports (macOS-only)
* fix: correct type: ignore rules for claude_agent_sdk and fix utcnow deprecation
1057 lines
27 KiB
TypeScript
1057 lines
27 KiB
TypeScript
/**
|
|
* Client for calling Control Plane API routes (which proxy to the dataplane via SDK)
|
|
* This should be used in client components, not the SDK directly
|
|
*/
|
|
|
|
import { toast } from "sonner";
|
|
|
|
export interface WebhookHttpConfig {
|
|
method: string;
|
|
timeout_seconds: number;
|
|
headers: Record<string, string>;
|
|
params: Record<string, string>;
|
|
}
|
|
|
|
export interface Webhook {
|
|
id: string;
|
|
bank_id: string | null;
|
|
url: string;
|
|
event_types: string[];
|
|
enabled: boolean;
|
|
http_config: WebhookHttpConfig;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface WebhookDelivery {
|
|
id: string;
|
|
webhook_id: string | null;
|
|
url: string;
|
|
event_type: string;
|
|
status: string;
|
|
attempts: number;
|
|
next_retry_at: string | null;
|
|
last_error: string | null;
|
|
last_response_status: number | null;
|
|
last_response_body: string | null;
|
|
last_attempt_at: string | null;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
export interface MentalModel {
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
source_query: string;
|
|
content: string;
|
|
tags: string[];
|
|
max_tokens: number;
|
|
trigger: { refresh_after_consolidation: boolean };
|
|
last_refreshed_at: string;
|
|
created_at: string;
|
|
reflect_response?: any;
|
|
}
|
|
|
|
export class ControlPlaneClient {
|
|
private async fetchApi<T>(path: string, options?: RequestInit): Promise<T> {
|
|
try {
|
|
const response = await fetch(path, {
|
|
...options,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...options?.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Try to parse error response
|
|
let errorMessage = `HTTP ${response.status}`;
|
|
let errorDetails: string | undefined;
|
|
|
|
try {
|
|
const errorData = await response.json();
|
|
errorMessage = errorData.error || errorMessage;
|
|
errorDetails = errorData.details;
|
|
} catch {
|
|
// If JSON parse fails, try to get text
|
|
try {
|
|
const errorText = await response.text();
|
|
if (errorText) {
|
|
errorDetails = errorText;
|
|
}
|
|
} catch {
|
|
// Ignore text parse errors
|
|
}
|
|
}
|
|
|
|
// Show toast with different styles based on status code
|
|
const description = errorDetails || errorMessage;
|
|
const status = response.status;
|
|
|
|
if (status >= 400 && status < 500) {
|
|
// Client errors (4xx) - validation, bad request, etc. - show as warning
|
|
toast.warning("Client Error", {
|
|
description,
|
|
duration: 5000,
|
|
});
|
|
} else if (status >= 500) {
|
|
// Server errors (5xx) - show as error
|
|
toast.error("Server Error", {
|
|
description,
|
|
duration: 5000,
|
|
});
|
|
} else {
|
|
// Other HTTP errors - show as error
|
|
toast.error("API Error", {
|
|
description,
|
|
duration: 5000,
|
|
});
|
|
}
|
|
|
|
// Still throw error for callers that want to handle it
|
|
const error = new Error(errorMessage);
|
|
(error as any).status = response.status;
|
|
(error as any).details = errorDetails;
|
|
throw error;
|
|
}
|
|
|
|
return response.json();
|
|
} catch (error) {
|
|
// If it's not a response error (network error, etc.), show toast
|
|
if (!(error as any).status) {
|
|
toast.error("Network Error", {
|
|
description: error instanceof Error ? error.message : "Failed to connect to server",
|
|
duration: 5000,
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all banks
|
|
*/
|
|
async listBanks() {
|
|
return this.fetchApi<{ banks: any[] }>("/api/banks", { cache: "no-store" as RequestCache });
|
|
}
|
|
|
|
/**
|
|
* Create a new bank
|
|
*/
|
|
async createBank(bankId: string) {
|
|
return this.fetchApi<{ bank_id: string }>("/api/banks", {
|
|
method: "POST",
|
|
body: JSON.stringify({ bank_id: bankId }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Recall memories
|
|
*/
|
|
async recall(params: {
|
|
query: string;
|
|
types?: string[];
|
|
bank_id: string;
|
|
budget?: string;
|
|
max_tokens?: number;
|
|
trace?: boolean;
|
|
include?: {
|
|
entities?: { max_tokens: number } | null;
|
|
chunks?: { max_tokens: number } | null;
|
|
source_facts?: { max_tokens?: number } | null;
|
|
};
|
|
query_timestamp?: string;
|
|
tags?: string[];
|
|
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
|
}) {
|
|
return this.fetchApi("/api/recall", {
|
|
method: "POST",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reflect and generate answer
|
|
*/
|
|
async reflect(params: {
|
|
query: string;
|
|
bank_id: string;
|
|
budget?: string;
|
|
max_tokens?: number;
|
|
include_facts?: boolean;
|
|
include_tool_calls?: boolean;
|
|
tags?: string[];
|
|
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
|
}) {
|
|
return this.fetchApi("/api/reflect", {
|
|
method: "POST",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retain memories (batch)
|
|
*/
|
|
async retain(params: {
|
|
bank_id: string;
|
|
items: Array<{
|
|
content: string;
|
|
timestamp?: string;
|
|
context?: string;
|
|
document_id?: string;
|
|
metadata?: Record<string, string>;
|
|
entities?: Array<{ text: string; type?: string }>;
|
|
tags?: string[];
|
|
observation_scopes?: "per_tag" | "combined" | "all_combinations" | string[][];
|
|
strategy?: string;
|
|
}>;
|
|
document_id?: string;
|
|
async?: boolean;
|
|
}) {
|
|
const endpoint = params.async ? "/api/memories/retain_async" : "/api/memories/retain";
|
|
return this.fetchApi<{ message?: string }>(endpoint, {
|
|
method: "POST",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get bank statistics
|
|
*/
|
|
async getBankStats(bankId: string) {
|
|
return this.fetchApi(`/api/stats/${bankId}`);
|
|
}
|
|
|
|
/**
|
|
* Get graph data
|
|
*/
|
|
async getGraph(params: {
|
|
bank_id: string;
|
|
type?: string;
|
|
limit?: number;
|
|
q?: string;
|
|
tags?: string[];
|
|
}) {
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.append("bank_id", params.bank_id);
|
|
if (params.type) queryParams.append("type", params.type);
|
|
if (params.limit) queryParams.append("limit", params.limit.toString());
|
|
if (params.q) queryParams.append("q", params.q);
|
|
if (params.tags && params.tags.length > 0) {
|
|
params.tags.forEach((tag) => queryParams.append("tags", tag));
|
|
}
|
|
return this.fetchApi(`/api/graph?${queryParams}`);
|
|
}
|
|
|
|
/**
|
|
* List operations with optional filtering and pagination
|
|
*/
|
|
async listOperations(
|
|
bankId: string,
|
|
options?: { status?: string; type?: string; limit?: number; offset?: number }
|
|
) {
|
|
const params = new URLSearchParams();
|
|
if (options?.status) params.append("status", options.status);
|
|
if (options?.type) params.append("type", options.type);
|
|
if (options?.limit) params.append("limit", options.limit.toString());
|
|
if (options?.offset) params.append("offset", options.offset.toString());
|
|
const query = params.toString();
|
|
return this.fetchApi<{
|
|
bank_id: string;
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
operations: Array<{
|
|
id: string;
|
|
task_type: string;
|
|
items_count: number;
|
|
document_id: string | null;
|
|
created_at: string;
|
|
status: string;
|
|
error_message: string | null;
|
|
}>;
|
|
}>(`/api/operations/${bankId}${query ? `?${query}` : ""}`);
|
|
}
|
|
|
|
/**
|
|
* Cancel a pending operation
|
|
*/
|
|
async cancelOperation(bankId: string, operationId: string) {
|
|
return this.fetchApi<{
|
|
success: boolean;
|
|
message: string;
|
|
operation_id: string;
|
|
}>(`/api/operations/${bankId}?operation_id=${operationId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retry a failed operation
|
|
*/
|
|
async retryOperation(bankId: string, operationId: string) {
|
|
return this.fetchApi<{
|
|
success: boolean;
|
|
message: string;
|
|
operation_id: string;
|
|
}>(`/api/banks/${bankId}/operations/${operationId}`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List entities
|
|
*/
|
|
async listEntities(params: { bank_id: string; limit?: number; offset?: number }) {
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.append("bank_id", params.bank_id);
|
|
if (params.limit) queryParams.append("limit", params.limit.toString());
|
|
if (params.offset) queryParams.append("offset", params.offset.toString());
|
|
return this.fetchApi<{
|
|
items: any[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}>(`/api/entities?${queryParams}`);
|
|
}
|
|
|
|
/**
|
|
* Get entity details
|
|
*/
|
|
async getEntity(entityId: string, bankId: string) {
|
|
return this.fetchApi(`/api/entities/${entityId}?bank_id=${bankId}`);
|
|
}
|
|
|
|
/**
|
|
* Regenerate entity observations
|
|
*/
|
|
async regenerateEntityObservations(entityId: string, bankId: string) {
|
|
return this.fetchApi(`/api/entities/${entityId}/regenerate?bank_id=${bankId}`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List documents
|
|
*/
|
|
async listDocuments(params: { bank_id: string; q?: string; limit?: number; offset?: number }) {
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.append("bank_id", params.bank_id);
|
|
if (params.q) queryParams.append("q", params.q);
|
|
if (params.limit) queryParams.append("limit", params.limit.toString());
|
|
if (params.offset) queryParams.append("offset", params.offset.toString());
|
|
return this.fetchApi(`/api/documents?${queryParams}`);
|
|
}
|
|
|
|
/**
|
|
* Get document
|
|
*/
|
|
async getDocument(documentId: string, bankId: string) {
|
|
return this.fetchApi(`/api/documents/${documentId}?bank_id=${bankId}`);
|
|
}
|
|
|
|
/**
|
|
* Update tags on a document and its associated memory units
|
|
*/
|
|
async updateDocument(documentId: string, bankId: string, tags: string[]) {
|
|
return this.fetchApi<{ success: boolean }>(
|
|
`/api/documents/${encodeURIComponent(documentId)}?bank_id=${bankId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ tags }),
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete document and all its associated memory units
|
|
*/
|
|
async deleteDocument(documentId: string, bankId: string) {
|
|
return this.fetchApi<{
|
|
success: boolean;
|
|
message: string;
|
|
document_id: string;
|
|
memory_units_deleted: number;
|
|
}>(`/api/documents/${documentId}?bank_id=${bankId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete an entire memory bank and all its data
|
|
*/
|
|
async deleteBank(bankId: string) {
|
|
return this.fetchApi<{
|
|
success: boolean;
|
|
message: string;
|
|
deleted_count: number;
|
|
}>(`/api/banks/${bankId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear all observations for a bank
|
|
*/
|
|
async clearObservations(bankId: string) {
|
|
return this.fetchApi<{
|
|
success: boolean;
|
|
message: string;
|
|
deleted_count: number;
|
|
}>(`/api/banks/${bankId}/observations`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger consolidation for a bank
|
|
*/
|
|
async triggerConsolidation(bankId: string) {
|
|
return this.fetchApi<{
|
|
operation_id: string;
|
|
deduplicated: boolean;
|
|
}>(`/api/banks/${bankId}/consolidate`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Recover failed consolidation for a bank (reset memories marked consolidation_failed_at)
|
|
*/
|
|
async recoverConsolidation(bankId: string) {
|
|
return this.fetchApi<{
|
|
retried_count: number;
|
|
}>(`/api/banks/${bankId}/consolidation-recover`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get chunk
|
|
*/
|
|
async getChunk(chunkId: string) {
|
|
return this.fetchApi(`/api/chunks/${chunkId}`);
|
|
}
|
|
|
|
/**
|
|
* Get a single memory by ID
|
|
*/
|
|
async getMemory(memoryId: string, bankId: string) {
|
|
return this.fetchApi<{
|
|
id: string;
|
|
text: string;
|
|
context: string;
|
|
date: string;
|
|
type: string;
|
|
mentioned_at: string | null;
|
|
occurred_start: string | null;
|
|
occurred_end: string | null;
|
|
entities: string[];
|
|
document_id: string | null;
|
|
chunk_id: string | null;
|
|
tags: string[];
|
|
observation_scopes: string | string[][] | null;
|
|
history?: {
|
|
previous_text: string;
|
|
previous_tags: string[];
|
|
previous_occurred_start: string | null;
|
|
previous_occurred_end: string | null;
|
|
previous_mentioned_at: string | null;
|
|
changed_at: string;
|
|
new_source_memory_ids: string[];
|
|
}[];
|
|
}>(`/api/memories/${memoryId}?bank_id=${bankId}`);
|
|
}
|
|
|
|
/**
|
|
* Get the history of an observation with resolved source facts
|
|
*/
|
|
async getObservationHistory(memoryId: string, bankId: string) {
|
|
return this.fetchApi<
|
|
{
|
|
previous_text: string;
|
|
previous_tags: string[];
|
|
previous_occurred_start: string | null;
|
|
previous_occurred_end: string | null;
|
|
previous_mentioned_at: string | null;
|
|
changed_at: string;
|
|
new_source_memory_ids: string[];
|
|
source_facts: {
|
|
id: string;
|
|
text: string | null;
|
|
type: string | null;
|
|
context: string | null;
|
|
is_new: boolean;
|
|
}[];
|
|
}[]
|
|
>(`/api/memories/${memoryId}/history?bank_id=${bankId}`);
|
|
}
|
|
|
|
/**
|
|
* Get bank profile
|
|
*/
|
|
async getBankProfile(bankId: string) {
|
|
return this.fetchApi<{
|
|
bank_id: string;
|
|
name: string;
|
|
disposition: {
|
|
skepticism: number;
|
|
literalism: number;
|
|
empathy: number;
|
|
};
|
|
mission: string;
|
|
background?: string; // Deprecated, kept for backwards compatibility
|
|
}>(`/api/profile/${bankId}`);
|
|
}
|
|
|
|
/**
|
|
* Set bank mission
|
|
*/
|
|
async setBankMission(bankId: string, mission: string) {
|
|
return this.fetchApi(`/api/banks/${bankId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ mission }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List directives for a bank
|
|
*/
|
|
async listDirectives(bankId: string, tags?: string[], tagsMatch?: string) {
|
|
const params = new URLSearchParams();
|
|
if (tags && tags.length > 0) {
|
|
tags.forEach((t) => params.append("tags", t));
|
|
}
|
|
if (tagsMatch) {
|
|
params.append("tags_match", tagsMatch);
|
|
}
|
|
const query = params.toString();
|
|
return this.fetchApi<{
|
|
items: Array<{
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
content: string;
|
|
priority: number;
|
|
is_active: boolean;
|
|
tags: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>;
|
|
}>(`/api/banks/${bankId}/directives${query ? `?${query}` : ""}`);
|
|
}
|
|
|
|
/**
|
|
* Create a directive
|
|
*/
|
|
async createDirective(
|
|
bankId: string,
|
|
params: {
|
|
name: string;
|
|
content: string;
|
|
priority?: number;
|
|
is_active?: boolean;
|
|
tags?: string[];
|
|
}
|
|
) {
|
|
return this.fetchApi<{
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
content: string;
|
|
priority: number;
|
|
is_active: boolean;
|
|
tags: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>(`/api/banks/${bankId}/directives`, {
|
|
method: "POST",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a directive
|
|
*/
|
|
async getDirective(bankId: string, directiveId: string) {
|
|
return this.fetchApi<{
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
content: string;
|
|
priority: number;
|
|
is_active: boolean;
|
|
tags: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>(`/api/banks/${bankId}/directives/${directiveId}`);
|
|
}
|
|
|
|
/**
|
|
* Delete a directive
|
|
*/
|
|
async deleteDirective(bankId: string, directiveId: string) {
|
|
return this.fetchApi(`/api/banks/${bankId}/directives/${directiveId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update a directive
|
|
*/
|
|
async updateDirective(
|
|
bankId: string,
|
|
directiveId: string,
|
|
params: {
|
|
name?: string;
|
|
content?: string;
|
|
priority?: number;
|
|
is_active?: boolean;
|
|
tags?: string[];
|
|
}
|
|
) {
|
|
return this.fetchApi<{
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
content: string;
|
|
priority: number;
|
|
is_active: boolean;
|
|
tags: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>(`/api/banks/${bankId}/directives/${directiveId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get operation status
|
|
*/
|
|
async getOperationStatus(bankId: string, operationId: string) {
|
|
return this.fetchApi<{
|
|
operation_id: string;
|
|
status: "pending" | "completed" | "failed" | "not_found";
|
|
operation_type: string | null;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
completed_at: string | null;
|
|
error_message: string | null;
|
|
}>(`/api/banks/${bankId}/operations/${operationId}`);
|
|
}
|
|
|
|
/**
|
|
* Update bank profile
|
|
*/
|
|
async updateBankProfile(
|
|
bankId: string,
|
|
profile: {
|
|
name?: string;
|
|
disposition?: {
|
|
skepticism: number;
|
|
literalism: number;
|
|
empathy: number;
|
|
};
|
|
mission?: string;
|
|
}
|
|
) {
|
|
return this.fetchApi(`/api/profile/${bankId}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(profile),
|
|
});
|
|
}
|
|
|
|
// ============= OBSERVATIONS (auto-consolidated, read-only) =============
|
|
|
|
/**
|
|
* List observations for a bank (auto-consolidated knowledge)
|
|
*/
|
|
async listObservations(bankId: string, tags?: string[], tagsMatch?: string) {
|
|
const params = new URLSearchParams();
|
|
if (tags && tags.length > 0) {
|
|
tags.forEach((t) => params.append("tags", t));
|
|
}
|
|
if (tagsMatch) {
|
|
params.append("tags_match", tagsMatch);
|
|
}
|
|
const query = params.toString();
|
|
return this.fetchApi<{
|
|
items: Array<{
|
|
id: string;
|
|
bank_id: string;
|
|
text: string;
|
|
proof_count: number;
|
|
history: Array<{
|
|
previous_text: string;
|
|
changed_at: string;
|
|
reason: string;
|
|
}>;
|
|
tags: string[];
|
|
source_memory_ids: string[];
|
|
source_memories: Array<{
|
|
id: string;
|
|
text: string;
|
|
type: string;
|
|
context?: string;
|
|
occurred_start?: string;
|
|
mentioned_at?: string;
|
|
}>;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>;
|
|
}>(`/api/banks/${bankId}/observations${query ? `?${query}` : ""}`);
|
|
}
|
|
|
|
/**
|
|
* Get an observation with source memories
|
|
*/
|
|
async getObservation(bankId: string, observationId: string) {
|
|
return this.fetchApi<{
|
|
id: string;
|
|
bank_id: string;
|
|
text: string;
|
|
proof_count: number;
|
|
history: Array<{
|
|
previous_text: string;
|
|
changed_at: string;
|
|
reason: string;
|
|
}>;
|
|
tags: string[];
|
|
source_memory_ids: string[];
|
|
source_memories: Array<{
|
|
id: string;
|
|
text: string;
|
|
type: string;
|
|
context?: string;
|
|
occurred_start?: string;
|
|
mentioned_at?: string;
|
|
}>;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}>(`/api/banks/${bankId}/observations/${observationId}`);
|
|
}
|
|
|
|
// ============= MENTAL MODELS (stored reflect responses) =============
|
|
|
|
/**
|
|
* List mental models for a bank
|
|
*/
|
|
async listMentalModels(bankId: string, tags?: string[], tagsMatch?: string) {
|
|
const params = new URLSearchParams();
|
|
if (tags && tags.length > 0) {
|
|
tags.forEach((t) => params.append("tags", t));
|
|
}
|
|
if (tagsMatch) {
|
|
params.append("tags_match", tagsMatch);
|
|
}
|
|
const query = params.toString();
|
|
return this.fetchApi<{
|
|
items: Array<{
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
source_query: string;
|
|
content: string;
|
|
tags: string[];
|
|
max_tokens: number;
|
|
trigger: { refresh_after_consolidation: boolean };
|
|
last_refreshed_at: string;
|
|
created_at: string;
|
|
reflect_response?: {
|
|
text: string;
|
|
based_on: Record<string, Array<{ id: string; text: string; type: string }>>;
|
|
};
|
|
}>;
|
|
}>(`/api/banks/${bankId}/mental-models${query ? `?${query}` : ""}`);
|
|
}
|
|
|
|
/**
|
|
* Create a mental model (async - content auto-generated in background)
|
|
* Returns operation_id to track progress
|
|
*/
|
|
async createMentalModel(
|
|
bankId: string,
|
|
params: {
|
|
id?: string;
|
|
name: string;
|
|
source_query: string;
|
|
tags?: string[];
|
|
max_tokens?: number;
|
|
trigger?: { refresh_after_consolidation: boolean };
|
|
}
|
|
) {
|
|
return this.fetchApi<{
|
|
operation_id: string;
|
|
}>(`/api/banks/${bankId}/mental-models`, {
|
|
method: "POST",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a mental model
|
|
*/
|
|
async getMentalModel(bankId: string, mentalModelId: string): Promise<MentalModel> {
|
|
return this.fetchApi<MentalModel>(`/api/banks/${bankId}/mental-models/${mentalModelId}`);
|
|
}
|
|
|
|
/**
|
|
* Update a mental model
|
|
*/
|
|
async updateMentalModel(
|
|
bankId: string,
|
|
mentalModelId: string,
|
|
params: {
|
|
name?: string;
|
|
source_query?: string;
|
|
max_tokens?: number;
|
|
tags?: string[];
|
|
trigger?: { refresh_after_consolidation: boolean };
|
|
}
|
|
) {
|
|
return this.fetchApi<{
|
|
id: string;
|
|
bank_id: string;
|
|
name: string;
|
|
source_query: string;
|
|
content: string;
|
|
tags: string[];
|
|
max_tokens: number;
|
|
trigger: { refresh_after_consolidation: boolean };
|
|
last_refreshed_at: string;
|
|
created_at: string;
|
|
reflect_response?: {
|
|
text: string;
|
|
based_on: Record<string, Array<{ id: string; text: string; type: string }>>;
|
|
};
|
|
}>(`/api/banks/${bankId}/mental-models/${mentalModelId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a mental model
|
|
*/
|
|
async deleteMentalModel(bankId: string, mentalModelId: string) {
|
|
return this.fetchApi(`/api/banks/${bankId}/mental-models/${mentalModelId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refresh a mental model (re-run source query) - async operation
|
|
*/
|
|
async refreshMentalModel(bankId: string, mentalModelId: string) {
|
|
return this.fetchApi<{
|
|
operation_id: string;
|
|
}>(`/api/banks/${bankId}/mental-models/${mentalModelId}/refresh`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the refresh history of a mental model
|
|
*/
|
|
async getMentalModelHistory(bankId: string, mentalModelId: string) {
|
|
return this.fetchApi<
|
|
{
|
|
previous_content: string | null;
|
|
changed_at: string;
|
|
}[]
|
|
>(`/api/banks/${bankId}/mental-models/${mentalModelId}/history`);
|
|
}
|
|
|
|
/**
|
|
* Get API version and feature flags
|
|
* Use this to check which capabilities are available in the dataplane
|
|
*/
|
|
async getVersion() {
|
|
return this.fetchApi<{
|
|
api_version: string;
|
|
features: {
|
|
observations: boolean;
|
|
mcp: boolean;
|
|
worker: boolean;
|
|
bank_config_api: boolean;
|
|
file_upload_api: boolean;
|
|
};
|
|
}>("/api/version");
|
|
}
|
|
|
|
/**
|
|
* Upload files for retain (uses file conversion API)
|
|
* Requires file_upload_api feature flag to be enabled
|
|
* Converter is configured server-side via HINDSIGHT_API_FILE_CONVERTER
|
|
*/
|
|
async uploadFiles(params: {
|
|
bank_id: string;
|
|
files: File[];
|
|
document_tags?: string[];
|
|
async?: boolean;
|
|
files_metadata?: Array<{
|
|
document_id?: string;
|
|
context?: string;
|
|
metadata?: Record<string, any>;
|
|
tags?: string[];
|
|
timestamp?: string;
|
|
strategy?: string;
|
|
}>;
|
|
}) {
|
|
const formData = new FormData();
|
|
|
|
// Add files
|
|
params.files.forEach((file) => {
|
|
formData.append("files", file);
|
|
});
|
|
|
|
// Add request JSON (including bank_id)
|
|
const requestData: any = {
|
|
bank_id: params.bank_id,
|
|
async: params.async ?? true,
|
|
};
|
|
if (params.document_tags) requestData.document_tags = params.document_tags;
|
|
if (params.files_metadata) requestData.files_metadata = params.files_metadata;
|
|
|
|
formData.append("request", JSON.stringify(requestData));
|
|
|
|
// Use fetch directly for multipart/form-data
|
|
const response = await fetch(`/api/files/retain`, {
|
|
method: "POST",
|
|
body: formData,
|
|
// Don't set Content-Type - browser will set it with boundary
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = `HTTP ${response.status}`;
|
|
try {
|
|
const errorData = await response.json();
|
|
errorMessage = errorData.error || errorMessage;
|
|
} catch {
|
|
// Ignore parse errors
|
|
}
|
|
const error = new Error(errorMessage);
|
|
(error as any).status = response.status;
|
|
throw error;
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Get bank configuration (resolved with hierarchy)
|
|
*/
|
|
async getBankConfig(bankId: string) {
|
|
return this.fetchApi<{
|
|
bank_id: string;
|
|
config: Record<string, any>;
|
|
overrides: Record<string, any>;
|
|
}>(`/api/banks/${bankId}/config`);
|
|
}
|
|
|
|
/**
|
|
* Update bank configuration overrides
|
|
*/
|
|
async updateBankConfig(bankId: string, updates: Record<string, any>) {
|
|
return this.fetchApi<{
|
|
bank_id: string;
|
|
config: Record<string, any>;
|
|
overrides: Record<string, any>;
|
|
}>(`/api/banks/${bankId}/config`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ updates }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reset bank configuration to defaults
|
|
*/
|
|
async resetBankConfig(bankId: string) {
|
|
return this.fetchApi<{
|
|
bank_id: string;
|
|
config: Record<string, any>;
|
|
overrides: Record<string, any>;
|
|
}>(`/api/banks/${bankId}/config`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List webhooks for a bank
|
|
*/
|
|
async listWebhooks(bankId: string): Promise<{ items: Webhook[] }> {
|
|
return this.fetchApi<{ items: Webhook[] }>(`/api/banks/${bankId}/webhooks`);
|
|
}
|
|
|
|
/**
|
|
* Create a webhook
|
|
*/
|
|
async createWebhook(
|
|
bankId: string,
|
|
params: {
|
|
url: string;
|
|
secret?: string;
|
|
event_types?: string[];
|
|
enabled?: boolean;
|
|
http_config?: WebhookHttpConfig;
|
|
}
|
|
): Promise<Webhook> {
|
|
return this.fetchApi<Webhook>(`/api/banks/${bankId}/webhooks`, {
|
|
method: "POST",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update a webhook (PATCH — only provided fields are changed)
|
|
*/
|
|
async updateWebhook(
|
|
bankId: string,
|
|
webhookId: string,
|
|
params: {
|
|
url?: string;
|
|
secret?: string | null;
|
|
event_types?: string[];
|
|
enabled?: boolean;
|
|
http_config?: WebhookHttpConfig;
|
|
}
|
|
): Promise<Webhook> {
|
|
return this.fetchApi<Webhook>(`/api/banks/${bankId}/webhooks/${webhookId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(params),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a webhook
|
|
*/
|
|
async deleteWebhook(bankId: string, webhookId: string): Promise<{ success: boolean }> {
|
|
return this.fetchApi<{ success: boolean }>(`/api/banks/${bankId}/webhooks/${webhookId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List webhook deliveries
|
|
*/
|
|
async listWebhookDeliveries(
|
|
bankId: string,
|
|
webhookId: string,
|
|
limit?: number,
|
|
cursor?: string
|
|
): Promise<{ items: WebhookDelivery[]; next_cursor: string | null }> {
|
|
const params = new URLSearchParams();
|
|
if (limit) params.append("limit", limit.toString());
|
|
if (cursor) params.append("cursor", cursor);
|
|
const query = params.toString();
|
|
return this.fetchApi<{ items: WebhookDelivery[]; next_cursor: string | null }>(
|
|
`/api/banks/${bankId}/webhooks/${webhookId}/deliveries${query ? `?${query}` : ""}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const client = new ControlPlaneClient();
|