Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ddf8bf535 | ||
|
|
4eeb8282a8 | ||
|
|
e97a5c9a6e | ||
|
|
a50567f864 | ||
|
|
461b00d4d9 | ||
|
|
4bc772d8f8 | ||
|
|
99a8978905 | ||
|
|
91106f30ef | ||
|
|
5b1c3486f3 | ||
|
|
685e4cf0ef | ||
|
|
73a0ad0cc3 | ||
|
|
f7cc9ad663 | ||
|
|
843dcec77b | ||
|
|
ae0e3cec8d | ||
|
|
a9967627ae | ||
|
|
f6d659c927 | ||
|
|
794b83d839 | ||
|
|
dcc2d69d6f | ||
|
|
4ba2fffe8d | ||
|
|
0cffa43cbd | ||
|
|
70677457d9 | ||
|
|
ee2d8f7540 | ||
|
|
b962de50d8 | ||
|
|
130bb2d616 | ||
|
|
b0c1afb19f | ||
|
|
9654a06e22 |
+6
-1
@@ -2,7 +2,7 @@
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# LLM Configuration (Required)
|
||||
# Supported providers: openai, groq, ollama, gemini, anthropic, lmstudio, vertexai, minimax, volcano
|
||||
# Supported providers: openai, groq, ollama, gemini, anthropic, lmstudio, vertexai, minimax, deepseek, volcano
|
||||
HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
HINDSIGHT_API_LLM_API_KEY=your-api-key-here
|
||||
HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
|
||||
@@ -25,6 +25,11 @@ HINDSIGHT_API_LLM_BASE_URL=https://api.openai.com/v1
|
||||
# HINDSIGHT_API_LLM_API_KEY=your-minimax-api-key
|
||||
# HINDSIGHT_API_LLM_MODEL=MiniMax-M2.7
|
||||
|
||||
# Example: DeepSeek configuration (https://api.deepseek.com)
|
||||
# HINDSIGHT_API_LLM_PROVIDER=deepseek
|
||||
# HINDSIGHT_API_LLM_API_KEY=your-deepseek-api-key
|
||||
# HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash # or deepseek-v4-pro / deepseek-chat / deepseek-reasoner
|
||||
|
||||
# Example: LM Studio local configuration (Qwen 2.5 32B recommended)
|
||||
# HINDSIGHT_API_LLM_PROVIDER=lmstudio
|
||||
# HINDSIGHT_API_LLM_API_KEY=lmstudio
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Upload perf results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: perf-results-${{ github.sha }}
|
||||
path: hindsight-dev/perf-results.json
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
|
||||
- name: Upload LoComo results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: locomo-results-${{ github.sha }}
|
||||
path: hindsight-dev/benchmarks/locomo/results/
|
||||
|
||||
@@ -50,6 +50,7 @@ jobs:
|
||||
integrations-cloudflare-oauth-proxy: ${{ steps.filter.outputs.integrations-cloudflare-oauth-proxy }}
|
||||
integrations-lockfiles: ${{ steps.filter.outputs.integrations-lockfiles }}
|
||||
integrations-openai-agents: ${{ steps.filter.outputs.integrations-openai-agents }}
|
||||
integrations-pipecat: ${{ steps.filter.outputs.integrations-pipecat }}
|
||||
dev: ${{ steps.filter.outputs.dev }}
|
||||
ci: ${{ steps.filter.outputs.ci }}
|
||||
# Secrets are available for internal PRs, pull_request_review, and workflow_dispatch.
|
||||
@@ -136,6 +137,8 @@ jobs:
|
||||
- 'scripts/check-integration-lockfiles.sh'
|
||||
integrations-openai-agents:
|
||||
- 'hindsight-integrations/openai-agents/**'
|
||||
integrations-pipecat:
|
||||
- 'hindsight-integrations/pipecat/**'
|
||||
dev:
|
||||
- 'hindsight-dev/**'
|
||||
ci:
|
||||
@@ -599,6 +602,44 @@ jobs:
|
||||
working-directory: ./hindsight-integrations/paperclip
|
||||
run: npm test
|
||||
|
||||
test-pipecat-integration:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
github.event_name != 'pull_request_review' &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
needs.detect-changes.outputs.integrations-pipecat == 'true' ||
|
||||
needs.detect-changes.outputs.ci == 'true')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build pipecat integration
|
||||
working-directory: ./hindsight-integrations/pipecat
|
||||
run: uv build
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-integrations/pipecat
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./hindsight-integrations/pipecat
|
||||
run: uv run pytest tests -v
|
||||
|
||||
|
||||
build-control-plane:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
@@ -2908,6 +2949,7 @@ jobs:
|
||||
- test-cloudflare-oauth-proxy-integration
|
||||
- build-chat-integration
|
||||
- test-paperclip-integration
|
||||
- test-pipecat-integration
|
||||
- build-control-plane
|
||||
- build-docs
|
||||
- test-rust-cli
|
||||
@@ -2943,7 +2985,7 @@ jobs:
|
||||
steps:
|
||||
- name: Determine overall result
|
||||
id: result
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const needs = ${{ toJSON(needs) }};
|
||||
@@ -2976,7 +3018,7 @@ jobs:
|
||||
core.setOutput('run_url', runUrl);
|
||||
|
||||
- name: Report status to PR
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.repos.createCommitStatus({
|
||||
@@ -2990,7 +3032,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: Comment on PR
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
+10
-10
@@ -18,17 +18,17 @@ npm install @vectorize-io/hindsight-all @vectorize-io/hindsight-client
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { HindsightServer, consoleLogger } from '@vectorize-io/hindsight-all';
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
import { HindsightServer, consoleLogger } from "@vectorize-io/hindsight-all";
|
||||
import { HindsightClient } from "@vectorize-io/hindsight-client";
|
||||
|
||||
const server = new HindsightServer({
|
||||
profile: 'my-app',
|
||||
profile: "my-app",
|
||||
port: 9077,
|
||||
env: {
|
||||
HINDSIGHT_API_LLM_PROVIDER: 'anthropic',
|
||||
HINDSIGHT_API_LLM_PROVIDER: "anthropic",
|
||||
HINDSIGHT_API_LLM_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
HINDSIGHT_API_LLM_MODEL: 'claude-sonnet-4-20250514',
|
||||
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: '0',
|
||||
HINDSIGHT_API_LLM_MODEL: "claude-sonnet-4-20250514",
|
||||
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: "0",
|
||||
},
|
||||
logger: consoleLogger,
|
||||
});
|
||||
@@ -37,11 +37,11 @@ await server.start();
|
||||
|
||||
const client = new HindsightClient({ baseUrl: server.getBaseUrl() });
|
||||
|
||||
await client.retain('user-123', 'User prefers dark mode and concise answers.', {
|
||||
documentId: 'pref-2026-04-01',
|
||||
await client.retain("user-123", "User prefers dark mode and concise answers.", {
|
||||
documentId: "pref-2026-04-01",
|
||||
});
|
||||
|
||||
const recall = await client.recall('user-123', 'what are the user preferences?');
|
||||
const recall = await client.recall("user-123", "what are the user preferences?");
|
||||
console.log(recall.results);
|
||||
|
||||
await server.stop();
|
||||
@@ -62,7 +62,7 @@ If you're hacking on the Python `hindsight-embed` package in the same monorepo,
|
||||
|
||||
```ts
|
||||
new HindsightServer({
|
||||
embedPackagePath: '/path/to/hindsight-embed',
|
||||
embedPackagePath: "/path/to/hindsight-embed",
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getEmbedCommand } from './command.js';
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getEmbedCommand } from "./command.js";
|
||||
|
||||
describe('getEmbedCommand', () => {
|
||||
it('defaults to uvx hindsight-embed@latest', () => {
|
||||
expect(getEmbedCommand()).toEqual(['uvx', 'hindsight-embed@latest']);
|
||||
describe("getEmbedCommand", () => {
|
||||
it("defaults to uvx hindsight-embed@latest", () => {
|
||||
expect(getEmbedCommand()).toEqual(["uvx", "hindsight-embed@latest"]);
|
||||
});
|
||||
|
||||
it('honours an explicit version', () => {
|
||||
expect(getEmbedCommand({ embedVersion: '0.5.0' })).toEqual(['uvx', '[email protected]']);
|
||||
it("honours an explicit version", () => {
|
||||
expect(getEmbedCommand({ embedVersion: "0.5.0" })).toEqual(["uvx", "[email protected]"]);
|
||||
});
|
||||
|
||||
it('treats an empty version as latest', () => {
|
||||
expect(getEmbedCommand({ embedVersion: '' })).toEqual(['uvx', 'hindsight-embed@latest']);
|
||||
it("treats an empty version as latest", () => {
|
||||
expect(getEmbedCommand({ embedVersion: "" })).toEqual(["uvx", "hindsight-embed@latest"]);
|
||||
});
|
||||
|
||||
it('uses uv run --directory when a local path is given', () => {
|
||||
expect(getEmbedCommand({ embedPackagePath: '/abs/path' })).toEqual([
|
||||
'uv',
|
||||
'run',
|
||||
'--directory',
|
||||
'/abs/path',
|
||||
'hindsight-embed',
|
||||
it("uses uv run --directory when a local path is given", () => {
|
||||
expect(getEmbedCommand({ embedPackagePath: "/abs/path" })).toEqual([
|
||||
"uv",
|
||||
"run",
|
||||
"--directory",
|
||||
"/abs/path",
|
||||
"hindsight-embed",
|
||||
]);
|
||||
});
|
||||
|
||||
it('local path takes precedence over version', () => {
|
||||
expect(
|
||||
getEmbedCommand({ embedPackagePath: '/abs/path', embedVersion: '0.5.0' }),
|
||||
).toEqual(['uv', 'run', '--directory', '/abs/path', 'hindsight-embed']);
|
||||
it("local path takes precedence over version", () => {
|
||||
expect(getEmbedCommand({ embedPackagePath: "/abs/path", embedVersion: "0.5.0" })).toEqual([
|
||||
"uv",
|
||||
"run",
|
||||
"--directory",
|
||||
"/abs/path",
|
||||
"hindsight-embed",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@ export interface EmbedCommandOptions {
|
||||
|
||||
export function getEmbedCommand(opts: EmbedCommandOptions = {}): string[] {
|
||||
if (opts.embedPackagePath) {
|
||||
return ['uv', 'run', '--directory', opts.embedPackagePath, 'hindsight-embed'];
|
||||
return ["uv", "run", "--directory", opts.embedPackagePath, "hindsight-embed"];
|
||||
}
|
||||
const version = opts.embedVersion && opts.embedVersion.length > 0 ? opts.embedVersion : 'latest';
|
||||
return ['uvx', `hindsight-embed@${version}`];
|
||||
const version = opts.embedVersion && opts.embedVersion.length > 0 ? opts.embedVersion : "latest";
|
||||
return ["uvx", `hindsight-embed@${version}`];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { HindsightServer } from './server.js';
|
||||
export { getEmbedCommand } from './command.js';
|
||||
export { silentLogger, consoleLogger } from './logger.js';
|
||||
export { HindsightServer } from "./server.js";
|
||||
export { getEmbedCommand } from "./command.js";
|
||||
export { silentLogger, consoleLogger } from "./logger.js";
|
||||
|
||||
export type { Logger } from './logger.js';
|
||||
export type { EmbedCommandOptions } from './command.js';
|
||||
export type { HindsightServerOptions } from './types.js';
|
||||
export type { Logger } from "./logger.js";
|
||||
export type { EmbedCommandOptions } from "./command.js";
|
||||
export type { HindsightServerOptions } from "./types.js";
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HindsightServer } from './server.js';
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { HindsightServer } from "./server.js";
|
||||
|
||||
describe('HindsightServer construction', () => {
|
||||
it('defaults base URL to http://127.0.0.1:8888', () => {
|
||||
describe("HindsightServer construction", () => {
|
||||
it("defaults base URL to http://127.0.0.1:8888", () => {
|
||||
const server = new HindsightServer();
|
||||
expect(server.getBaseUrl()).toBe('http://127.0.0.1:8888');
|
||||
expect(server.getProfile()).toBe('default');
|
||||
expect(server.getBaseUrl()).toBe("http://127.0.0.1:8888");
|
||||
expect(server.getProfile()).toBe("default");
|
||||
});
|
||||
|
||||
it('honours custom profile, port, and host', () => {
|
||||
const server = new HindsightServer({ profile: 'app', port: 9077, host: '0.0.0.0' });
|
||||
expect(server.getProfile()).toBe('app');
|
||||
expect(server.getBaseUrl()).toBe('http://0.0.0.0:9077');
|
||||
it("honours custom profile, port, and host", () => {
|
||||
const server = new HindsightServer({ profile: "app", port: 9077, host: "0.0.0.0" });
|
||||
expect(server.getProfile()).toBe("app");
|
||||
expect(server.getBaseUrl()).toBe("http://0.0.0.0:9077");
|
||||
});
|
||||
|
||||
it('accepts open env pass-through without complaining about unknown keys', () => {
|
||||
it("accepts open env pass-through without complaining about unknown keys", () => {
|
||||
const server = new HindsightServer({
|
||||
env: {
|
||||
HINDSIGHT_API_LLM_PROVIDER: 'openai',
|
||||
HINDSIGHT_API_LLM_MODEL: 'gpt-4o-mini',
|
||||
HINDSIGHT_API_LLM_PROVIDER: "openai",
|
||||
HINDSIGHT_API_LLM_MODEL: "gpt-4o-mini",
|
||||
// A field that does not exist today — should still be accepted
|
||||
HINDSIGHT_FUTURE_FLAG: 'enabled',
|
||||
HINDSIGHT_FUTURE_FLAG: "enabled",
|
||||
},
|
||||
});
|
||||
expect(server).toBeInstanceOf(HindsightServer);
|
||||
});
|
||||
|
||||
it('exposes checkHealth that returns false when no daemon is running', async () => {
|
||||
it("exposes checkHealth that returns false when no daemon is running", async () => {
|
||||
// Random high port that nothing is listening on.
|
||||
const server = new HindsightServer({ port: 1, readyTimeoutMs: 100 });
|
||||
const healthy = await server.checkHealth();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { getEmbedCommand } from './command.js';
|
||||
import { silentLogger } from './logger.js';
|
||||
import type { Logger } from './logger.js';
|
||||
import type { HindsightServerOptions } from './types.js';
|
||||
import { spawn } from "child_process";
|
||||
import { getEmbedCommand } from "./command.js";
|
||||
import { silentLogger } from "./logger.js";
|
||||
import type { Logger } from "./logger.js";
|
||||
import type { HindsightServerOptions } from "./types.js";
|
||||
|
||||
const DEFAULT_PORT = 8888;
|
||||
const DEFAULT_HOST = '127.0.0.1';
|
||||
const DEFAULT_PROFILE = 'default';
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const DEFAULT_PROFILE = "default";
|
||||
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
||||
const DEFAULT_READY_POLL_INTERVAL_MS = 1_000;
|
||||
|
||||
@@ -61,7 +61,7 @@ export class HindsightServer {
|
||||
this.userEnv = opts.env ?? {};
|
||||
this.extraProfileCreateArgs = opts.extraProfileCreateArgs ?? [];
|
||||
this.extraDaemonStartArgs = opts.extraDaemonStartArgs ?? [];
|
||||
this.platformCpuWorkaround = opts.platformCpuWorkaround ?? (process.platform === 'darwin');
|
||||
this.platformCpuWorkaround = opts.platformCpuWorkaround ?? process.platform === "darwin";
|
||||
this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
||||
this.readyPollIntervalMs = opts.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS;
|
||||
this.logger = opts.logger ?? silentLogger;
|
||||
@@ -100,22 +100,22 @@ export class HindsightServer {
|
||||
embedVersion: this.embedVersion,
|
||||
embedPackagePath: this.embedPackagePath,
|
||||
});
|
||||
const args = [...baseArgs, 'daemon', '--profile', this.profile, 'stop'];
|
||||
const args = [...baseArgs, "daemon", "--profile", this.profile, "stop"];
|
||||
|
||||
const child = spawn(cmd, args, { stdio: 'pipe' });
|
||||
this.pipeOutput(child, 'daemon.stop');
|
||||
const child = spawn(cmd, args, { stdio: "pipe" });
|
||||
this.pipeOutput(child, "daemon.stop");
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.logger.warn(`[hindsight] daemon stop timed out after 5s`);
|
||||
resolve();
|
||||
}, 5_000);
|
||||
child.on('exit', () => {
|
||||
child.on("exit", () => {
|
||||
clearTimeout(timeout);
|
||||
this.logger.info(`[hindsight] daemon stopped`);
|
||||
resolve();
|
||||
});
|
||||
child.on('error', (err) => {
|
||||
child.on("error", (err) => {
|
||||
clearTimeout(timeout);
|
||||
this.logger.warn(`[hindsight] error stopping daemon: ${err.message}`);
|
||||
resolve();
|
||||
@@ -147,9 +147,9 @@ export class HindsightServer {
|
||||
private buildEnv(): NodeJS.ProcessEnv {
|
||||
const merged: NodeJS.ProcessEnv = { ...process.env };
|
||||
|
||||
if (this.platformCpuWorkaround && process.platform === 'darwin') {
|
||||
merged['HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU'] = '1';
|
||||
merged['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
|
||||
if (this.platformCpuWorkaround && process.platform === "darwin") {
|
||||
merged["HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU"] = "1";
|
||||
merged["HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU"] = "1";
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(this.userEnv)) {
|
||||
@@ -175,11 +175,11 @@ export class HindsightServer {
|
||||
});
|
||||
const createArgs = [
|
||||
...baseArgs,
|
||||
'profile',
|
||||
'create',
|
||||
"profile",
|
||||
"create",
|
||||
this.profile,
|
||||
'--merge',
|
||||
'--port',
|
||||
"--merge",
|
||||
"--port",
|
||||
String(this.port),
|
||||
];
|
||||
|
||||
@@ -189,12 +189,12 @@ export class HindsightServer {
|
||||
// host state into profile config.
|
||||
const envForProfile = this.collectProfileEnv(env);
|
||||
for (const [key, value] of Object.entries(envForProfile)) {
|
||||
createArgs.push('--env', `${key}=${value}`);
|
||||
createArgs.push("--env", `${key}=${value}`);
|
||||
}
|
||||
|
||||
createArgs.push(...this.extraProfileCreateArgs);
|
||||
|
||||
await this.runCommand(cmd, createArgs, env, 'profile.create');
|
||||
await this.runCommand(cmd, createArgs, env, "profile.create");
|
||||
}
|
||||
|
||||
/** Collect only the env vars that should be written into the profile file. */
|
||||
@@ -209,10 +209,10 @@ export class HindsightServer {
|
||||
}
|
||||
|
||||
// 2. CPU workaround — only if auto-applied and not already overridden.
|
||||
if (this.platformCpuWorkaround && process.platform === 'darwin') {
|
||||
if (this.platformCpuWorkaround && process.platform === "darwin") {
|
||||
const cpuKeys = [
|
||||
'HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU',
|
||||
'HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU',
|
||||
"HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU",
|
||||
"HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU",
|
||||
];
|
||||
for (const key of cpuKeys) {
|
||||
if (!(key in out) && env[key] !== undefined) {
|
||||
@@ -231,14 +231,14 @@ export class HindsightServer {
|
||||
});
|
||||
const args = [
|
||||
...baseArgs,
|
||||
'daemon',
|
||||
'--profile',
|
||||
"daemon",
|
||||
"--profile",
|
||||
this.profile,
|
||||
'start',
|
||||
"start",
|
||||
...this.extraDaemonStartArgs,
|
||||
];
|
||||
|
||||
await this.runCommand(cmd, args, env, 'daemon.start');
|
||||
await this.runCommand(cmd, args, env, "daemon.start");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,34 +249,34 @@ export class HindsightServer {
|
||||
cmd: string,
|
||||
args: string[],
|
||||
env: NodeJS.ProcessEnv,
|
||||
label: string,
|
||||
label: string
|
||||
): Promise<void> {
|
||||
const child = spawn(cmd, args, { stdio: 'pipe', env });
|
||||
let output = '';
|
||||
child.stdout?.on('data', (data: Buffer) => {
|
||||
const child = spawn(cmd, args, { stdio: "pipe", env });
|
||||
let output = "";
|
||||
child.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
for (const line of text.trimEnd().split('\n')) {
|
||||
for (const line of text.trimEnd().split("\n")) {
|
||||
if (line) this.logger.info(`[hindsight:${label}] ${line}`);
|
||||
}
|
||||
});
|
||||
child.stderr?.on('data', (data: Buffer) => {
|
||||
child.stderr?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
for (const line of text.trimEnd().split('\n')) {
|
||||
for (const line of text.trimEnd().split("\n")) {
|
||||
if (line) this.logger.warn(`[hindsight:${label}] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.on('exit', (code) => {
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`${label} failed with code ${code}: ${output.trim()}`));
|
||||
}
|
||||
});
|
||||
child.on('error', (err) => {
|
||||
child.on("error", (err) => {
|
||||
reject(new Error(`${label} failed to spawn: ${err.message}`, { cause: err }));
|
||||
});
|
||||
});
|
||||
@@ -284,13 +284,13 @@ export class HindsightServer {
|
||||
|
||||
/** Stream a spawned child's stdout/stderr through the logger without blocking. */
|
||||
private pipeOutput(child: ReturnType<typeof spawn>, label: string): void {
|
||||
child.stdout?.on('data', (data: Buffer) => {
|
||||
for (const line of data.toString().trimEnd().split('\n')) {
|
||||
child.stdout?.on("data", (data: Buffer) => {
|
||||
for (const line of data.toString().trimEnd().split("\n")) {
|
||||
if (line) this.logger.info(`[hindsight:${label}] ${line}`);
|
||||
}
|
||||
});
|
||||
child.stderr?.on('data', (data: Buffer) => {
|
||||
for (const line of data.toString().trimEnd().split('\n')) {
|
||||
child.stderr?.on("data", (data: Buffer) => {
|
||||
for (const line of data.toString().trimEnd().split("\n")) {
|
||||
if (line) this.logger.warn(`[hindsight:${label}] ${line}`);
|
||||
}
|
||||
});
|
||||
@@ -316,7 +316,7 @@ export class HindsightServer {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.readyPollIntervalMs));
|
||||
}
|
||||
throw new Error(
|
||||
`Hindsight daemon did not become ready within ${this.readyTimeoutMs}ms at ${this.baseUrl}`,
|
||||
`Hindsight daemon did not become ready within ${this.readyTimeoutMs}ms at ${this.baseUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Logger } from './logger.js';
|
||||
import type { Logger } from "./logger.js";
|
||||
|
||||
/**
|
||||
* Options for {@link HindsightServer}.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
outDir: 'dist',
|
||||
outDir: "dist",
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
bundle: true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
include: ["src/**/*.test.ts"],
|
||||
environment: "node",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4323,7 +4323,8 @@ def _register_routes(app: FastAPI):
|
||||
response_model=ListTagsResponse,
|
||||
summary="List tags",
|
||||
description="List all unique tags in a memory bank with usage counts. "
|
||||
"Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.",
|
||||
"Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive. "
|
||||
"Use `source=mental_models` to list tags used on mental models instead of memories.",
|
||||
operation_id="list_tags",
|
||||
tags=["Memory"],
|
||||
)
|
||||
@@ -4334,6 +4335,10 @@ def _register_routes(app: FastAPI):
|
||||
description="Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). "
|
||||
"Use '*' as wildcard. Case-insensitive.",
|
||||
),
|
||||
source: Literal["memories", "mental_models"] = Query(
|
||||
default="memories",
|
||||
description="Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.",
|
||||
),
|
||||
limit: int = Query(default=100, description="Maximum number of tags to return"),
|
||||
offset: int = Query(default=0, description="Offset for pagination"),
|
||||
request_context: RequestContext = Depends(get_request_context),
|
||||
@@ -4350,17 +4355,27 @@ def _register_routes(app: FastAPI):
|
||||
Args:
|
||||
bank_id: Memory Bank ID (from path)
|
||||
q: Wildcard pattern to filter tags (use '*' as wildcard)
|
||||
source: Tag source — 'memories' (memory_units, default) or 'mental_models'
|
||||
limit: Maximum number of tags to return (default: 100)
|
||||
offset: Offset for pagination (default: 0)
|
||||
"""
|
||||
try:
|
||||
data = await app.state.memory.list_tags(
|
||||
bank_id=bank_id,
|
||||
pattern=q,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
request_context=request_context,
|
||||
)
|
||||
if source == "mental_models":
|
||||
data = await app.state.memory.list_mental_model_tags(
|
||||
bank_id=bank_id,
|
||||
pattern=q,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
request_context=request_context,
|
||||
)
|
||||
else:
|
||||
data = await app.state.memory.list_tags(
|
||||
bank_id=bank_id,
|
||||
pattern=q,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
request_context=request_context,
|
||||
)
|
||||
return data
|
||||
except OperationValidationError as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||
@@ -4666,7 +4681,7 @@ def _register_routes(app: FastAPI):
|
||||
"/v1/default/banks/{bank_id}/profile",
|
||||
response_model=BankProfileResponse,
|
||||
summary="Get memory bank profile",
|
||||
description="Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.",
|
||||
description="Get disposition traits and mission for a memory bank. Returns 404 if the bank does not exist.",
|
||||
operation_id="get_bank_profile",
|
||||
tags=["Banks"],
|
||||
deprecated=True,
|
||||
@@ -4674,7 +4689,15 @@ def _register_routes(app: FastAPI):
|
||||
async def api_get_bank_profile(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
||||
"""Get memory bank profile (disposition + mission)."""
|
||||
try:
|
||||
profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
# Read endpoints must not have create-as-side-effect: a client
|
||||
# holding onto a stale bank_id (e.g., a UI polling after the user
|
||||
# changed context) would otherwise silently re-create the bank in
|
||||
# an unrelated tenant. Surface a missing bank as 404.
|
||||
profile = await app.state.memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
if profile is None:
|
||||
raise HTTPException(status_code=404, detail=f"Bank '{bank_id}' not found")
|
||||
# Convert DispositionTraits object to dict for Pydantic
|
||||
disposition_dict = (
|
||||
profile["disposition"].model_dump()
|
||||
@@ -5010,8 +5033,10 @@ def _register_routes(app: FastAPI):
|
||||
):
|
||||
"""Export a bank's config and mental models as a template manifest."""
|
||||
try:
|
||||
# Authenticate and ensure bank exists
|
||||
profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
# Read endpoint: do not auto-create on missing bank.
|
||||
profile = await app.state.memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
if profile is None:
|
||||
raise HTTPException(status_code=404, detail=f"Bank '{bank_id}' not found")
|
||||
|
||||
@@ -6110,8 +6135,14 @@ def _register_routes(app: FastAPI):
|
||||
|
||||
pool = await app.state.memory._get_pool()
|
||||
|
||||
# Ensure bank exists
|
||||
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
# Read endpoint: verify bank exists without auto-creating it.
|
||||
if (
|
||||
await app.state.memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
is None
|
||||
):
|
||||
raise HTTPException(status_code=404, detail=f"Bank '{bank_id}' not found")
|
||||
|
||||
from hindsight_api.engine.db_utils import acquire_with_retry
|
||||
|
||||
@@ -6225,7 +6256,14 @@ def _register_routes(app: FastAPI):
|
||||
from hindsight_api.engine.memory_engine import fq_table
|
||||
|
||||
pool = await app.state.memory._get_pool()
|
||||
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
||||
# Read endpoint: verify bank exists without auto-creating it.
|
||||
if (
|
||||
await app.state.memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
is None
|
||||
):
|
||||
raise HTTPException(status_code=404, detail=f"Bank '{bank_id}' not found")
|
||||
|
||||
# Determine time range (always per-day buckets)
|
||||
from datetime import timedelta as _td
|
||||
|
||||
@@ -8,6 +8,7 @@ from contextvars import ContextVar
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api import __version__ as HINDSIGHT_VERSION
|
||||
from hindsight_api.config import _get_raw_config
|
||||
from hindsight_api.engine.memory_engine import _current_schema
|
||||
from hindsight_api.extensions import MCPExtension, load_extension
|
||||
@@ -89,7 +90,7 @@ def create_mcp_server(memory: MemoryEngine, multi_bank: bool = True) -> FastMCP:
|
||||
Returns:
|
||||
Configured FastMCP server instance
|
||||
"""
|
||||
mcp = FastMCP("hindsight-mcp-server")
|
||||
mcp = FastMCP("hindsight-mcp-server", version=HINDSIGHT_VERSION)
|
||||
|
||||
global_config = _get_raw_config()
|
||||
|
||||
|
||||
@@ -132,11 +132,13 @@ ENV_LLM_TIMEOUT = "HINDSIGHT_API_LLM_TIMEOUT"
|
||||
ENV_LLM_GROQ_SERVICE_TIER = "HINDSIGHT_API_LLM_GROQ_SERVICE_TIER"
|
||||
ENV_LLM_OPENAI_SERVICE_TIER = "HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER"
|
||||
ENV_LLM_EXTRA_BODY = "HINDSIGHT_API_LLM_EXTRA_BODY"
|
||||
ENV_LLM_SIMPLIFY_JSON_SCHEMA = "HINDSIGHT_API_LLM_SIMPLIFY_JSON_SCHEMA"
|
||||
|
||||
# Defaults for service tiers
|
||||
DEFAULT_LLM_GROQ_SERVICE_TIER = "auto" # "on_demand", "flex", or "auto"
|
||||
DEFAULT_LLM_OPENAI_SERVICE_TIER = None # None (default) or "flex" (50% cheaper)
|
||||
DEFAULT_LLM_EXTRA_BODY = None # None = no extra body params; JSON dict merged into OpenAI extra_body
|
||||
DEFAULT_LLM_SIMPLIFY_JSON_SCHEMA = True # Flatten $ref/$defs/anyOf in JSON schemas for better LLM compliance
|
||||
|
||||
# Per-operation LLM configuration (optional, falls back to global LLM config)
|
||||
ENV_RETAIN_LLM_PROVIDER = "HINDSIGHT_API_RETAIN_LLM_PROVIDER"
|
||||
@@ -245,6 +247,7 @@ ENV_RERANKER_TEI_HTTP_TIMEOUT = "HINDSIGHT_API_RERANKER_TEI_HTTP_TIMEOUT"
|
||||
ENV_RERANKER_MAX_CANDIDATES = "HINDSIGHT_API_RERANKER_MAX_CANDIDATES"
|
||||
ENV_RERANKER_FLASHRANK_MODEL = "HINDSIGHT_API_RERANKER_FLASHRANK_MODEL"
|
||||
ENV_RERANKER_FLASHRANK_CACHE_DIR = "HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR"
|
||||
ENV_RERANKER_FLASHRANK_CPU_MEM_ARENA = "HINDSIGHT_API_RERANKER_FLASHRANK_CPU_MEM_ARENA"
|
||||
|
||||
# ZeroEntropy configuration (reranker only)
|
||||
ENV_RERANKER_ZEROENTROPY_API_KEY = "HINDSIGHT_API_RERANKER_ZEROENTROPY_API_KEY"
|
||||
@@ -330,6 +333,7 @@ ENV_FILE_PARSER = "HINDSIGHT_API_FILE_PARSER"
|
||||
ENV_FILE_PARSER_ALLOWLIST = "HINDSIGHT_API_FILE_PARSER_ALLOWLIST"
|
||||
ENV_FILE_PARSER_IRIS_TOKEN = "HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN"
|
||||
ENV_FILE_PARSER_IRIS_ORG_ID = "HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID"
|
||||
ENV_FILE_PARSER_LLAMA_PARSE_API_KEY = "HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY"
|
||||
ENV_FILE_CONVERSION_MAX_BATCH_SIZE_MB = "HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB"
|
||||
ENV_FILE_CONVERSION_MAX_BATCH_SIZE = "HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE"
|
||||
ENV_ENABLE_FILE_UPLOAD_API = "HINDSIGHT_API_ENABLE_FILE_UPLOAD_API"
|
||||
@@ -345,6 +349,7 @@ ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS = "HINDSIGHT_API_CONSOLIDATION_SOURCE_
|
||||
ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION = (
|
||||
"HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION"
|
||||
)
|
||||
ENV_CONSOLIDATION_RECALL_BUDGET = "HINDSIGHT_API_CONSOLIDATION_RECALL_BUDGET"
|
||||
ENV_CONSOLIDATION_MAX_ATTEMPTS = "HINDSIGHT_API_CONSOLIDATION_MAX_ATTEMPTS"
|
||||
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
|
||||
ENV_MAX_OBSERVATIONS_PER_SCOPE = "HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE"
|
||||
@@ -443,6 +448,7 @@ PROVIDER_DEFAULT_MODELS = {
|
||||
"gemini": "gemini-2.5-flash",
|
||||
"groq": "openai/gpt-oss-120b",
|
||||
"minimax": "MiniMax-M2.7",
|
||||
"deepseek": "deepseek-v4-flash",
|
||||
"ollama": "gemma3:12b",
|
||||
"llamacpp": "gemma-4-e2b-it",
|
||||
"lmstudio": "local-model",
|
||||
@@ -505,6 +511,7 @@ DEFAULT_RERANKER_TEI_HTTP_TIMEOUT = 30.0 # HTTP timeout for TEI reranker reques
|
||||
DEFAULT_RERANKER_MAX_CANDIDATES = 300
|
||||
DEFAULT_RERANKER_FLASHRANK_MODEL = "ms-marco-MiniLM-L-12-v2" # Best balance of speed and quality
|
||||
DEFAULT_RERANKER_FLASHRANK_CACHE_DIR = None # Use default cache directory
|
||||
DEFAULT_RERANKER_FLASHRANK_CPU_MEM_ARENA = False # Disable ONNX CPU memory arena to bound RSS
|
||||
|
||||
DEFAULT_EMBEDDINGS_COHERE_MODEL = "embed-english-v3.0"
|
||||
DEFAULT_RERANKER_COHERE_MODEL = "rerank-english-v3.0"
|
||||
@@ -594,9 +601,10 @@ DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = (
|
||||
)
|
||||
DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 8 # Facts per LLM call (1 = no batching; >1 = batch mode)
|
||||
DEFAULT_CONSOLIDATION_MAX_TOKENS = 512 # Max tokens for recall when finding related observations
|
||||
DEFAULT_CONSOLIDATION_RECALL_BUDGET = "low" # Budget level for consolidation recall (low/mid/high)
|
||||
DEFAULT_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS = (
|
||||
-1
|
||||
) # Total token budget for source facts in consolidation recall (-1 = unlimited)
|
||||
4096 # Total token budget for source facts in consolidation recall (-1 = unlimited)
|
||||
)
|
||||
DEFAULT_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION = (
|
||||
256 # Max tokens of source facts per observation in consolidation prompt (-1 = unlimited)
|
||||
)
|
||||
@@ -836,6 +844,7 @@ class HindsightConfig:
|
||||
llm_extra_body: (
|
||||
dict | None
|
||||
) # Extra body params merged into OpenAI-compatible API calls (e.g. {"chat_template_kwargs": {"enable_thinking": true}})
|
||||
llm_simplify_json_schema: bool # Flatten $ref/$defs/anyOf in JSON schemas for better LLM compliance (default: true)
|
||||
|
||||
# Vertex AI configuration
|
||||
llm_vertexai_project_id: str | None
|
||||
@@ -1005,6 +1014,7 @@ class HindsightConfig:
|
||||
file_parser_allowlist: list[str] | None # Parsers clients may request (None = all registered)
|
||||
file_parser_iris_token: str | None # Vectorize API token for iris parser (VECTORIZE_TOKEN)
|
||||
file_parser_iris_org_id: str | None # Vectorize org ID for iris parser (VECTORIZE_ORG_ID)
|
||||
file_parser_llama_parse_api_key: str | None # LlamaCloud API key for llama_parse parser
|
||||
file_conversion_max_batch_size_mb: int # Max total batch size in MB (all files combined)
|
||||
file_conversion_max_batch_size: int # Max files per request
|
||||
enable_file_upload_api: bool
|
||||
@@ -1018,6 +1028,7 @@ class HindsightConfig:
|
||||
consolidation_max_memories_per_round: int
|
||||
consolidation_llm_batch_size: int
|
||||
consolidation_max_tokens: int
|
||||
consolidation_recall_budget: str
|
||||
consolidation_source_facts_max_tokens: int
|
||||
consolidation_source_facts_max_tokens_per_observation: int
|
||||
consolidation_max_attempts: int
|
||||
@@ -1143,6 +1154,7 @@ class HindsightConfig:
|
||||
"file_storage_azure_account_key",
|
||||
# File parser credentials
|
||||
"file_parser_iris_token",
|
||||
"file_parser_llama_parse_api_key",
|
||||
}
|
||||
|
||||
# CONFIGURABLE_FIELDS: Safe behavioral settings that can be customized per-tenant/bank
|
||||
@@ -1330,6 +1342,10 @@ class HindsightConfig:
|
||||
llm_groq_service_tier=os.getenv(ENV_LLM_GROQ_SERVICE_TIER, DEFAULT_LLM_GROQ_SERVICE_TIER),
|
||||
llm_openai_service_tier=os.getenv(ENV_LLM_OPENAI_SERVICE_TIER, DEFAULT_LLM_OPENAI_SERVICE_TIER),
|
||||
llm_extra_body=json.loads(os.getenv(ENV_LLM_EXTRA_BODY, "null")),
|
||||
llm_simplify_json_schema=os.getenv(
|
||||
ENV_LLM_SIMPLIFY_JSON_SCHEMA, str(DEFAULT_LLM_SIMPLIFY_JSON_SCHEMA)
|
||||
).lower()
|
||||
in ("true", "1"),
|
||||
# Vertex AI
|
||||
llm_vertexai_project_id=os.getenv(ENV_LLM_VERTEXAI_PROJECT_ID) or DEFAULT_LLM_VERTEXAI_PROJECT_ID,
|
||||
llm_vertexai_region=os.getenv(ENV_LLM_VERTEXAI_REGION, DEFAULT_LLM_VERTEXAI_REGION),
|
||||
@@ -1625,6 +1641,7 @@ class HindsightConfig:
|
||||
else None,
|
||||
file_parser_iris_token=os.getenv(ENV_FILE_PARSER_IRIS_TOKEN) or None,
|
||||
file_parser_iris_org_id=os.getenv(ENV_FILE_PARSER_IRIS_ORG_ID) or None,
|
||||
file_parser_llama_parse_api_key=os.getenv(ENV_FILE_PARSER_LLAMA_PARSE_API_KEY) or None,
|
||||
file_conversion_max_batch_size_mb=int(
|
||||
os.getenv(ENV_FILE_CONVERSION_MAX_BATCH_SIZE_MB, str(DEFAULT_FILE_CONVERSION_MAX_BATCH_SIZE_MB))
|
||||
),
|
||||
@@ -1662,6 +1679,7 @@ class HindsightConfig:
|
||||
consolidation_max_tokens=int(
|
||||
os.getenv(ENV_CONSOLIDATION_MAX_TOKENS, str(DEFAULT_CONSOLIDATION_MAX_TOKENS))
|
||||
),
|
||||
consolidation_recall_budget=os.getenv(ENV_CONSOLIDATION_RECALL_BUDGET, DEFAULT_CONSOLIDATION_RECALL_BUDGET),
|
||||
consolidation_source_facts_max_tokens=int(
|
||||
os.getenv(ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS, str(DEFAULT_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS))
|
||||
),
|
||||
|
||||
@@ -28,7 +28,7 @@ from pydantic import BaseModel, field_validator
|
||||
|
||||
from ...config import get_config
|
||||
from ..llm_wrapper import sanitize_llm_output
|
||||
from ..memory_engine import fq_table
|
||||
from ..memory_engine import Budget, fq_table
|
||||
from ..retain import embedding_utils
|
||||
from .prompts import build_batch_consolidation_prompt
|
||||
|
||||
@@ -1138,10 +1138,14 @@ async def _find_related_observations(
|
||||
else:
|
||||
recall_span = None
|
||||
|
||||
# Resolve budget: consolidation doesn't need deep recall, default to LOW to reduce memory fan-out
|
||||
recall_budget = Budget(config.consolidation_recall_budget)
|
||||
|
||||
try:
|
||||
recall_result = await memory_engine.recall_async(
|
||||
bank_id=bank_id,
|
||||
query=query,
|
||||
budget=recall_budget,
|
||||
max_tokens=config.consolidation_max_tokens, # Token budget for observations (configurable)
|
||||
fact_type=["observation"], # Only retrieve observations
|
||||
request_context=request_context,
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..config import (
|
||||
DEFAULT_LITELLM_API_BASE,
|
||||
DEFAULT_RERANKER_COHERE_MODEL,
|
||||
DEFAULT_RERANKER_FLASHRANK_CACHE_DIR,
|
||||
DEFAULT_RERANKER_FLASHRANK_CPU_MEM_ARENA,
|
||||
DEFAULT_RERANKER_FLASHRANK_MODEL,
|
||||
DEFAULT_RERANKER_GOOGLE_MODEL,
|
||||
DEFAULT_RERANKER_LITELLM_MAX_TOKENS_PER_DOC,
|
||||
@@ -39,6 +40,7 @@ from ..config import (
|
||||
ENV_RERANKER_COHERE_API_KEY,
|
||||
ENV_RERANKER_COHERE_MODEL,
|
||||
ENV_RERANKER_FLASHRANK_CACHE_DIR,
|
||||
ENV_RERANKER_FLASHRANK_CPU_MEM_ARENA,
|
||||
ENV_RERANKER_FLASHRANK_MODEL,
|
||||
ENV_RERANKER_GOOGLE_PROJECT_ID,
|
||||
ENV_RERANKER_LITELLM_SDK_API_KEY,
|
||||
@@ -864,6 +866,7 @@ class FlashRankCrossEncoder(CrossEncoderModel):
|
||||
cache_dir: str | None = None,
|
||||
max_length: int = 512,
|
||||
max_concurrent: int = 4,
|
||||
cpu_mem_arena: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize FlashRank cross-encoder.
|
||||
@@ -873,10 +876,15 @@ class FlashRankCrossEncoder(CrossEncoderModel):
|
||||
cache_dir: Directory to cache downloaded models. Default: system cache
|
||||
max_length: Maximum sequence length for reranking. Default: 512
|
||||
max_concurrent: Maximum concurrent reranking calls. Default: 4
|
||||
cpu_mem_arena: Enable ONNX Runtime CPU memory arena. Default: False.
|
||||
When True, ONNX pre-allocates a memory arena that never
|
||||
shrinks, causing RSS to grow monotonically. False trades
|
||||
slightly slower per-call allocation for bounded RSS.
|
||||
"""
|
||||
self.model_name = model_name or DEFAULT_RERANKER_FLASHRANK_MODEL
|
||||
self.cache_dir = cache_dir or DEFAULT_RERANKER_FLASHRANK_CACHE_DIR
|
||||
self.max_length = max_length
|
||||
self.cpu_mem_arena = cpu_mem_arena
|
||||
self._ranker = None
|
||||
FlashRankCrossEncoder._max_concurrent = max_concurrent
|
||||
|
||||
@@ -894,15 +902,47 @@ class FlashRankCrossEncoder(CrossEncoderModel):
|
||||
except ImportError:
|
||||
raise ImportError("flashrank is required for FlashRankCrossEncoder. Install it with: pip install flashrank")
|
||||
|
||||
logger.info(f"Reranker: initializing FlashRank provider with model {self.model_name}")
|
||||
logger.info(
|
||||
f"Reranker: initializing FlashRank provider with model {self.model_name}"
|
||||
f" (cpu_mem_arena={self.cpu_mem_arena})"
|
||||
)
|
||||
|
||||
# Configure ONNX session options before Ranker creates the session.
|
||||
# When cpu_mem_arena=False (default), ONNX won't pre-allocate an arena
|
||||
# that grows monotonically, keeping RSS bounded after rerank batches.
|
||||
if not self.cpu_mem_arena:
|
||||
import onnxruntime as ort
|
||||
|
||||
session_options = ort.SessionOptions()
|
||||
session_options.enable_cpu_mem_arena = False
|
||||
else:
|
||||
session_options = None
|
||||
|
||||
# Initialize ranker with optional cache directory
|
||||
ranker_kwargs = {"model_name": self.model_name, "max_length": self.max_length}
|
||||
ranker_kwargs: dict = {"model_name": self.model_name, "max_length": self.max_length}
|
||||
if self.cache_dir:
|
||||
ranker_kwargs["cache_dir"] = self.cache_dir
|
||||
|
||||
self._ranker = Ranker(**ranker_kwargs)
|
||||
|
||||
# Patch the ONNX session options if arena is disabled.
|
||||
# FlashRank's Ranker doesn't expose SessionOptions in its API,
|
||||
# so we replace the session after initialization.
|
||||
if session_options is not None and hasattr(self._ranker, "session"):
|
||||
import onnxruntime as ort
|
||||
|
||||
model_file = None
|
||||
model_dir = getattr(self._ranker, "model_dir", None)
|
||||
if model_dir:
|
||||
from pathlib import Path
|
||||
|
||||
for candidate in Path(model_dir).glob("*.onnx"):
|
||||
model_file = str(candidate)
|
||||
break
|
||||
if model_file:
|
||||
self._ranker.session = ort.InferenceSession(model_file, sess_options=session_options)
|
||||
logger.info("Reranker: replaced FlashRank ONNX session with cpu_mem_arena=False")
|
||||
|
||||
# Initialize shared executor
|
||||
if FlashRankCrossEncoder._executor is None:
|
||||
FlashRankCrossEncoder._executor = ThreadPoolExecutor(
|
||||
@@ -1552,7 +1592,10 @@ def create_cross_encoder_from_env() -> CrossEncoderModel:
|
||||
elif provider == "flashrank":
|
||||
model = os.environ.get(ENV_RERANKER_FLASHRANK_MODEL, DEFAULT_RERANKER_FLASHRANK_MODEL)
|
||||
cache_dir = os.environ.get(ENV_RERANKER_FLASHRANK_CACHE_DIR, DEFAULT_RERANKER_FLASHRANK_CACHE_DIR)
|
||||
return FlashRankCrossEncoder(model_name=model, cache_dir=cache_dir)
|
||||
cpu_mem_arena = os.environ.get(
|
||||
ENV_RERANKER_FLASHRANK_CPU_MEM_ARENA, str(DEFAULT_RERANKER_FLASHRANK_CPU_MEM_ARENA)
|
||||
).lower() in ("true", "1", "yes")
|
||||
return FlashRankCrossEncoder(model_name=model, cache_dir=cache_dir, cpu_mem_arena=cpu_mem_arena)
|
||||
elif provider == "litellm":
|
||||
return LiteLLMCrossEncoder(
|
||||
api_base=config.reranker_litellm_api_base,
|
||||
|
||||
@@ -161,16 +161,22 @@ class MemoryEngineInterface(ABC):
|
||||
bank_id: str,
|
||||
*,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any]:
|
||||
create_if_missing: bool = True,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Get bank profile including disposition and mission.
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID.
|
||||
request_context: Request context for authentication.
|
||||
create_if_missing: If True (default), the bank is auto-created
|
||||
with defaults if it does not exist. Pass False to make this
|
||||
a strict read — returns None if the bank does not exist.
|
||||
|
||||
Returns:
|
||||
Bank profile dict with bank_id, name, disposition, and mission.
|
||||
Bank profile dict with bank_id, name, disposition, and mission,
|
||||
or None when create_if_missing=False and the bank does not
|
||||
exist.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ def create_llm_provider(
|
||||
extra_args=config.llamacpp_extra_args,
|
||||
)
|
||||
|
||||
elif provider_lower in ("openai", "groq", "ollama", "lmstudio", "minimax", "volcano", "openrouter"):
|
||||
elif provider_lower in ("openai", "groq", "ollama", "lmstudio", "minimax", "deepseek", "volcano", "openrouter"):
|
||||
return OpenAICompatibleLLM(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
@@ -360,6 +360,7 @@ class LLMProvider:
|
||||
"mock",
|
||||
"none",
|
||||
"minimax",
|
||||
"deepseek",
|
||||
"litellm",
|
||||
"bedrock",
|
||||
"volcano",
|
||||
@@ -378,6 +379,8 @@ class LLMProvider:
|
||||
self.base_url = "http://localhost:1234/v1"
|
||||
elif self.provider == "minimax":
|
||||
self.base_url = "https://api.minimax.io/v1"
|
||||
elif self.provider == "deepseek":
|
||||
self.base_url = "https://api.deepseek.com"
|
||||
elif self.provider == "openrouter":
|
||||
self.base_url = "https://openrouter.ai/api/v1"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
import asyncpg
|
||||
import httpx
|
||||
@@ -1905,7 +1905,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
logger.debug(f"File storage initialized ({config.file_storage_type})")
|
||||
|
||||
# Initialize parser registry
|
||||
from .parsers import FileParserRegistry, IrisParser, MarkitdownParser
|
||||
from .parsers import FileParserRegistry, IrisParser, LlamaParseParser, MarkitdownParser
|
||||
|
||||
self._parser_registry = FileParserRegistry()
|
||||
try:
|
||||
@@ -1920,6 +1920,12 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
logger.debug("Registered iris parser")
|
||||
else:
|
||||
logger.debug("Iris parser not registered (VECTORIZE_TOKEN or VECTORIZE_ORG_ID not set)")
|
||||
llama_parse_key = config.file_parser_llama_parse_api_key
|
||||
if llama_parse_key:
|
||||
self._parser_registry.register(LlamaParseParser(api_key=llama_parse_key))
|
||||
logger.debug("Registered llama_parse parser")
|
||||
else:
|
||||
logger.debug("LlamaParse parser not registered (HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY not set)")
|
||||
|
||||
# Initialize webhook manager
|
||||
from ..webhooks import WebhookManager
|
||||
@@ -5470,22 +5476,51 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
|
||||
# ==================== bank profile Methods ====================
|
||||
|
||||
# Type-checker overloads: when create_if_missing is True (the default),
|
||||
# this method always returns a profile dict — the type checker can rely
|
||||
# on non-None for every existing caller. Only when create_if_missing is
|
||||
# explicitly False does the return become Optional.
|
||||
@overload
|
||||
async def get_bank_profile(
|
||||
self,
|
||||
bank_id: str,
|
||||
*,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any]:
|
||||
create_if_missing: Literal[True] = True,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
@overload
|
||||
async def get_bank_profile(
|
||||
self,
|
||||
bank_id: str,
|
||||
*,
|
||||
request_context: "RequestContext",
|
||||
create_if_missing: Literal[False],
|
||||
) -> dict[str, Any] | None: ...
|
||||
|
||||
async def get_bank_profile(
|
||||
self,
|
||||
bank_id: str,
|
||||
*,
|
||||
request_context: "RequestContext",
|
||||
create_if_missing: bool = True,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Get bank profile (name, disposition + mission).
|
||||
Auto-creates agent with default values if not exists.
|
||||
|
||||
Args:
|
||||
bank_id: bank IDentifier
|
||||
request_context: Request context for authentication.
|
||||
create_if_missing: If True (default), the bank is auto-created
|
||||
with defaults when it does not exist. Pass False from read-
|
||||
only callers (HTTP GET handlers, polling, etc.) so a missing
|
||||
bank surfaces as None rather than being silently created.
|
||||
The caller is then responsible for translating None to a
|
||||
404 (or similar).
|
||||
|
||||
Returns:
|
||||
Dict with name, disposition traits, and mission
|
||||
Dict with name, disposition traits, and mission, or None when
|
||||
create_if_missing=False and the bank does not exist.
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
if self._operation_validator:
|
||||
@@ -5494,7 +5529,13 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
ctx = BankReadContext(bank_id=bank_id, operation="get_bank_profile", request_context=request_context)
|
||||
await self._validate_operation(self._operation_validator.validate_bank_read(ctx))
|
||||
pool = await self._get_pool()
|
||||
profile, created = await bank_utils.get_or_create_bank_profile(pool, bank_id)
|
||||
if not create_if_missing:
|
||||
existing = await bank_utils.get_bank_profile_if_exists(pool, bank_id)
|
||||
if existing is None:
|
||||
return None
|
||||
profile, created = existing, False
|
||||
else:
|
||||
profile, created = await bank_utils.get_or_create_bank_profile(pool, bank_id)
|
||||
|
||||
# Apply HINDSIGHT_API_DEFAULT_BANK_TEMPLATE to freshly-created banks. Done
|
||||
# before reading the resolved config below so the template's overrides
|
||||
@@ -6386,22 +6427,70 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
|
||||
ctx = BankReadContext(bank_id=bank_id, operation="list_tags", request_context=request_context)
|
||||
await self._validate_operation(self._operation_validator.validate_bank_read(ctx))
|
||||
return await self._list_tags_from_table(
|
||||
table="memory_units",
|
||||
bank_id=bank_id,
|
||||
pattern=pattern,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
async def list_mental_model_tags(
|
||||
self,
|
||||
bank_id: str,
|
||||
*,
|
||||
pattern: str | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
request_context: "RequestContext",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
List all unique tags used on mental models in a bank with usage counts.
|
||||
|
||||
Same wildcard semantics as list_tags. Useful to populate tag autocompletion
|
||||
for UIs filtering mental models by tag.
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
if self._operation_validator:
|
||||
from hindsight_api.extensions import BankReadContext
|
||||
|
||||
ctx = BankReadContext(
|
||||
bank_id=bank_id,
|
||||
operation="list_mental_model_tags",
|
||||
request_context=request_context,
|
||||
)
|
||||
await self._validate_operation(self._operation_validator.validate_bank_read(ctx))
|
||||
return await self._list_tags_from_table(
|
||||
table="mental_models",
|
||||
bank_id=bank_id,
|
||||
pattern=pattern,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
async def _list_tags_from_table(
|
||||
self,
|
||||
*,
|
||||
table: str,
|
||||
bank_id: str,
|
||||
pattern: str | None,
|
||||
limit: int,
|
||||
offset: int,
|
||||
) -> dict[str, Any]:
|
||||
pool = await self._get_pool()
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
# Build pattern filter if provided (convert * to % for ILIKE)
|
||||
pattern_clause = ""
|
||||
params: list[Any] = [bank_id]
|
||||
if pattern:
|
||||
# Convert wildcard pattern: * -> % for SQL ILIKE
|
||||
sql_pattern = pattern.replace("*", "%")
|
||||
pattern_clause = "AND tag ILIKE $2"
|
||||
params.append(sql_pattern)
|
||||
|
||||
# Get total count of distinct tags matching pattern
|
||||
total_row = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT COUNT(DISTINCT tag) as total
|
||||
FROM {fq_table("memory_units")}, unnest(tags) AS tag
|
||||
FROM {fq_table(table)}, unnest(tags) AS tag
|
||||
WHERE bank_id = $1 AND tags IS NOT NULL AND tags != '{{}}'
|
||||
{pattern_clause}
|
||||
""",
|
||||
@@ -6409,7 +6498,6 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
)
|
||||
total = total_row["total"] if total_row else 0
|
||||
|
||||
# Get paginated tags with counts, ordered by frequency
|
||||
limit_param = len(params) + 1
|
||||
offset_param = len(params) + 2
|
||||
params.extend([limit, offset])
|
||||
@@ -6417,7 +6505,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT tag, COUNT(*) as count
|
||||
FROM {fq_table("memory_units")}, unnest(tags) AS tag
|
||||
FROM {fq_table(table)}, unnest(tags) AS tag
|
||||
WHERE bank_id = $1 AND tags IS NOT NULL AND tags != '{{}}'
|
||||
{pattern_clause}
|
||||
GROUP BY tag
|
||||
|
||||
@@ -5,12 +5,14 @@ from dataclasses import dataclass
|
||||
|
||||
from .base import FileParser, UnsupportedFileTypeError
|
||||
from .iris import IrisParser
|
||||
from .llama_parse import LlamaParseParser
|
||||
from .markitdown import MarkitdownParser
|
||||
|
||||
__all__ = [
|
||||
"FileParser",
|
||||
"UnsupportedFileTypeError",
|
||||
"IrisParser",
|
||||
"LlamaParseParser",
|
||||
"MarkitdownParser",
|
||||
"FileParserRegistry",
|
||||
"ConvertResult",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""LlamaParse parser implementation using the LlamaIndex Cloud parsing API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import mimetypes
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from .base import FileParser, UnsupportedFileTypeError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_LLAMA_PARSE_BASE_URL = "https://api.cloud.llamaindex.ai/api/parsing"
|
||||
_DEFAULT_POLL_INTERVAL = 2.0 # seconds
|
||||
_DEFAULT_TIMEOUT = 300.0 # seconds
|
||||
|
||||
# HTTP status codes that indicate the file type is not supported.
|
||||
# Other 4xx codes (401, 403, 429, etc.) are operational errors, not file-type issues.
|
||||
_UNSUPPORTED_FILE_STATUS_CODES = {400, 415, 422}
|
||||
|
||||
|
||||
class LlamaParseParser(FileParser):
|
||||
"""
|
||||
LlamaParse file parser using LlamaIndex's hosted parsing service.
|
||||
|
||||
Uploads files to the LlamaParse API, polls until the parse job completes,
|
||||
and returns the resulting markdown. The API determines which file types
|
||||
are supported — UnsupportedFileTypeError is raised if the file is rejected.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
poll_interval: float = _DEFAULT_POLL_INTERVAL,
|
||||
timeout: float = _DEFAULT_TIMEOUT,
|
||||
):
|
||||
"""
|
||||
Initialize llama_parse parser.
|
||||
|
||||
Args:
|
||||
api_key: LlamaCloud API key (typically starts with "llx-")
|
||||
poll_interval: Seconds between status poll requests (default: 2)
|
||||
timeout: Maximum seconds to wait for parsing (default: 300)
|
||||
"""
|
||||
self._api_key = api_key
|
||||
self._poll_interval = poll_interval
|
||||
self._timeout = timeout
|
||||
self._auth_headers = {"Authorization": f"Bearer {api_key}"}
|
||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0, read=120.0))
|
||||
|
||||
async def convert(self, file_data: bytes, filename: str) -> str:
|
||||
"""
|
||||
Parse file to markdown using the LlamaParse API.
|
||||
|
||||
Raises:
|
||||
UnsupportedFileTypeError: If the LlamaParse API rejects the file type
|
||||
RuntimeError: If parsing fails for another reason
|
||||
"""
|
||||
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
|
||||
# Step 1: Upload file and start parse job
|
||||
upload_resp = await self._client.post(
|
||||
f"{_LLAMA_PARSE_BASE_URL}/upload",
|
||||
headers=self._auth_headers,
|
||||
# Ensure file_data is plain bytes (storage backends may return obstore.Bytes)
|
||||
files={"file": (filename, bytes(file_data), content_type)},
|
||||
)
|
||||
_raise_for_status(upload_resp, filename, "upload")
|
||||
job_id: str = upload_resp.json()["id"]
|
||||
|
||||
# Step 2: Poll job status until SUCCESS or ERROR
|
||||
deadline = time.monotonic() + self._timeout
|
||||
while True:
|
||||
status_resp = await self._client.get(
|
||||
f"{_LLAMA_PARSE_BASE_URL}/job/{job_id}",
|
||||
headers=self._auth_headers,
|
||||
)
|
||||
_raise_for_status(status_resp, filename, "poll job status")
|
||||
status_data = status_resp.json()
|
||||
status = status_data.get("status")
|
||||
|
||||
if status == "SUCCESS":
|
||||
break
|
||||
if status in ("ERROR", "CANCELLED"):
|
||||
error = status_data.get("error_code") or status_data.get("error") or "unknown error"
|
||||
raise RuntimeError(f"LlamaParse job failed for '{filename}': {error}")
|
||||
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError(f"LlamaParse job timed out after {self._timeout}s for '{filename}'")
|
||||
|
||||
await asyncio.sleep(self._poll_interval)
|
||||
|
||||
# Step 3: Fetch the markdown result
|
||||
result_resp = await self._client.get(
|
||||
f"{_LLAMA_PARSE_BASE_URL}/job/{job_id}/result/markdown",
|
||||
headers=self._auth_headers,
|
||||
)
|
||||
_raise_for_status(result_resp, filename, "fetch markdown result")
|
||||
markdown = result_resp.json().get("markdown")
|
||||
if not markdown:
|
||||
raise RuntimeError(f"No content extracted from '{filename}'")
|
||||
return markdown
|
||||
|
||||
def name(self) -> str:
|
||||
"""Get parser name."""
|
||||
return "llama_parse"
|
||||
|
||||
|
||||
def _raise_for_status(response: httpx.Response, filename: str, step: str) -> None:
|
||||
"""
|
||||
Raise an appropriate error for HTTP errors.
|
||||
|
||||
Raises UnsupportedFileTypeError for 400/415/422 (file rejected by the API).
|
||||
Raises RuntimeError for all other errors (auth, rate-limit, server errors).
|
||||
"""
|
||||
if not response.is_error:
|
||||
return
|
||||
body = response.text or "<empty>"
|
||||
msg = (
|
||||
f"LlamaParse API error during {step} for '{filename}': {response.status_code} {response.reason_phrase} — {body}"
|
||||
)
|
||||
if response.status_code in _UNSUPPORTED_FILE_STATUS_CODES:
|
||||
raise UnsupportedFileTypeError(msg)
|
||||
raise RuntimeError(msg)
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
OpenAI-compatible LLM provider supporting OpenAI, Groq, Ollama, LMStudio, and MiniMax.
|
||||
OpenAI-compatible LLM provider supporting OpenAI, Groq, Ollama, LMStudio, MiniMax, and DeepSeek.
|
||||
|
||||
This provider handles all OpenAI API-compatible models including:
|
||||
- OpenAI: GPT-4, GPT-4o, GPT-5, o1, o3 (reasoning models)
|
||||
@@ -7,6 +7,7 @@ This provider handles all OpenAI API-compatible models including:
|
||||
- Ollama: Local models with native streaming API support
|
||||
- LMStudio: Local models with OpenAI-compatible API
|
||||
- MiniMax: MiniMax-M2.7 models with 1M context window
|
||||
- DeepSeek: deepseek-v4-flash / deepseek-v4-pro / deepseek-chat / deepseek-reasoner via api.deepseek.com
|
||||
|
||||
Features:
|
||||
- Reasoning models with extended thinking (o1, o3, GPT-5 families)
|
||||
@@ -60,6 +61,77 @@ def _strip_code_fences(content: str) -> str:
|
||||
return content
|
||||
|
||||
|
||||
def _simplify_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Simplify a Pydantic JSON schema for maximum LLM compatibility.
|
||||
|
||||
Pydantic v2's model_json_schema() produces schemas with $ref/$defs, anyOf
|
||||
(for Optional fields), and const — features that Ollama's grammar-based
|
||||
constrained decoding silently fails on, and that confuse weaker models when
|
||||
the schema appears as a text hint in the prompt.
|
||||
|
||||
This function:
|
||||
1. Resolves all $ref/$defs by inlining referenced definitions
|
||||
2. Simplifies anyOf nullable unions (e.g. anyOf: [{type: "string"}, {type: "null"}])
|
||||
to just the non-null type, keeping default/description
|
||||
3. Replaces const with single-element enum
|
||||
"""
|
||||
defs = schema.get("$defs", {})
|
||||
|
||||
def _resolve(node: Any) -> Any:
|
||||
if not isinstance(node, dict):
|
||||
if isinstance(node, list):
|
||||
return [_resolve(item) for item in node]
|
||||
return node
|
||||
|
||||
# Resolve $ref first
|
||||
if "$ref" in node:
|
||||
ref_path = node["$ref"] # e.g. "#/$defs/Entity"
|
||||
ref_name = ref_path.rsplit("/", 1)[-1]
|
||||
if ref_name in defs:
|
||||
# Inline the definition, merging any sibling keys (e.g. description)
|
||||
resolved = _resolve(dict(defs[ref_name]))
|
||||
# Preserve sibling keys from the referencing node
|
||||
for k, v in node.items():
|
||||
if k != "$ref":
|
||||
resolved[k] = _resolve(v)
|
||||
return resolved
|
||||
return node # unresolvable ref, leave as-is
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in node.items():
|
||||
if key == "$defs":
|
||||
continue # drop $defs — everything is inlined now
|
||||
|
||||
if key == "anyOf" and isinstance(value, list):
|
||||
# Simplify nullable anyOf: [{type: "string"}, {type: "null"}] → {type: "string"}
|
||||
non_null = [_resolve(v) for v in value if not (isinstance(v, dict) and v.get("type") == "null")]
|
||||
if len(non_null) == 1:
|
||||
# Single non-null type — inline it, preserving sibling keys
|
||||
simplified = dict(non_null[0])
|
||||
for k, v in node.items():
|
||||
if k not in ("anyOf",) and k not in simplified:
|
||||
simplified[k] = _resolve(v)
|
||||
return simplified
|
||||
elif len(non_null) > 1:
|
||||
# Multiple non-null types — keep anyOf but resolved
|
||||
result["anyOf"] = non_null
|
||||
else:
|
||||
# All null — just use null
|
||||
result["type"] = "null"
|
||||
continue
|
||||
|
||||
if key == "const":
|
||||
# Replace const with single-element enum for broader compatibility
|
||||
result["enum"] = [value]
|
||||
continue
|
||||
|
||||
result[key] = _resolve(value)
|
||||
|
||||
return result
|
||||
|
||||
return _resolve(schema)
|
||||
|
||||
|
||||
def _summarize_status_error(e: APIStatusError, body_max: int = 400) -> str:
|
||||
"""Render an APIStatusError with status code + truncated response body.
|
||||
|
||||
@@ -96,6 +168,7 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
- Ollama: Local models with native streaming API for better structured output
|
||||
- LMStudio: Local models with OpenAI-compatible API
|
||||
- MiniMax: MiniMax-M2.7 models via OpenAI-compatible API (https://api.minimax.io/v1)
|
||||
- DeepSeek: deepseek-v4-flash / deepseek-v4-pro / deepseek-chat / deepseek-reasoner via https://api.deepseek.com
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -127,7 +200,17 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
super().__init__(provider, api_key, base_url, model, reasoning_effort, **kwargs)
|
||||
|
||||
# Validate provider
|
||||
valid_providers = ["openai", "groq", "ollama", "lmstudio", "llamacpp", "minimax", "volcano", "openrouter"]
|
||||
valid_providers = [
|
||||
"openai",
|
||||
"groq",
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"llamacpp",
|
||||
"minimax",
|
||||
"deepseek",
|
||||
"volcano",
|
||||
"openrouter",
|
||||
]
|
||||
if self.provider not in valid_providers:
|
||||
raise ValueError(f"OpenAICompatibleLLM only supports: {', '.join(valid_providers)}. Got: {self.provider}")
|
||||
|
||||
@@ -141,6 +224,8 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
self.base_url = "http://localhost:1234/v1"
|
||||
elif self.provider == "minimax":
|
||||
self.base_url = "https://api.minimax.io/v1"
|
||||
elif self.provider == "deepseek":
|
||||
self.base_url = "https://api.deepseek.com"
|
||||
elif self.provider == "openrouter":
|
||||
self.base_url = "https://openrouter.ai/api/v1"
|
||||
|
||||
@@ -149,7 +234,7 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
self.api_key = "local"
|
||||
|
||||
# Validate API key for cloud providers
|
||||
if self.provider in ("openai", "groq", "minimax", "openrouter") and not self.api_key:
|
||||
if self.provider in ("openai", "groq", "minimax", "deepseek", "openrouter") and not self.api_key:
|
||||
raise ValueError(f"API key is required for {self.provider}")
|
||||
|
||||
# Service tier configuration (from config, not env vars)
|
||||
@@ -356,6 +441,12 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
schema = None
|
||||
if hasattr(response_format, "model_json_schema"):
|
||||
schema = response_format.model_json_schema()
|
||||
# Simplify schema for better LLM compliance — resolves $ref/$defs,
|
||||
# simplifies anyOf nullables, replaces const with enum.
|
||||
from hindsight_api.config import get_config
|
||||
|
||||
if get_config().llm_simplify_json_schema:
|
||||
schema = _simplify_json_schema(schema)
|
||||
|
||||
if strict_schema and schema is not None:
|
||||
# Use OpenAI's strict JSON schema enforcement
|
||||
@@ -653,6 +744,15 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
if "deepseek" in self.model.lower() and request_tool_choice != "auto":
|
||||
request_tool_choice = None
|
||||
|
||||
# "auto" is the OpenAI API default — omitting tool_choice is semantically
|
||||
# identical. Some providers (e.g. DeepSeek's reasoner pathway, which
|
||||
# deepseek-v4-flash falls into when thinking mode is enabled) reject the
|
||||
# parameter outright, returning HTTP 400 even for value "auto". Sending it
|
||||
# only when the caller asks for a non-default behaviour avoids those 400s
|
||||
# without changing semantics for compliant providers.
|
||||
if request_tool_choice == "auto":
|
||||
request_tool_choice = None
|
||||
|
||||
# DeepSeek tool-call replies can carry provider-specific reasoning_content.
|
||||
# The normalized tool result does not retain it, but replaying assistant
|
||||
# tool_calls without the field can trigger a 400. DeepSeek accepts an
|
||||
@@ -660,11 +760,7 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
if "deepseek" in self.model.lower():
|
||||
normalized_messages: list[dict[str, Any]] = []
|
||||
for msg in messages:
|
||||
if (
|
||||
msg.get("role") == "assistant"
|
||||
and msg.get("tool_calls")
|
||||
and "reasoning_content" not in msg
|
||||
):
|
||||
if msg.get("role") == "assistant" and msg.get("tool_calls") and "reasoning_content" not in msg:
|
||||
normalized_msg = dict(msg)
|
||||
normalized_msg["reasoning_content"] = ""
|
||||
normalized_messages.append(normalized_msg)
|
||||
@@ -835,8 +931,14 @@ class OpenAICompatibleLLM(LLMInterface):
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# Get the JSON schema from the Pydantic model
|
||||
# Get the JSON schema from the Pydantic model and simplify it for Ollama's
|
||||
# grammar engine, which doesn't support $ref, anyOf, or const.
|
||||
schema = response_format.model_json_schema() if hasattr(response_format, "model_json_schema") else None
|
||||
if schema:
|
||||
from hindsight_api.config import get_config
|
||||
|
||||
if get_config().llm_simplify_json_schema:
|
||||
schema = _simplify_json_schema(schema)
|
||||
|
||||
# Build the base URL for Ollama's native API
|
||||
# Default OpenAI-compatible URL is http://localhost:11434/v1
|
||||
|
||||
@@ -117,6 +117,41 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
|
||||
return profile
|
||||
|
||||
|
||||
async def get_bank_profile_if_exists(pool, bank_id: str) -> BankProfile | None:
|
||||
"""
|
||||
Get bank profile (name, disposition + mission) without auto-creating.
|
||||
|
||||
Returns None if the bank does not exist. This is the read-only variant
|
||||
of get_bank_profile, intended for read endpoints where a bank that
|
||||
doesn't exist should surface as 404 rather than be silently created.
|
||||
|
||||
Args:
|
||||
pool: Database connection pool
|
||||
bank_id: bank IDentifier
|
||||
|
||||
Returns:
|
||||
BankProfile if the bank exists, otherwise None.
|
||||
"""
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT name, disposition, mission
|
||||
FROM {fq_table("banks")} WHERE bank_id = $1
|
||||
""",
|
||||
bank_id,
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
disposition_data = row["disposition"]
|
||||
if isinstance(disposition_data, str):
|
||||
disposition_data = json.loads(disposition_data)
|
||||
return BankProfile(
|
||||
name=row["name"],
|
||||
disposition=DispositionTraits(**disposition_data),
|
||||
mission=row["mission"] or "",
|
||||
)
|
||||
|
||||
|
||||
async def get_or_create_bank_profile(pool, bank_id: str) -> tuple[BankProfile, bool]:
|
||||
"""
|
||||
Get bank profile, auto-creating with defaults if it doesn't exist.
|
||||
|
||||
@@ -110,12 +110,6 @@ class CausalRelation(BaseModel):
|
||||
relation_type: Literal["caused_by"] = Field(
|
||||
description="How this fact relates to the target: 'caused_by' = this fact was caused by the target"
|
||||
)
|
||||
strength: float = Field(
|
||||
description="Strength of relationship (0.0 to 1.0)",
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
default=1.0,
|
||||
)
|
||||
|
||||
|
||||
class FactCausalRelation(BaseModel):
|
||||
@@ -134,12 +128,6 @@ class FactCausalRelation(BaseModel):
|
||||
relation_type: Literal["caused_by"] = Field(
|
||||
description="How this fact relates to the target fact: 'caused_by' = this fact was caused by the target fact"
|
||||
)
|
||||
strength: float = Field(
|
||||
description="Strength of relationship (0.0 to 1.0). 1.0 = strong, 0.5 = moderate",
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
default=1.0,
|
||||
)
|
||||
|
||||
|
||||
class ExtractedFact(BaseModel):
|
||||
@@ -1216,7 +1204,6 @@ async def _extract_facts_from_chunk(
|
||||
# New schema uses target_index
|
||||
target_idx = rel.get("target_index")
|
||||
relation_type = rel.get("relation_type")
|
||||
strength = rel.get("strength", 1.0)
|
||||
|
||||
if target_idx is None or relation_type is None:
|
||||
continue
|
||||
@@ -1233,7 +1220,6 @@ async def _extract_facts_from_chunk(
|
||||
CausalRelation(
|
||||
target_fact_index=target_idx,
|
||||
relation_type=relation_type,
|
||||
strength=strength,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -1909,7 +1895,6 @@ async def extract_facts_from_contents_batch_api(
|
||||
continue
|
||||
target_idx = rel.get("target_index")
|
||||
relation_type = rel.get("relation_type")
|
||||
strength = rel.get("strength", 1.0)
|
||||
|
||||
if target_idx is None or relation_type is None:
|
||||
continue
|
||||
@@ -1918,9 +1903,7 @@ async def extract_facts_from_contents_batch_api(
|
||||
|
||||
try:
|
||||
validated_relations.append(
|
||||
CausalRelation(
|
||||
target_fact_index=target_idx, relation_type=relation_type, strength=strength
|
||||
)
|
||||
CausalRelation(target_fact_index=target_idx, relation_type=relation_type)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -2250,7 +2233,6 @@ def _convert_causal_relations(relations_from_llm, fact_start_idx: int) -> list[C
|
||||
causal_relation = CausalRelationType(
|
||||
relation_type=rel.relation_type,
|
||||
target_fact_index=fact_start_idx + rel.target_fact_index,
|
||||
strength=rel.strength,
|
||||
)
|
||||
causal_relations.append(causal_relation)
|
||||
return causal_relations
|
||||
|
||||
@@ -97,7 +97,6 @@ async def create_causal_links_batch(conn, bank_id: str, unit_ids: list[str], fac
|
||||
{
|
||||
"relation_type": rel.relation_type,
|
||||
"target_fact_index": rel.target_fact_index,
|
||||
"strength": rel.strength,
|
||||
}
|
||||
for rel in fact.causal_relations
|
||||
]
|
||||
|
||||
@@ -990,7 +990,6 @@ async def create_causal_links_batch(
|
||||
Each element is a list of dicts with:
|
||||
- target_fact_index: Index into unit_ids for the target fact
|
||||
- relation_type: "caused_by"
|
||||
- strength: Float in [0.0, 1.0] representing relationship strength
|
||||
|
||||
Returns:
|
||||
Number of causal links created
|
||||
@@ -1017,7 +1016,6 @@ async def create_causal_links_batch(
|
||||
for relation in causal_relations:
|
||||
target_idx = relation["target_fact_index"]
|
||||
relation_type = relation["relation_type"]
|
||||
strength = relation.get("strength", 1.0)
|
||||
|
||||
# Validate relation_type - only "caused_by" is supported (DB constraint)
|
||||
valid_types = {"caused_by"}
|
||||
@@ -1040,10 +1038,7 @@ async def create_causal_links_batch(
|
||||
if from_unit_id == to_unit_id:
|
||||
continue
|
||||
|
||||
# Add the causal link
|
||||
# link_type is the relation_type (e.g., "causes", "caused_by")
|
||||
# weight is the strength of the relationship
|
||||
links.append((from_unit_id, to_unit_id, relation_type, strength, None))
|
||||
links.append((from_unit_id, to_unit_id, relation_type, 1.0, None))
|
||||
|
||||
if links:
|
||||
insert_start = time_mod.time()
|
||||
|
||||
@@ -99,7 +99,6 @@ class CausalRelation:
|
||||
|
||||
relation_type: str # "caused_by"
|
||||
target_fact_index: int # Index of the target fact in the batch
|
||||
strength: float = 1.0 # Strength of the causal relationship
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -112,16 +112,6 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
The Python merge step applies per-signal score transformations.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
causal_weight_threshold: float = 0.3,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
causal_weight_threshold: Minimum weight for causal links to follow.
|
||||
"""
|
||||
self.causal_weight_threshold = causal_weight_threshold
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "link_expansion"
|
||||
@@ -387,7 +377,6 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
JOIN {mu} mu ON ml.to_unit_id = mu.id
|
||||
WHERE ml.from_unit_id = ANY($1::uuid[])
|
||||
AND ml.link_type IN ('causes', 'caused_by', 'enables', 'prevents')
|
||||
AND ml.weight >= $4
|
||||
AND mu.fact_type = $2
|
||||
ORDER BY mu.id, ml.weight DESC
|
||||
LIMIT $3
|
||||
@@ -403,7 +392,7 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
SELECT * FROM causal_expanded
|
||||
"""
|
||||
|
||||
params = [seed_ids, fact_type, budget, self.causal_weight_threshold]
|
||||
params = [seed_ids, fact_type, budget]
|
||||
|
||||
try:
|
||||
all_rows = await asyncio.wait_for(
|
||||
@@ -559,7 +548,7 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
FROM {ml} ml JOIN {mu} mu ON ml.to_unit_id = mu.id
|
||||
WHERE ml.from_unit_id = ANY($1::uuid[])
|
||||
AND ml.link_type IN ('causes', 'caused_by', 'enables', 'prevents')
|
||||
AND ml.weight >= $3 AND mu.fact_type = 'observation'
|
||||
AND mu.fact_type = 'observation'
|
||||
ORDER BY mu.id, ml.weight DESC LIMIT $2
|
||||
)
|
||||
SELECT * FROM semantic_expanded
|
||||
@@ -568,7 +557,6 @@ class LinkExpansionRetriever(GraphRetriever):
|
||||
""",
|
||||
seed_ids,
|
||||
budget,
|
||||
self.causal_weight_threshold,
|
||||
)
|
||||
|
||||
semantic_rows = [r for r in sem_causal_rows if r["source"] == "semantic"]
|
||||
|
||||
@@ -16,6 +16,42 @@ def unique_agent_id(prefix: str) -> str:
|
||||
class TestAgentProfile:
|
||||
"""Tests for agent profile management."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_bank_profile_no_auto_create_returns_none(
|
||||
self, memory: MemoryEngine, request_context
|
||||
):
|
||||
"""When create_if_missing=False is passed, a missing bank returns None
|
||||
rather than being silently auto-created. This is what read-only
|
||||
endpoints (HTTP GET, polling, etc.) must use to avoid creating banks
|
||||
as a side effect of a stale client request."""
|
||||
bank_id = unique_agent_id("test_no_auto_create")
|
||||
|
||||
# First call with create_if_missing=False on a non-existent bank
|
||||
result = await memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
assert result is None, "Expected None for missing bank with create_if_missing=False"
|
||||
|
||||
# Verify the bank was NOT created as a side effect
|
||||
result_again = await memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
assert result_again is None, "Bank must not exist after read-only call"
|
||||
|
||||
# And explicit auto-create still works
|
||||
created = await memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=True
|
||||
)
|
||||
assert created is not None
|
||||
assert created["disposition"]["skepticism"] == 3
|
||||
|
||||
# Now read-only call sees it
|
||||
seen = await memory.get_bank_profile(
|
||||
bank_id, request_context=request_context, create_if_missing=False
|
||||
)
|
||||
assert seen is not None
|
||||
assert seen["disposition"]["skepticism"] == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_agent_profile_creates_default(self, memory: MemoryEngine, request_context):
|
||||
"""Test that getting a profile for a new agent creates default disposition."""
|
||||
|
||||
@@ -54,7 +54,6 @@ After searching for weeks, I finally found a cheaper apartment in Brooklyn.
|
||||
"from_fact_index": i,
|
||||
"to_fact_index": rel.target_fact_index,
|
||||
"relation_type": rel.relation_type,
|
||||
"strength": rel.strength,
|
||||
"from_fact_text": fact.fact[:50],
|
||||
}
|
||||
)
|
||||
@@ -180,29 +179,3 @@ The new role enabled me to lead a team of engineers.
|
||||
f"Must reference previous facts only (valid range: 0 to {i - 1})"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_causal_relation_strength_values(self):
|
||||
"""
|
||||
Test that causal relation strength values are within valid range [0.0, 1.0].
|
||||
"""
|
||||
text = """
|
||||
The stock market crash directly caused the company to lay off employees.
|
||||
The layoffs indirectly led to reduced consumer spending in the area.
|
||||
Reduced spending somewhat affected local businesses.
|
||||
"""
|
||||
|
||||
context = "Economic impact story"
|
||||
llm_config = LLMConfig.from_env()
|
||||
|
||||
facts, _, _ = await extract_facts_from_text(
|
||||
text=text, event_date=datetime(2024, 4, 1), context=context, llm_config=llm_config, agent_name="TestUser",
|
||||
config=_get_raw_config(),
|
||||
)
|
||||
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.causal_relations:
|
||||
for rel in fact.causal_relations:
|
||||
assert 0.0 <= rel.strength <= 1.0, (
|
||||
f"Causal relation strength {rel.strength} is outside valid range [0.0, 1.0]. "
|
||||
f"Fact {i}: {fact.fact[:50]}..."
|
||||
)
|
||||
|
||||
@@ -45,6 +45,28 @@ def _make_deepseek_llm(model: str = "deepseek-v4-flash") -> OpenAICompatibleLLM:
|
||||
)
|
||||
|
||||
|
||||
def test_deepseek_first_class_provider_sets_default_base_url():
|
||||
"""provider="deepseek" is a configuration shortcut for the api.deepseek.com endpoint."""
|
||||
llm = OpenAICompatibleLLM(
|
||||
provider="deepseek",
|
||||
api_key="sk-test",
|
||||
base_url="",
|
||||
model="deepseek-v4-flash",
|
||||
)
|
||||
|
||||
assert llm.base_url == "https://api.deepseek.com"
|
||||
|
||||
|
||||
def test_deepseek_first_class_provider_requires_api_key():
|
||||
with pytest.raises(ValueError, match="API key is required for deepseek"):
|
||||
OpenAICompatibleLLM(
|
||||
provider="deepseek",
|
||||
api_key="",
|
||||
base_url="",
|
||||
model="deepseek-v4-flash",
|
||||
)
|
||||
|
||||
|
||||
def _make_tool_call_response(tool_name: str = "search_observations") -> MagicMock:
|
||||
mock_tc = MagicMock()
|
||||
mock_tc.id = "call_deepseek_123"
|
||||
@@ -97,6 +119,26 @@ async def test_deepseek_named_tool_choice_filters_tools_but_omits_tool_choice():
|
||||
assert sent_kwargs["tools"][0]["function"]["name"] == "search_observations"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deepseek_auto_tool_choice_is_omitted():
|
||||
"""DeepSeek's reasoner pathway (e.g. v4-flash + thinking mode) returns
|
||||
400 for any tool_choice value. Since "auto" is the API default, omit it."""
|
||||
llm = _make_deepseek_llm()
|
||||
|
||||
with patch.object(llm._client.chat.completions, "create", new_callable=AsyncMock) as mock_create:
|
||||
mock_create.return_value = _make_tool_call_response("search_observations")
|
||||
|
||||
await llm.call_with_tools(
|
||||
messages=[{"role": "user", "content": "Search observations."}],
|
||||
tools=TOOLS,
|
||||
tool_choice="auto",
|
||||
max_retries=0,
|
||||
)
|
||||
|
||||
sent_kwargs = mock_create.call_args.kwargs
|
||||
assert "tool_choice" not in sent_kwargs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deepseek_tool_history_gets_empty_reasoning_content_fallback():
|
||||
"""DeepSeek requires reasoning_content when replaying assistant tool_calls."""
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
Tests for the LlamaParse file parser.
|
||||
|
||||
Unit tests run always (mocked HTTP). Integration tests require
|
||||
HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY in the environment.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from hindsight_api.config import ENV_FILE_PARSER_LLAMA_PARSE_API_KEY
|
||||
from hindsight_api.engine.parsers.base import UnsupportedFileTypeError
|
||||
from hindsight_api.engine.parsers.llama_parse import LlamaParseParser
|
||||
|
||||
_api_key = os.getenv(ENV_FILE_PARSER_LLAMA_PARSE_API_KEY)
|
||||
|
||||
# Minimal valid PDF with the text "Hello from Hindsight"
|
||||
_SAMPLE_PDF = b"""%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
|
||||
/Contents 4 0 R /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Length 44 >>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (Hello from Hindsight) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000274 00000 n
|
||||
trailer << /Size 5 /Root 1 0 R >>
|
||||
startxref
|
||||
369
|
||||
%%EOF"""
|
||||
|
||||
|
||||
def _mock_response(status_code: int, json_data: dict | None = None, text: str = "") -> httpx.Response:
|
||||
"""Build a fake httpx.Response."""
|
||||
content = json.dumps(json_data).encode() if json_data is not None else text.encode()
|
||||
return httpx.Response(
|
||||
status_code=status_code,
|
||||
content=content,
|
||||
request=httpx.Request("GET", "https://fake"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unit tests (always run — mocked HTTP)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_success():
|
||||
"""Happy path: upload → poll SUCCESS → fetch markdown."""
|
||||
parser = LlamaParseParser(api_key="llx-test", poll_interval=0.0, timeout=10.0)
|
||||
|
||||
upload_resp = _mock_response(200, {"id": "job-123"})
|
||||
poll_resp = _mock_response(200, {"status": "SUCCESS"})
|
||||
result_resp = _mock_response(200, {"markdown": "# Hello"})
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=upload_resp)
|
||||
parser._client.get = AsyncMock(side_effect=[poll_resp, result_resp])
|
||||
|
||||
result = await parser.convert(b"fake-pdf", "test.pdf")
|
||||
assert result == "# Hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_polls_until_success():
|
||||
"""Parser should poll multiple times before SUCCESS."""
|
||||
parser = LlamaParseParser(api_key="llx-test", poll_interval=0.0, timeout=10.0)
|
||||
|
||||
upload_resp = _mock_response(200, {"id": "job-456"})
|
||||
pending_resp = _mock_response(200, {"status": "PENDING"})
|
||||
success_resp = _mock_response(200, {"status": "SUCCESS"})
|
||||
result_resp = _mock_response(200, {"markdown": "parsed content"})
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=upload_resp)
|
||||
parser._client.get = AsyncMock(side_effect=[pending_resp, pending_resp, success_resp, result_resp])
|
||||
|
||||
result = await parser.convert(b"fake", "doc.pdf")
|
||||
assert result == "parsed content"
|
||||
assert parser._client.get.call_count == 4 # 2 pending + 1 success + 1 result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_job_error():
|
||||
"""Parser should raise RuntimeError when job status is ERROR."""
|
||||
parser = LlamaParseParser(api_key="llx-test", poll_interval=0.0, timeout=10.0)
|
||||
|
||||
upload_resp = _mock_response(200, {"id": "job-err"})
|
||||
error_resp = _mock_response(200, {"status": "ERROR", "error_code": "PARSE_FAILED"})
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=upload_resp)
|
||||
parser._client.get = AsyncMock(return_value=error_resp)
|
||||
|
||||
with pytest.raises(RuntimeError, match="PARSE_FAILED"):
|
||||
await parser.convert(b"bad", "bad.pdf")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_convert_timeout():
|
||||
"""Parser should raise RuntimeError on timeout."""
|
||||
parser = LlamaParseParser(api_key="llx-test", poll_interval=0.0, timeout=0.0)
|
||||
|
||||
upload_resp = _mock_response(200, {"id": "job-slow"})
|
||||
pending_resp = _mock_response(200, {"status": "PENDING"})
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=upload_resp)
|
||||
parser._client.get = AsyncMock(return_value=pending_resp)
|
||||
|
||||
with pytest.raises(RuntimeError, match="timed out"):
|
||||
await parser.convert(b"data", "slow.pdf")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_unsupported_file_type():
|
||||
"""400/415/422 on upload should raise UnsupportedFileTypeError."""
|
||||
for status_code in (400, 415, 422):
|
||||
parser = LlamaParseParser(api_key="llx-test")
|
||||
reject_resp = _mock_response(status_code, text="unsupported format")
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=reject_resp)
|
||||
|
||||
with pytest.raises(UnsupportedFileTypeError):
|
||||
await parser.convert(b"data", "file.xyz")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_error_raises_runtime_error():
|
||||
"""401/403 should raise RuntimeError, not UnsupportedFileTypeError."""
|
||||
for status_code in (401, 403):
|
||||
parser = LlamaParseParser(api_key="bad-key")
|
||||
auth_resp = _mock_response(status_code, text="unauthorized")
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=auth_resp)
|
||||
|
||||
with pytest.raises(RuntimeError, match="unauthorized"):
|
||||
await parser.convert(b"data", "file.pdf")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_raises_runtime_error():
|
||||
"""429 should raise RuntimeError, not UnsupportedFileTypeError."""
|
||||
parser = LlamaParseParser(api_key="llx-test")
|
||||
rate_resp = _mock_response(429, text="rate limited")
|
||||
|
||||
parser._client = AsyncMock()
|
||||
parser._client.post = AsyncMock(return_value=rate_resp)
|
||||
|
||||
with pytest.raises(RuntimeError, match="rate limited"):
|
||||
await parser.convert(b"data", "file.pdf")
|
||||
|
||||
|
||||
def test_parser_name():
|
||||
"""LlamaParseParser.name() should return 'llama_parse'."""
|
||||
parser = LlamaParseParser(api_key="llx-test")
|
||||
assert parser.name() == "llama_parse"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration tests (require API key)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_integration = pytest.mark.skipif(
|
||||
not _api_key,
|
||||
reason="HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY not set",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llama_parse_parser() -> LlamaParseParser:
|
||||
assert _api_key is not None
|
||||
return LlamaParseParser(api_key=_api_key)
|
||||
|
||||
|
||||
@_integration
|
||||
@pytest.mark.asyncio
|
||||
async def test_llama_parse_parser_converts_pdf(llama_parse_parser: LlamaParseParser):
|
||||
"""LlamaParseParser should extract text from a valid PDF."""
|
||||
result = await llama_parse_parser.convert(_SAMPLE_PDF, "sample.pdf")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
@@ -31,6 +31,9 @@ MODEL_MATRIX = [
|
||||
# Groq models
|
||||
("groq", "openai/gpt-oss-120b"),
|
||||
("groq", "openai/gpt-oss-20b"),
|
||||
# DeepSeek models
|
||||
("deepseek", "deepseek-v4-flash"),
|
||||
("deepseek", "deepseek-chat"),
|
||||
# Gemini models
|
||||
("gemini", "gemini-2.5-flash"),
|
||||
("gemini", "gemini-2.5-flash-lite"),
|
||||
@@ -57,6 +60,7 @@ def get_api_key_for_provider(provider: str) -> str | None:
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"groq": "GROQ_API_KEY",
|
||||
"gemini": "GEMINI_API_KEY",
|
||||
"deepseek": "DEEPSEEK_API_KEY",
|
||||
}
|
||||
env_var = provider_key_map.get(provider)
|
||||
return os.getenv(env_var) if env_var else None
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Tests for MCP server identity reported via serverInfo."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from hindsight_api import __version__ as HINDSIGHT_VERSION
|
||||
from hindsight_api.api.mcp import create_mcp_server
|
||||
|
||||
|
||||
def test_mcp_server_reports_hindsight_version():
|
||||
"""serverInfo.version should be Hindsight's version, not the FastMCP library version."""
|
||||
memory = MagicMock()
|
||||
server = create_mcp_server(memory, multi_bank=True)
|
||||
assert server.version == HINDSIGHT_VERSION
|
||||
@@ -1501,3 +1501,98 @@ async def test_tag_groups_nested_and_containing_or(api_client):
|
||||
assert any("Alice" in t and "step 8" in t for t in texts), "Should find Alice step:8"
|
||||
assert not any("step 9" in t for t in texts), "Should NOT find step 9"
|
||||
assert not any("Bob" in t for t in texts), "Should NOT find Bob"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for list_mental_model_tags API endpoint
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def _create_mental_model_via_engine(memory, *, bank_id, name, tags, request_context):
|
||||
"""Helper that creates a mental model directly through the engine without an LLM call."""
|
||||
# Ensure the bank exists (mental_models has a FK to banks).
|
||||
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
||||
return await memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
name=name,
|
||||
source_query=f"Source query for {name}",
|
||||
content=f"Content for {name}",
|
||||
tags=tags,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_mental_model_tags_returns_only_mental_model_tags(memory, request_context):
|
||||
"""Mental-model tag listing should reflect only mental_models.tags, not memory_units.tags."""
|
||||
bank_id = f"mm_tags_basic_{datetime.now().timestamp()}"
|
||||
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM A", tags=["topic:alpha", "shared"], request_context=request_context
|
||||
)
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM B", tags=["topic:beta", "shared"], request_context=request_context
|
||||
)
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM C", tags=["topic:alpha"], request_context=request_context
|
||||
)
|
||||
|
||||
result = await memory.list_mental_model_tags(bank_id=bank_id, request_context=request_context)
|
||||
|
||||
tags_map = {item["tag"]: item["count"] for item in result["items"]}
|
||||
assert tags_map == {"topic:alpha": 2, "topic:beta": 1, "shared": 2}
|
||||
assert result["total"] == 3
|
||||
|
||||
# Sanity check: the regular list_tags (which queries memory_units) should not see these tags
|
||||
# since no memories exist in this bank.
|
||||
memory_tags = await memory.list_tags(bank_id=bank_id, request_context=request_context)
|
||||
assert memory_tags["items"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_mental_model_tags_with_wildcard(memory, request_context):
|
||||
"""Wildcard 'topic:*' should only match mental-model tags with that prefix."""
|
||||
bank_id = f"mm_tags_wildcard_{datetime.now().timestamp()}"
|
||||
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM 1", tags=["topic:alpha", "user:alice"], request_context=request_context
|
||||
)
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM 2", tags=["topic:beta"], request_context=request_context
|
||||
)
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM 3", tags=["session:abc"], request_context=request_context
|
||||
)
|
||||
|
||||
result = await memory.list_mental_model_tags(
|
||||
bank_id=bank_id, pattern="topic:*", request_context=request_context
|
||||
)
|
||||
|
||||
returned = sorted(item["tag"] for item in result["items"])
|
||||
assert returned == ["topic:alpha", "topic:beta"]
|
||||
assert result["total"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tags_endpoint_with_source_mental_models(memory, request_context):
|
||||
"""`/tags?source=mental_models` returns mental-model tags, not memory_units tags."""
|
||||
bank_id = f"mm_tags_source_{datetime.now().timestamp()}"
|
||||
|
||||
await _create_mental_model_via_engine(
|
||||
memory, bank_id=bank_id, name="MM 1", tags=["alpha"], request_context=request_context
|
||||
)
|
||||
|
||||
app = create_app(memory, initialize_memory=False)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get(
|
||||
f"/v1/default/banks/{bank_id}/tags", params={"source": "mental_models"}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert {item["tag"] for item in body["items"]} == {"alpha"}
|
||||
|
||||
# Default source ('memories') must NOT pick up the mental-model tag.
|
||||
default_response = await client.get(f"/v1/default/banks/{bank_id}/tags")
|
||||
assert default_response.status_code == 200, default_response.text
|
||||
assert default_response.json()["items"] == []
|
||||
|
||||
@@ -712,7 +712,7 @@ impl ApiClient {
|
||||
self.runtime.block_on(async {
|
||||
let response = self
|
||||
.client
|
||||
.list_tags(bank_id, limit, offset, q, None)
|
||||
.list_tags(bank_id, limit, offset, q, None, None)
|
||||
.await?;
|
||||
Ok(response.into_inner())
|
||||
})
|
||||
|
||||
@@ -1776,7 +1776,9 @@ paths:
|
||||
/v1/default/banks/{bank_id}/tags:
|
||||
get:
|
||||
description: "List all unique tags in a memory bank with usage counts. Supports\
|
||||
\ wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive."
|
||||
\ wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.\
|
||||
\ Use `source=mental_models` to list tags used on mental models instead of\
|
||||
\ memories."
|
||||
operationId: list_tags
|
||||
parameters:
|
||||
- explode: false
|
||||
@@ -1797,6 +1799,22 @@ paths:
|
||||
nullable: true
|
||||
type: string
|
||||
style: form
|
||||
- description: "Where to read tags from: 'memories' (memory_units, default)\
|
||||
\ or 'mental_models'."
|
||||
explode: true
|
||||
in: query
|
||||
name: source
|
||||
required: false
|
||||
schema:
|
||||
default: memories
|
||||
description: "Where to read tags from: 'memories' (memory_units, default)\
|
||||
\ or 'mental_models'."
|
||||
enum:
|
||||
- memories
|
||||
- mental_models
|
||||
title: Source
|
||||
type: string
|
||||
style: form
|
||||
- description: Maximum number of tags to return
|
||||
explode: true
|
||||
in: query
|
||||
@@ -2127,8 +2145,8 @@ paths:
|
||||
/v1/default/banks/{bank_id}/profile:
|
||||
get:
|
||||
deprecated: true
|
||||
description: Get disposition traits and mission for a memory bank. Auto-creates
|
||||
agent with defaults if not exists.
|
||||
description: Get disposition traits and mission for a memory bank. Returns 404
|
||||
if the bank does not exist.
|
||||
operationId: get_bank_profile
|
||||
parameters:
|
||||
- explode: false
|
||||
|
||||
@@ -799,7 +799,7 @@ func (r ApiGetBankProfileRequest) Execute() (*BankProfileResponse, *http.Respons
|
||||
/*
|
||||
GetBankProfile Get memory bank profile
|
||||
|
||||
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
|
||||
Get disposition traits and mission for a memory bank. Returns 404 if the bank does not exist.
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param bankId
|
||||
|
||||
@@ -911,6 +911,7 @@ type ApiListTagsRequest struct {
|
||||
ApiService *MemoryAPIService
|
||||
bankId string
|
||||
q *string
|
||||
source *string
|
||||
limit *int32
|
||||
offset *int32
|
||||
authorization *string
|
||||
@@ -922,6 +923,12 @@ func (r ApiListTagsRequest) Q(q string) ApiListTagsRequest {
|
||||
return r
|
||||
}
|
||||
|
||||
// Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.
|
||||
func (r ApiListTagsRequest) Source(source string) ApiListTagsRequest {
|
||||
r.source = &source
|
||||
return r
|
||||
}
|
||||
|
||||
// Maximum number of tags to return
|
||||
func (r ApiListTagsRequest) Limit(limit int32) ApiListTagsRequest {
|
||||
r.limit = &limit
|
||||
@@ -946,7 +953,7 @@ func (r ApiListTagsRequest) Execute() (*ListTagsResponse, *http.Response, error)
|
||||
/*
|
||||
ListTags List tags
|
||||
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive. Use `source=mental_models` to list tags used on mental models instead of memories.
|
||||
|
||||
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||
@param bankId
|
||||
@@ -985,6 +992,12 @@ func (a *MemoryAPIService) ListTagsExecute(r ApiListTagsRequest) (*ListTagsRespo
|
||||
if r.q != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "q", r.q, "form", "")
|
||||
}
|
||||
if r.source != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "source", r.source, "form", "")
|
||||
} else {
|
||||
var defaultValue string = "memories"
|
||||
r.source = &defaultValue
|
||||
}
|
||||
if r.limit != nil {
|
||||
parameterAddToHeaderOrQuery(localVarQueryParams, "limit", r.limit, "form", "")
|
||||
} else {
|
||||
|
||||
@@ -1797,7 +1797,7 @@ class BanksApi:
|
||||
) -> BankProfileResponse:
|
||||
"""(Deprecated) Get memory bank profile
|
||||
|
||||
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
|
||||
Get disposition traits and mission for a memory bank. Returns 404 if the bank does not exist.
|
||||
|
||||
:param bank_id: (required)
|
||||
:type bank_id: str
|
||||
@@ -1870,7 +1870,7 @@ class BanksApi:
|
||||
) -> ApiResponse[BankProfileResponse]:
|
||||
"""(Deprecated) Get memory bank profile
|
||||
|
||||
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
|
||||
Get disposition traits and mission for a memory bank. Returns 404 if the bank does not exist.
|
||||
|
||||
:param bank_id: (required)
|
||||
:type bank_id: str
|
||||
@@ -1943,7 +1943,7 @@ class BanksApi:
|
||||
) -> RESTResponseType:
|
||||
"""(Deprecated) Get memory bank profile
|
||||
|
||||
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
|
||||
Get disposition traits and mission for a memory bank. Returns 404 if the bank does not exist.
|
||||
|
||||
:param bank_id: (required)
|
||||
:type bank_id: str
|
||||
|
||||
@@ -16,7 +16,7 @@ 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 Field, StrictInt, StrictStr
|
||||
from pydantic import Field, StrictInt, StrictStr, field_validator
|
||||
from typing import Any, List, Optional
|
||||
from typing_extensions import Annotated
|
||||
from hindsight_client_api.models.clear_memory_observations_response import ClearMemoryObservationsResponse
|
||||
@@ -1989,6 +1989,7 @@ class MemoryApi:
|
||||
self,
|
||||
bank_id: StrictStr,
|
||||
q: Annotated[Optional[StrictStr], Field(description="Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.")] = None,
|
||||
source: Annotated[Optional[StrictStr], Field(description="Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.")] = None,
|
||||
limit: Annotated[Optional[StrictInt], Field(description="Maximum number of tags to return")] = None,
|
||||
offset: Annotated[Optional[StrictInt], Field(description="Offset for pagination")] = None,
|
||||
authorization: Optional[StrictStr] = None,
|
||||
@@ -2007,12 +2008,14 @@ class MemoryApi:
|
||||
) -> ListTagsResponse:
|
||||
"""List tags
|
||||
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive. Use `source=mental_models` to list tags used on mental models instead of memories.
|
||||
|
||||
:param bank_id: (required)
|
||||
:type bank_id: str
|
||||
:param q: Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.
|
||||
:type q: str
|
||||
:param source: Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.
|
||||
:type source: str
|
||||
:param limit: Maximum number of tags to return
|
||||
:type limit: int
|
||||
:param offset: Offset for pagination
|
||||
@@ -2044,6 +2047,7 @@ class MemoryApi:
|
||||
_param = self._list_tags_serialize(
|
||||
bank_id=bank_id,
|
||||
q=q,
|
||||
source=source,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
authorization=authorization,
|
||||
@@ -2073,6 +2077,7 @@ class MemoryApi:
|
||||
self,
|
||||
bank_id: StrictStr,
|
||||
q: Annotated[Optional[StrictStr], Field(description="Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.")] = None,
|
||||
source: Annotated[Optional[StrictStr], Field(description="Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.")] = None,
|
||||
limit: Annotated[Optional[StrictInt], Field(description="Maximum number of tags to return")] = None,
|
||||
offset: Annotated[Optional[StrictInt], Field(description="Offset for pagination")] = None,
|
||||
authorization: Optional[StrictStr] = None,
|
||||
@@ -2091,12 +2096,14 @@ class MemoryApi:
|
||||
) -> ApiResponse[ListTagsResponse]:
|
||||
"""List tags
|
||||
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive. Use `source=mental_models` to list tags used on mental models instead of memories.
|
||||
|
||||
:param bank_id: (required)
|
||||
:type bank_id: str
|
||||
:param q: Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.
|
||||
:type q: str
|
||||
:param source: Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.
|
||||
:type source: str
|
||||
:param limit: Maximum number of tags to return
|
||||
:type limit: int
|
||||
:param offset: Offset for pagination
|
||||
@@ -2128,6 +2135,7 @@ class MemoryApi:
|
||||
_param = self._list_tags_serialize(
|
||||
bank_id=bank_id,
|
||||
q=q,
|
||||
source=source,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
authorization=authorization,
|
||||
@@ -2157,6 +2165,7 @@ class MemoryApi:
|
||||
self,
|
||||
bank_id: StrictStr,
|
||||
q: Annotated[Optional[StrictStr], Field(description="Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.")] = None,
|
||||
source: Annotated[Optional[StrictStr], Field(description="Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.")] = None,
|
||||
limit: Annotated[Optional[StrictInt], Field(description="Maximum number of tags to return")] = None,
|
||||
offset: Annotated[Optional[StrictInt], Field(description="Offset for pagination")] = None,
|
||||
authorization: Optional[StrictStr] = None,
|
||||
@@ -2175,12 +2184,14 @@ class MemoryApi:
|
||||
) -> RESTResponseType:
|
||||
"""List tags
|
||||
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.
|
||||
List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive. Use `source=mental_models` to list tags used on mental models instead of memories.
|
||||
|
||||
:param bank_id: (required)
|
||||
:type bank_id: str
|
||||
:param q: Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.
|
||||
:type q: str
|
||||
:param source: Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.
|
||||
:type source: str
|
||||
:param limit: Maximum number of tags to return
|
||||
:type limit: int
|
||||
:param offset: Offset for pagination
|
||||
@@ -2212,6 +2223,7 @@ class MemoryApi:
|
||||
_param = self._list_tags_serialize(
|
||||
bank_id=bank_id,
|
||||
q=q,
|
||||
source=source,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
authorization=authorization,
|
||||
@@ -2236,6 +2248,7 @@ class MemoryApi:
|
||||
self,
|
||||
bank_id,
|
||||
q,
|
||||
source,
|
||||
limit,
|
||||
offset,
|
||||
authorization,
|
||||
@@ -2267,6 +2280,10 @@ class MemoryApi:
|
||||
|
||||
_query_params.append(('q', q))
|
||||
|
||||
if source is not None:
|
||||
|
||||
_query_params.append(('source', source))
|
||||
|
||||
if limit is not None:
|
||||
|
||||
_query_params.append(('limit', limit))
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -13,18 +13,18 @@ yarn add @vectorize-io/hindsight-client
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
import { HindsightClient } from "@vectorize-io/hindsight-client";
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
const client = new HindsightClient({ baseUrl: "http://localhost:8888" });
|
||||
|
||||
// Retain information
|
||||
await client.retain('my-bank', 'Alice works at Google in Mountain View.');
|
||||
await client.retain("my-bank", "Alice works at Google in Mountain View.");
|
||||
|
||||
// Recall memories
|
||||
const results = await client.recall('my-bank', 'Where does Alice work?');
|
||||
const results = await client.recall("my-bank", "Where does Alice work?");
|
||||
|
||||
// Reflect and get an opinion
|
||||
const response = await client.reflect('my-bank', 'What do you think about Alice\'s career?');
|
||||
const response = await client.reflect("my-bank", "What do you think about Alice's career?");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
@@ -34,10 +34,10 @@ const response = await client.reflect('my-bank', 'What do you think about Alice\
|
||||
Store a single memory.
|
||||
|
||||
```typescript
|
||||
await client.retain('my-bank', 'User prefers dark mode', {
|
||||
await client.retain("my-bank", "User prefers dark mode", {
|
||||
timestamp: new Date(),
|
||||
context: 'Settings conversation',
|
||||
metadata: { source: 'chat' }
|
||||
context: "Settings conversation",
|
||||
metadata: { source: "chat" },
|
||||
});
|
||||
```
|
||||
|
||||
@@ -46,10 +46,11 @@ await client.retain('my-bank', 'User prefers dark mode', {
|
||||
Store multiple memories in batch.
|
||||
|
||||
```typescript
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Alice loves hiking' },
|
||||
{ content: 'Alice visited Paris last summer' }
|
||||
], { async: true });
|
||||
await client.retainBatch(
|
||||
"my-bank",
|
||||
[{ content: "Alice loves hiking" }, { content: "Alice visited Paris last summer" }],
|
||||
{ async: true }
|
||||
);
|
||||
```
|
||||
|
||||
### `recall(bankId, query, options?)`
|
||||
@@ -57,8 +58,8 @@ await client.retainBatch('my-bank', [
|
||||
Recall memories matching a query.
|
||||
|
||||
```typescript
|
||||
const results = await client.recall('my-bank', 'What are Alice\'s hobbies?', {
|
||||
budget: 'mid'
|
||||
const results = await client.recall("my-bank", "What are Alice's hobbies?", {
|
||||
budget: "mid",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -67,8 +68,8 @@ const results = await client.recall('my-bank', 'What are Alice\'s hobbies?', {
|
||||
Generate a contextual answer using the bank's identity and memories.
|
||||
|
||||
```typescript
|
||||
const response = await client.reflect('my-bank', 'What should I do this weekend?', {
|
||||
budget: 'low'
|
||||
const response = await client.reflect("my-bank", "What should I do this weekend?", {
|
||||
budget: "low",
|
||||
});
|
||||
console.log(response.text);
|
||||
```
|
||||
@@ -78,9 +79,9 @@ console.log(response.text);
|
||||
Create or update a memory bank with personality.
|
||||
|
||||
```typescript
|
||||
await client.createBank('my-bank', {
|
||||
name: 'My Assistant',
|
||||
background: 'A helpful assistant that remembers everything.'
|
||||
await client.createBank("my-bank", {
|
||||
name: "My Assistant",
|
||||
background: "A helpful assistant that remembers everything.",
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import {
|
||||
type ClientOptions,
|
||||
type Config,
|
||||
createClient,
|
||||
createConfig,
|
||||
} from "./client";
|
||||
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
|
||||
import type { ClientOptions as ClientOptions2 } from "./types.gen";
|
||||
|
||||
/**
|
||||
@@ -17,7 +12,7 @@ import type { ClientOptions as ClientOptions2 } from "./types.gen";
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
override?: Config<ClientOptions & T>
|
||||
) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export const client = createClient(createConfig<ClientOptions2>());
|
||||
|
||||
@@ -3,12 +3,7 @@
|
||||
import { createSseClient } from "../core/serverSentEvents.gen";
|
||||
import type { HttpMethod } from "../core/types.gen";
|
||||
import { getValidRequestBody } from "../core/utils.gen";
|
||||
import type {
|
||||
Client,
|
||||
Config,
|
||||
RequestOptions,
|
||||
ResolvedRequestOptions,
|
||||
} from "./types.gen";
|
||||
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen";
|
||||
import {
|
||||
buildUrl,
|
||||
createConfig,
|
||||
@@ -34,12 +29,7 @@ export const createClient = (config: Config = {}): Client => {
|
||||
return getConfig();
|
||||
};
|
||||
|
||||
const interceptors = createInterceptors<
|
||||
Request,
|
||||
Response,
|
||||
unknown,
|
||||
ResolvedRequestOptions
|
||||
>();
|
||||
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
|
||||
|
||||
const beforeRequest = async (options: RequestOptions) => {
|
||||
const opts = {
|
||||
@@ -107,12 +97,7 @@ export const createClient = (config: Config = {}): Client => {
|
||||
|
||||
for (const fn of interceptors.error.fns) {
|
||||
if (fn) {
|
||||
finalError = (await fn(
|
||||
error,
|
||||
undefined as any,
|
||||
request,
|
||||
opts,
|
||||
)) as unknown;
|
||||
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +134,7 @@ export const createClient = (config: Config = {}): Client => {
|
||||
? getParseAs(response.headers.get("Content-Type"))
|
||||
: opts.parseAs) ?? "json";
|
||||
|
||||
if (
|
||||
response.status === 204 ||
|
||||
response.headers.get("Content-Length") === "0"
|
||||
) {
|
||||
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
|
||||
let emptyData: any;
|
||||
switch (parseAs) {
|
||||
case "arrayBuffer":
|
||||
@@ -248,30 +230,28 @@ export const createClient = (config: Config = {}): Client => {
|
||||
};
|
||||
};
|
||||
|
||||
const makeMethodFn =
|
||||
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
||||
request({ ...options, method });
|
||||
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
||||
request({ ...options, method });
|
||||
|
||||
const makeSseFn =
|
||||
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
||||
const { opts, url } = await beforeRequest(options);
|
||||
return createSseClient({
|
||||
...opts,
|
||||
body: opts.body as BodyInit | null | undefined,
|
||||
headers: opts.headers as unknown as Record<string, string>,
|
||||
method,
|
||||
onRequest: async (url, init) => {
|
||||
let request = new Request(url, init);
|
||||
for (const fn of interceptors.request.fns) {
|
||||
if (fn) {
|
||||
request = await fn(request, opts);
|
||||
}
|
||||
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
||||
const { opts, url } = await beforeRequest(options);
|
||||
return createSseClient({
|
||||
...opts,
|
||||
body: opts.body as BodyInit | null | undefined,
|
||||
headers: opts.headers as unknown as Record<string, string>,
|
||||
method,
|
||||
onRequest: async (url, init) => {
|
||||
let request = new Request(url, init);
|
||||
for (const fn of interceptors.request.fns) {
|
||||
if (fn) {
|
||||
request = await fn(request, opts);
|
||||
}
|
||||
return request;
|
||||
},
|
||||
url,
|
||||
});
|
||||
};
|
||||
}
|
||||
return request;
|
||||
},
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
buildUrl,
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Auth } from "../core/auth.gen";
|
||||
import type {
|
||||
ServerSentEventsOptions,
|
||||
ServerSentEventsResult,
|
||||
} from "../core/serverSentEvents.gen";
|
||||
import type {
|
||||
Client as CoreClient,
|
||||
Config as CoreConfig,
|
||||
} from "../core/types.gen";
|
||||
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen";
|
||||
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen";
|
||||
import type { Middleware } from "./utils.gen";
|
||||
|
||||
export type ResponseStyle = "data" | "fields";
|
||||
@@ -41,14 +35,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
|
||||
*
|
||||
* @default 'auto'
|
||||
*/
|
||||
parseAs?:
|
||||
| "arrayBuffer"
|
||||
| "auto"
|
||||
| "blob"
|
||||
| "formData"
|
||||
| "json"
|
||||
| "stream"
|
||||
| "text";
|
||||
parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text";
|
||||
/**
|
||||
* Should we return only data or multiple fields (data, error, response, etc.)?
|
||||
*
|
||||
@@ -117,32 +104,22 @@ export type RequestResult<
|
||||
? TData[keyof TData]
|
||||
: TData
|
||||
: {
|
||||
data: TData extends Record<string, unknown>
|
||||
? TData[keyof TData]
|
||||
: TData;
|
||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
||||
request: Request;
|
||||
response: Response;
|
||||
}
|
||||
>
|
||||
: Promise<
|
||||
TResponseStyle extends "data"
|
||||
?
|
||||
| (TData extends Record<string, unknown>
|
||||
? TData[keyof TData]
|
||||
: TData)
|
||||
| undefined
|
||||
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined
|
||||
: (
|
||||
| {
|
||||
data: TData extends Record<string, unknown>
|
||||
? TData[keyof TData]
|
||||
: TData;
|
||||
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData;
|
||||
error: undefined;
|
||||
}
|
||||
| {
|
||||
data: undefined;
|
||||
error: TError extends Record<string, unknown>
|
||||
? TError[keyof TError]
|
||||
: TError;
|
||||
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError;
|
||||
}
|
||||
) & {
|
||||
request: Request;
|
||||
@@ -162,7 +139,7 @@ type MethodFn = <
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||
|
||||
type SseFn = <
|
||||
@@ -171,7 +148,7 @@ type SseFn = <
|
||||
ThrowOnError extends boolean = false,
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">
|
||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
||||
|
||||
type RequestFn = <
|
||||
@@ -181,10 +158,7 @@ type RequestFn = <
|
||||
TResponseStyle extends ResponseStyle = "fields",
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
|
||||
Pick<
|
||||
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
|
||||
"method"
|
||||
>,
|
||||
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">
|
||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||
|
||||
type BuildUrlFn = <
|
||||
@@ -195,16 +169,10 @@ type BuildUrlFn = <
|
||||
url: string;
|
||||
},
|
||||
>(
|
||||
options: TData & Options<TData>,
|
||||
options: TData & Options<TData>
|
||||
) => string;
|
||||
|
||||
export type Client = CoreClient<
|
||||
RequestFn,
|
||||
Config,
|
||||
MethodFn,
|
||||
BuildUrlFn,
|
||||
SseFn
|
||||
> & {
|
||||
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
|
||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
||||
};
|
||||
|
||||
@@ -217,7 +185,7 @@ export type Client = CoreClient<
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
override?: Config<ClientOptions & T>
|
||||
) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export interface TDataShape {
|
||||
|
||||
@@ -9,12 +9,7 @@ import {
|
||||
serializePrimitiveParam,
|
||||
} from "../core/pathSerializer.gen";
|
||||
import { getUrl } from "../core/utils.gen";
|
||||
import type {
|
||||
Client,
|
||||
ClientOptions,
|
||||
Config,
|
||||
RequestOptions,
|
||||
} from "./types.gen";
|
||||
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen";
|
||||
|
||||
export const createQuerySerializer = <T = unknown>({
|
||||
parameters = {},
|
||||
@@ -70,9 +65,7 @@ export const createQuerySerializer = <T = unknown>({
|
||||
/**
|
||||
* Infers parseAs value from provided Content-Type header.
|
||||
*/
|
||||
export const getParseAs = (
|
||||
contentType: string | null,
|
||||
): Exclude<Config["parseAs"], "auto"> => {
|
||||
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => {
|
||||
if (!contentType) {
|
||||
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
||||
// which is effectively the same as the 'stream' option.
|
||||
@@ -85,10 +78,7 @@ export const getParseAs = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
cleanContent.startsWith("application/json") ||
|
||||
cleanContent.endsWith("+json")
|
||||
) {
|
||||
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
@@ -97,9 +87,7 @@ export const getParseAs = (
|
||||
}
|
||||
|
||||
if (
|
||||
["application/", "audio/", "image/", "video/"].some((type) =>
|
||||
cleanContent.startsWith(type),
|
||||
)
|
||||
["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))
|
||||
) {
|
||||
return "blob";
|
||||
}
|
||||
@@ -115,7 +103,7 @@ const checkForExistence = (
|
||||
options: Pick<RequestOptions, "auth" | "query"> & {
|
||||
headers: Headers;
|
||||
},
|
||||
name?: string,
|
||||
name?: string
|
||||
): boolean => {
|
||||
if (!name) {
|
||||
return false;
|
||||
@@ -206,10 +194,7 @@ export const mergeHeaders = (
|
||||
continue;
|
||||
}
|
||||
|
||||
const iterator =
|
||||
header instanceof Headers
|
||||
? headersEntries(header)
|
||||
: Object.entries(header);
|
||||
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
|
||||
|
||||
for (const [key, value] of iterator) {
|
||||
if (value === null) {
|
||||
@@ -223,7 +208,7 @@ export const mergeHeaders = (
|
||||
// content value in OpenAPI specification is 'application/json'
|
||||
mergedHeaders.set(
|
||||
key,
|
||||
typeof value === "object" ? JSON.stringify(value) : (value as string),
|
||||
typeof value === "object" ? JSON.stringify(value) : (value as string)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -235,18 +220,15 @@ type ErrInterceptor<Err, Res, Req, Options> = (
|
||||
error: Err,
|
||||
response: Res,
|
||||
request: Req,
|
||||
options: Options,
|
||||
options: Options
|
||||
) => Err | Promise<Err>;
|
||||
|
||||
type ReqInterceptor<Req, Options> = (
|
||||
request: Req,
|
||||
options: Options,
|
||||
) => Req | Promise<Req>;
|
||||
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>;
|
||||
|
||||
type ResInterceptor<Res, Req, Options> = (
|
||||
response: Res,
|
||||
request: Req,
|
||||
options: Options,
|
||||
options: Options
|
||||
) => Res | Promise<Res>;
|
||||
|
||||
class Interceptors<Interceptor> {
|
||||
@@ -275,10 +257,7 @@ class Interceptors<Interceptor> {
|
||||
return this.fns.indexOf(id);
|
||||
}
|
||||
|
||||
update(
|
||||
id: number | Interceptor,
|
||||
fn: Interceptor,
|
||||
): number | Interceptor | false {
|
||||
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
|
||||
const index = this.getInterceptorIndex(id);
|
||||
if (this.fns[index]) {
|
||||
this.fns[index] = fn;
|
||||
@@ -327,7 +306,7 @@ const defaultHeaders = {
|
||||
};
|
||||
|
||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {}
|
||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||
...jsonBodySerializer,
|
||||
headers: defaultHeaders,
|
||||
|
||||
@@ -21,10 +21,9 @@ export interface Auth {
|
||||
|
||||
export const getAuthToken = async (
|
||||
auth: Auth,
|
||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken
|
||||
): Promise<string | undefined> => {
|
||||
const token =
|
||||
typeof callback === "function" ? await callback(auth) : callback;
|
||||
const token = typeof callback === "function" ? await callback(auth) : callback;
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type {
|
||||
ArrayStyle,
|
||||
ObjectStyle,
|
||||
SerializerOptions,
|
||||
} from "./pathSerializer.gen";
|
||||
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen";
|
||||
|
||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||
|
||||
@@ -24,11 +20,7 @@ export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
||||
parameters?: Record<string, QuerySerializerOptionsObject>;
|
||||
};
|
||||
|
||||
const serializeFormDataPair = (
|
||||
data: FormData,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
|
||||
if (typeof value === "string" || value instanceof Blob) {
|
||||
data.append(key, value);
|
||||
} else if (value instanceof Date) {
|
||||
@@ -38,11 +30,7 @@ const serializeFormDataPair = (
|
||||
}
|
||||
};
|
||||
|
||||
const serializeUrlSearchParamsPair = (
|
||||
data: URLSearchParams,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => {
|
||||
if (typeof value === "string") {
|
||||
data.append(key, value);
|
||||
} else {
|
||||
@@ -52,7 +40,7 @@ const serializeUrlSearchParamsPair = (
|
||||
|
||||
export const formDataBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||
body: T,
|
||||
body: T
|
||||
): FormData => {
|
||||
const data = new FormData();
|
||||
|
||||
@@ -73,15 +61,11 @@ export const formDataBodySerializer = {
|
||||
|
||||
export const jsonBodySerializer = {
|
||||
bodySerializer: <T>(body: T): string =>
|
||||
JSON.stringify(body, (_key, value) =>
|
||||
typeof value === "bigint" ? value.toString() : value,
|
||||
),
|
||||
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)),
|
||||
};
|
||||
|
||||
export const urlSearchParamsBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||
body: T,
|
||||
): string => {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => {
|
||||
const data = new URLSearchParams();
|
||||
|
||||
Object.entries(body).forEach(([key, value]) => {
|
||||
|
||||
@@ -102,10 +102,7 @@ const stripEmptySlots = (params: Params) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const buildClientParams = (
|
||||
args: ReadonlyArray<unknown>,
|
||||
fields: FieldsConfig,
|
||||
) => {
|
||||
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
|
||||
const params: Params = {
|
||||
body: {},
|
||||
headers: {},
|
||||
@@ -148,15 +145,11 @@ export const buildClientParams = (
|
||||
params[field.map] = value;
|
||||
}
|
||||
} else {
|
||||
const extra = extraPrefixes.find(([prefix]) =>
|
||||
key.startsWith(prefix),
|
||||
);
|
||||
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
|
||||
|
||||
if (extra) {
|
||||
const [prefix, slot] = extra;
|
||||
(params[slot] as Record<string, unknown>)[
|
||||
key.slice(prefix.length)
|
||||
] = value;
|
||||
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
|
||||
} else if ("allowExtra" in config && config.allowExtra) {
|
||||
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
||||
if (allowed) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
interface SerializeOptions<T>
|
||||
extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
|
||||
|
||||
interface SerializePrimitiveOptions {
|
||||
allowReserved?: boolean;
|
||||
@@ -104,9 +103,7 @@ export const serializeArrayParam = ({
|
||||
});
|
||||
})
|
||||
.join(separator);
|
||||
return style === "label" || style === "matrix"
|
||||
? separator + joinedValues
|
||||
: joinedValues;
|
||||
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
||||
};
|
||||
|
||||
export const serializePrimitiveParam = ({
|
||||
@@ -120,7 +117,7 @@ export const serializePrimitiveParam = ({
|
||||
|
||||
if (typeof value === "object") {
|
||||
throw new Error(
|
||||
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.",
|
||||
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,11 +142,7 @@ export const serializeObjectParam = ({
|
||||
if (style !== "deepObject" && !explode) {
|
||||
let values: string[] = [];
|
||||
Object.entries(value).forEach(([key, v]) => {
|
||||
values = [
|
||||
...values,
|
||||
key,
|
||||
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
||||
];
|
||||
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)];
|
||||
});
|
||||
const joinedValues = values.join(",");
|
||||
switch (style) {
|
||||
@@ -171,10 +164,8 @@ export const serializeObjectParam = ({
|
||||
allowReserved,
|
||||
name: style === "deepObject" ? `${name}[${key}]` : key,
|
||||
value: v as string,
|
||||
}),
|
||||
})
|
||||
)
|
||||
.join(separator);
|
||||
return style === "label" || style === "matrix"
|
||||
? separator + joinedValues
|
||||
: joinedValues;
|
||||
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
|
||||
};
|
||||
|
||||
@@ -15,11 +15,7 @@ export type JsonValue =
|
||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
||||
*/
|
||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
typeof value === "function" ||
|
||||
typeof value === "symbol"
|
||||
) {
|
||||
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
@@ -61,9 +57,7 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
||||
*/
|
||||
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
||||
const entries = Array.from(params.entries()).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||||
const result: Record<string, JsonValue> = {};
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
@@ -86,26 +80,16 @@ const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
||||
/**
|
||||
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
||||
*/
|
||||
export const serializeQueryKeyValue = (
|
||||
value: unknown,
|
||||
): JsonValue | undefined => {
|
||||
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
) {
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (
|
||||
value === undefined ||
|
||||
typeof value === "function" ||
|
||||
typeof value === "symbol"
|
||||
) {
|
||||
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -121,10 +105,7 @@ export const serializeQueryKeyValue = (
|
||||
return stringifyToJsonValue(value);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof URLSearchParams !== "undefined" &&
|
||||
value instanceof URLSearchParams
|
||||
) {
|
||||
if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) {
|
||||
return serializeSearchParams(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
import type { Config } from "./types.gen";
|
||||
|
||||
export type ServerSentEventsOptions<TData = unknown> = Omit<
|
||||
RequestInit,
|
||||
"method"
|
||||
> &
|
||||
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
|
||||
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
|
||||
/**
|
||||
* Fetch API implementation. You can use this option to provide a custom
|
||||
@@ -74,11 +71,7 @@ export interface StreamEvent<TData = unknown> {
|
||||
retry?: number;
|
||||
}
|
||||
|
||||
export type ServerSentEventsResult<
|
||||
TData = unknown,
|
||||
TReturn = void,
|
||||
TNext = unknown,
|
||||
> = {
|
||||
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
|
||||
stream: AsyncGenerator<
|
||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
||||
TReturn,
|
||||
@@ -101,9 +94,7 @@ export const createSseClient = <TData = unknown>({
|
||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||
let lastEventId: string | undefined;
|
||||
|
||||
const sleep =
|
||||
sseSleepFn ??
|
||||
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
|
||||
const createStream = async function* () {
|
||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
||||
@@ -141,16 +132,11 @@ export const createSseClient = <TData = unknown>({
|
||||
const _fetch = options.fetch ?? globalThis.fetch;
|
||||
const response = await _fetch(request);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`SSE failed: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
|
||||
|
||||
if (!response.body) throw new Error("No body in SSE response");
|
||||
|
||||
const reader = response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.getReader();
|
||||
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
||||
|
||||
let buffer = "";
|
||||
|
||||
@@ -186,10 +172,7 @@ export const createSseClient = <TData = unknown>({
|
||||
} else if (line.startsWith("id:")) {
|
||||
lastEventId = line.replace(/^id:\s*/, "");
|
||||
} else if (line.startsWith("retry:")) {
|
||||
const parsed = Number.parseInt(
|
||||
line.replace(/^retry:\s*/, ""),
|
||||
10,
|
||||
);
|
||||
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
retryDelay = parsed;
|
||||
}
|
||||
@@ -241,18 +224,12 @@ export const createSseClient = <TData = unknown>({
|
||||
// connection failed or aborted; retry after delay
|
||||
onSseError?.(error);
|
||||
|
||||
if (
|
||||
sseMaxRetryAttempts !== undefined &&
|
||||
attempt >= sseMaxRetryAttempts
|
||||
) {
|
||||
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
|
||||
break; // stop after firing error
|
||||
}
|
||||
|
||||
// exponential backoff: double retry each attempt, cap at 30s
|
||||
const backoff = Math.min(
|
||||
retryDelay * 2 ** (attempt - 1),
|
||||
sseMaxRetryDelay ?? 30000,
|
||||
);
|
||||
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
|
||||
await sleep(backoff);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Auth, AuthToken } from "./auth.gen";
|
||||
import type {
|
||||
BodySerializer,
|
||||
QuerySerializer,
|
||||
QuerySerializerOptions,
|
||||
} from "./bodySerializer.gen";
|
||||
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen";
|
||||
|
||||
export type HttpMethod =
|
||||
| "connect"
|
||||
@@ -34,9 +30,7 @@ export type Client<
|
||||
setConfig: (config: Config) => Config;
|
||||
} & {
|
||||
[K in HttpMethod]: MethodFn;
|
||||
} & ([SseFn] extends [never]
|
||||
? { sse?: never }
|
||||
: { sse: { [K in HttpMethod]: SseFn } });
|
||||
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
@@ -59,13 +53,7 @@ export interface Config {
|
||||
| RequestInit["headers"]
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| (string | number | boolean)[]
|
||||
| null
|
||||
| undefined
|
||||
| unknown
|
||||
string | number | boolean | (string | number | boolean)[] | null | undefined | unknown
|
||||
>;
|
||||
/**
|
||||
* The request method.
|
||||
@@ -112,7 +100,5 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||
: false;
|
||||
|
||||
export type OmitNever<T extends Record<string, unknown>> = {
|
||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
||||
? never
|
||||
: K]: T[K];
|
||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K];
|
||||
};
|
||||
|
||||
@@ -44,10 +44,7 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeArrayParam({ explode, name, style, value }),
|
||||
);
|
||||
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -60,7 +57,7 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
style,
|
||||
value: value as Record<string, unknown>,
|
||||
valueOnly: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -71,13 +68,13 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
`;${serializePrimitiveParam({
|
||||
name,
|
||||
value: value as string,
|
||||
})}`,
|
||||
})}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const replaceValue = encodeURIComponent(
|
||||
style === "label" ? `.${value as string}` : (value as string),
|
||||
style === "label" ? `.${value as string}` : (value as string)
|
||||
);
|
||||
url = url.replace(match, replaceValue);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1869,12 +1869,7 @@ export type MemoryItem = {
|
||||
*
|
||||
* How to scope observations during consolidation. 'per_tag' runs one consolidation pass per individual tag, creating separate observations for each tag. 'combined' (default) runs a single pass with all tags together. A list of tag lists runs one pass per inner list, giving full control over which combinations to use.
|
||||
*/
|
||||
observation_scopes?:
|
||||
| "per_tag"
|
||||
| "combined"
|
||||
| "all_combinations"
|
||||
| Array<Array<string>>
|
||||
| null;
|
||||
observation_scopes?: "per_tag" | "combined" | "all_combinations" | Array<Array<string>> | null;
|
||||
/**
|
||||
* Strategy
|
||||
*
|
||||
@@ -2041,9 +2036,7 @@ export type MentalModelTriggerInput = {
|
||||
*
|
||||
* Compound boolean tag expressions to use during refresh instead of the model's own tags. When set, these tag groups are passed to reflect and the model's flat tags are NOT used for filtering. Supports nested and/or/not expressions for complex tag-based scoping.
|
||||
*/
|
||||
tag_groups?: Array<
|
||||
TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput
|
||||
> | null;
|
||||
tag_groups?: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput> | null;
|
||||
/**
|
||||
* Include Chunks
|
||||
*
|
||||
@@ -2195,13 +2188,7 @@ export type OperationStatusResponse = {
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status:
|
||||
| "pending"
|
||||
| "processing"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "not_found";
|
||||
status: "pending" | "processing" | "completed" | "failed" | "cancelled" | "not_found";
|
||||
/**
|
||||
* Operation Type
|
||||
*/
|
||||
@@ -2338,9 +2325,7 @@ export type RecallRequest = {
|
||||
*
|
||||
* Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.
|
||||
*/
|
||||
tag_groups?: Array<
|
||||
TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput
|
||||
> | null;
|
||||
tag_groups?: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2661,9 +2646,7 @@ export type ReflectRequest = {
|
||||
*
|
||||
* Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.
|
||||
*/
|
||||
tag_groups?: Array<
|
||||
TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput
|
||||
> | null;
|
||||
tag_groups?: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput> | null;
|
||||
/**
|
||||
* Fact Types
|
||||
*
|
||||
@@ -2917,9 +2900,7 @@ export type TagGroupAndInput = {
|
||||
/**
|
||||
* And
|
||||
*/
|
||||
and: Array<
|
||||
TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput
|
||||
>;
|
||||
and: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2931,9 +2912,7 @@ export type TagGroupAndOutput = {
|
||||
/**
|
||||
* And
|
||||
*/
|
||||
and: Array<
|
||||
TagGroupLeaf | TagGroupAndOutput | TagGroupOrOutput | TagGroupNotOutput
|
||||
>;
|
||||
and: Array<TagGroupLeaf | TagGroupAndOutput | TagGroupOrOutput | TagGroupNotOutput>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2985,9 +2964,7 @@ export type TagGroupOrInput = {
|
||||
/**
|
||||
* Or
|
||||
*/
|
||||
or: Array<
|
||||
TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput
|
||||
>;
|
||||
or: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2999,9 +2976,7 @@ export type TagGroupOrOutput = {
|
||||
/**
|
||||
* Or
|
||||
*/
|
||||
or: Array<
|
||||
TagGroupLeaf | TagGroupAndOutput | TagGroupOrOutput | TagGroupNotOutput
|
||||
>;
|
||||
or: Array<TagGroupLeaf | TagGroupAndOutput | TagGroupOrOutput | TagGroupNotOutput>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -3593,8 +3568,7 @@ export type ListMemoriesResponses = {
|
||||
200: ListMemoryUnitsResponse;
|
||||
};
|
||||
|
||||
export type ListMemoriesResponse =
|
||||
ListMemoriesResponses[keyof ListMemoriesResponses];
|
||||
export type ListMemoriesResponse = ListMemoriesResponses[keyof ListMemoriesResponses];
|
||||
|
||||
export type GetMemoryData = {
|
||||
body?: never;
|
||||
@@ -3698,8 +3672,7 @@ export type RecallMemoriesErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type RecallMemoriesError =
|
||||
RecallMemoriesErrors[keyof RecallMemoriesErrors];
|
||||
export type RecallMemoriesError = RecallMemoriesErrors[keyof RecallMemoriesErrors];
|
||||
|
||||
export type RecallMemoriesResponses = {
|
||||
/**
|
||||
@@ -3708,8 +3681,7 @@ export type RecallMemoriesResponses = {
|
||||
200: RecallResponse;
|
||||
};
|
||||
|
||||
export type RecallMemoriesResponse =
|
||||
RecallMemoriesResponses[keyof RecallMemoriesResponses];
|
||||
export type RecallMemoriesResponse = RecallMemoriesResponses[keyof RecallMemoriesResponses];
|
||||
|
||||
export type ReflectData = {
|
||||
body: ReflectRequest;
|
||||
@@ -3812,8 +3784,7 @@ export type GetAgentStatsResponses = {
|
||||
200: BankStatsResponse;
|
||||
};
|
||||
|
||||
export type GetAgentStatsResponse =
|
||||
GetAgentStatsResponses[keyof GetAgentStatsResponses];
|
||||
export type GetAgentStatsResponse = GetAgentStatsResponses[keyof GetAgentStatsResponses];
|
||||
|
||||
export type GetMemoriesTimeseriesData = {
|
||||
body?: never;
|
||||
@@ -3905,8 +3876,7 @@ export type ListEntitiesResponses = {
|
||||
200: EntityListResponse;
|
||||
};
|
||||
|
||||
export type ListEntitiesResponse =
|
||||
ListEntitiesResponses[keyof ListEntitiesResponses];
|
||||
export type ListEntitiesResponse = ListEntitiesResponses[keyof ListEntitiesResponses];
|
||||
|
||||
export type GetEntityGraphData = {
|
||||
body?: never;
|
||||
@@ -3946,8 +3916,7 @@ export type GetEntityGraphErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetEntityGraphError =
|
||||
GetEntityGraphErrors[keyof GetEntityGraphErrors];
|
||||
export type GetEntityGraphError = GetEntityGraphErrors[keyof GetEntityGraphErrors];
|
||||
|
||||
export type GetEntityGraphResponses = {
|
||||
/**
|
||||
@@ -3956,8 +3925,7 @@ export type GetEntityGraphResponses = {
|
||||
200: EntityGraphResponse;
|
||||
};
|
||||
|
||||
export type GetEntityGraphResponse =
|
||||
GetEntityGraphResponses[keyof GetEntityGraphResponses];
|
||||
export type GetEntityGraphResponse = GetEntityGraphResponses[keyof GetEntityGraphResponses];
|
||||
|
||||
export type GetEntityData = {
|
||||
body?: never;
|
||||
@@ -4093,8 +4061,7 @@ export type ListMentalModelsErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ListMentalModelsError =
|
||||
ListMentalModelsErrors[keyof ListMentalModelsErrors];
|
||||
export type ListMentalModelsError = ListMentalModelsErrors[keyof ListMentalModelsErrors];
|
||||
|
||||
export type ListMentalModelsResponses = {
|
||||
/**
|
||||
@@ -4103,8 +4070,7 @@ export type ListMentalModelsResponses = {
|
||||
200: MentalModelListResponse;
|
||||
};
|
||||
|
||||
export type ListMentalModelsResponse =
|
||||
ListMentalModelsResponses[keyof ListMentalModelsResponses];
|
||||
export type ListMentalModelsResponse = ListMentalModelsResponses[keyof ListMentalModelsResponses];
|
||||
|
||||
export type CreateMentalModelData = {
|
||||
body: CreateMentalModelRequest;
|
||||
@@ -4131,8 +4097,7 @@ export type CreateMentalModelErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CreateMentalModelError =
|
||||
CreateMentalModelErrors[keyof CreateMentalModelErrors];
|
||||
export type CreateMentalModelError = CreateMentalModelErrors[keyof CreateMentalModelErrors];
|
||||
|
||||
export type CreateMentalModelResponses = {
|
||||
/**
|
||||
@@ -4173,8 +4138,7 @@ export type DeleteMentalModelErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteMentalModelError =
|
||||
DeleteMentalModelErrors[keyof DeleteMentalModelErrors];
|
||||
export type DeleteMentalModelError = DeleteMentalModelErrors[keyof DeleteMentalModelErrors];
|
||||
|
||||
export type DeleteMentalModelResponses = {
|
||||
/**
|
||||
@@ -4219,8 +4183,7 @@ export type GetMentalModelErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetMentalModelError =
|
||||
GetMentalModelErrors[keyof GetMentalModelErrors];
|
||||
export type GetMentalModelError = GetMentalModelErrors[keyof GetMentalModelErrors];
|
||||
|
||||
export type GetMentalModelResponses = {
|
||||
/**
|
||||
@@ -4229,8 +4192,7 @@ export type GetMentalModelResponses = {
|
||||
200: MentalModelResponse;
|
||||
};
|
||||
|
||||
export type GetMentalModelResponse =
|
||||
GetMentalModelResponses[keyof GetMentalModelResponses];
|
||||
export type GetMentalModelResponse = GetMentalModelResponses[keyof GetMentalModelResponses];
|
||||
|
||||
export type UpdateMentalModelData = {
|
||||
body: UpdateMentalModelRequest;
|
||||
@@ -4261,8 +4223,7 @@ export type UpdateMentalModelErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateMentalModelError =
|
||||
UpdateMentalModelErrors[keyof UpdateMentalModelErrors];
|
||||
export type UpdateMentalModelError = UpdateMentalModelErrors[keyof UpdateMentalModelErrors];
|
||||
|
||||
export type UpdateMentalModelResponses = {
|
||||
/**
|
||||
@@ -4342,8 +4303,7 @@ export type RefreshMentalModelErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type RefreshMentalModelError =
|
||||
RefreshMentalModelErrors[keyof RefreshMentalModelErrors];
|
||||
export type RefreshMentalModelError = RefreshMentalModelErrors[keyof RefreshMentalModelErrors];
|
||||
|
||||
export type RefreshMentalModelResponses = {
|
||||
/**
|
||||
@@ -4407,8 +4367,7 @@ export type ListDirectivesErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ListDirectivesError =
|
||||
ListDirectivesErrors[keyof ListDirectivesErrors];
|
||||
export type ListDirectivesError = ListDirectivesErrors[keyof ListDirectivesErrors];
|
||||
|
||||
export type ListDirectivesResponses = {
|
||||
/**
|
||||
@@ -4417,8 +4376,7 @@ export type ListDirectivesResponses = {
|
||||
200: DirectiveListResponse;
|
||||
};
|
||||
|
||||
export type ListDirectivesResponse =
|
||||
ListDirectivesResponses[keyof ListDirectivesResponses];
|
||||
export type ListDirectivesResponse = ListDirectivesResponses[keyof ListDirectivesResponses];
|
||||
|
||||
export type CreateDirectiveData = {
|
||||
body: CreateDirectiveRequest;
|
||||
@@ -4445,8 +4403,7 @@ export type CreateDirectiveErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CreateDirectiveError =
|
||||
CreateDirectiveErrors[keyof CreateDirectiveErrors];
|
||||
export type CreateDirectiveError = CreateDirectiveErrors[keyof CreateDirectiveErrors];
|
||||
|
||||
export type CreateDirectiveResponses = {
|
||||
/**
|
||||
@@ -4455,8 +4412,7 @@ export type CreateDirectiveResponses = {
|
||||
200: DirectiveResponse;
|
||||
};
|
||||
|
||||
export type CreateDirectiveResponse =
|
||||
CreateDirectiveResponses[keyof CreateDirectiveResponses];
|
||||
export type CreateDirectiveResponse = CreateDirectiveResponses[keyof CreateDirectiveResponses];
|
||||
|
||||
export type DeleteDirectiveData = {
|
||||
body?: never;
|
||||
@@ -4487,8 +4443,7 @@ export type DeleteDirectiveErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteDirectiveError =
|
||||
DeleteDirectiveErrors[keyof DeleteDirectiveErrors];
|
||||
export type DeleteDirectiveError = DeleteDirectiveErrors[keyof DeleteDirectiveErrors];
|
||||
|
||||
export type DeleteDirectiveResponses = {
|
||||
/**
|
||||
@@ -4535,8 +4490,7 @@ export type GetDirectiveResponses = {
|
||||
200: DirectiveResponse;
|
||||
};
|
||||
|
||||
export type GetDirectiveResponse =
|
||||
GetDirectiveResponses[keyof GetDirectiveResponses];
|
||||
export type GetDirectiveResponse = GetDirectiveResponses[keyof GetDirectiveResponses];
|
||||
|
||||
export type UpdateDirectiveData = {
|
||||
body: UpdateDirectiveRequest;
|
||||
@@ -4567,8 +4521,7 @@ export type UpdateDirectiveErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateDirectiveError =
|
||||
UpdateDirectiveErrors[keyof UpdateDirectiveErrors];
|
||||
export type UpdateDirectiveError = UpdateDirectiveErrors[keyof UpdateDirectiveErrors];
|
||||
|
||||
export type UpdateDirectiveResponses = {
|
||||
/**
|
||||
@@ -4577,8 +4530,7 @@ export type UpdateDirectiveResponses = {
|
||||
200: DirectiveResponse;
|
||||
};
|
||||
|
||||
export type UpdateDirectiveResponse =
|
||||
UpdateDirectiveResponses[keyof UpdateDirectiveResponses];
|
||||
export type UpdateDirectiveResponse = UpdateDirectiveResponses[keyof UpdateDirectiveResponses];
|
||||
|
||||
export type ListDocumentsData = {
|
||||
body?: never;
|
||||
@@ -4641,8 +4593,7 @@ export type ListDocumentsResponses = {
|
||||
200: ListDocumentsResponse;
|
||||
};
|
||||
|
||||
export type ListDocumentsResponse2 =
|
||||
ListDocumentsResponses[keyof ListDocumentsResponses];
|
||||
export type ListDocumentsResponse2 = ListDocumentsResponses[keyof ListDocumentsResponses];
|
||||
|
||||
export type ListDocumentChunksData = {
|
||||
body?: never;
|
||||
@@ -4686,8 +4637,7 @@ export type ListDocumentChunksErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ListDocumentChunksError =
|
||||
ListDocumentChunksErrors[keyof ListDocumentChunksErrors];
|
||||
export type ListDocumentChunksError = ListDocumentChunksErrors[keyof ListDocumentChunksErrors];
|
||||
|
||||
export type ListDocumentChunksResponses = {
|
||||
/**
|
||||
@@ -4728,8 +4678,7 @@ export type ReprocessDocumentErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ReprocessDocumentError =
|
||||
ReprocessDocumentErrors[keyof ReprocessDocumentErrors];
|
||||
export type ReprocessDocumentError = ReprocessDocumentErrors[keyof ReprocessDocumentErrors];
|
||||
|
||||
export type ReprocessDocumentResponses = {
|
||||
/**
|
||||
@@ -4770,8 +4719,7 @@ export type DeleteDocumentErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteDocumentError =
|
||||
DeleteDocumentErrors[keyof DeleteDocumentErrors];
|
||||
export type DeleteDocumentError = DeleteDocumentErrors[keyof DeleteDocumentErrors];
|
||||
|
||||
export type DeleteDocumentResponses = {
|
||||
/**
|
||||
@@ -4780,8 +4728,7 @@ export type DeleteDocumentResponses = {
|
||||
200: DeleteDocumentResponse;
|
||||
};
|
||||
|
||||
export type DeleteDocumentResponse2 =
|
||||
DeleteDocumentResponses[keyof DeleteDocumentResponses];
|
||||
export type DeleteDocumentResponse2 = DeleteDocumentResponses[keyof DeleteDocumentResponses];
|
||||
|
||||
export type GetDocumentData = {
|
||||
body?: never;
|
||||
@@ -4821,8 +4768,7 @@ export type GetDocumentResponses = {
|
||||
200: DocumentResponse;
|
||||
};
|
||||
|
||||
export type GetDocumentResponse =
|
||||
GetDocumentResponses[keyof GetDocumentResponses];
|
||||
export type GetDocumentResponse = GetDocumentResponses[keyof GetDocumentResponses];
|
||||
|
||||
export type UpdateDocumentData = {
|
||||
body: UpdateDocumentRequest;
|
||||
@@ -4853,8 +4799,7 @@ export type UpdateDocumentErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateDocumentError =
|
||||
UpdateDocumentErrors[keyof UpdateDocumentErrors];
|
||||
export type UpdateDocumentError = UpdateDocumentErrors[keyof UpdateDocumentErrors];
|
||||
|
||||
export type UpdateDocumentResponses = {
|
||||
/**
|
||||
@@ -4863,8 +4808,7 @@ export type UpdateDocumentResponses = {
|
||||
200: UpdateDocumentResponse;
|
||||
};
|
||||
|
||||
export type UpdateDocumentResponse2 =
|
||||
UpdateDocumentResponses[keyof UpdateDocumentResponses];
|
||||
export type UpdateDocumentResponse2 = UpdateDocumentResponses[keyof UpdateDocumentResponses];
|
||||
|
||||
export type ListTagsData = {
|
||||
body?: never;
|
||||
@@ -4887,6 +4831,12 @@ export type ListTagsData = {
|
||||
* Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive.
|
||||
*/
|
||||
q?: string | null;
|
||||
/**
|
||||
* Source
|
||||
*
|
||||
* Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.
|
||||
*/
|
||||
source?: "memories" | "mental_models";
|
||||
/**
|
||||
* Limit
|
||||
*
|
||||
@@ -5013,8 +4963,7 @@ export type ListOperationsErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ListOperationsError =
|
||||
ListOperationsErrors[keyof ListOperationsErrors];
|
||||
export type ListOperationsError = ListOperationsErrors[keyof ListOperationsErrors];
|
||||
|
||||
export type ListOperationsResponses = {
|
||||
/**
|
||||
@@ -5023,8 +4972,7 @@ export type ListOperationsResponses = {
|
||||
200: OperationsListResponse;
|
||||
};
|
||||
|
||||
export type ListOperationsResponse =
|
||||
ListOperationsResponses[keyof ListOperationsResponses];
|
||||
export type ListOperationsResponse = ListOperationsResponses[keyof ListOperationsResponses];
|
||||
|
||||
export type CancelOperationData = {
|
||||
body?: never;
|
||||
@@ -5055,8 +5003,7 @@ export type CancelOperationErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CancelOperationError =
|
||||
CancelOperationErrors[keyof CancelOperationErrors];
|
||||
export type CancelOperationError = CancelOperationErrors[keyof CancelOperationErrors];
|
||||
|
||||
export type CancelOperationResponses = {
|
||||
/**
|
||||
@@ -5065,8 +5012,7 @@ export type CancelOperationResponses = {
|
||||
200: CancelOperationResponse;
|
||||
};
|
||||
|
||||
export type CancelOperationResponse2 =
|
||||
CancelOperationResponses[keyof CancelOperationResponses];
|
||||
export type CancelOperationResponse2 = CancelOperationResponses[keyof CancelOperationResponses];
|
||||
|
||||
export type GetOperationStatusData = {
|
||||
body?: never;
|
||||
@@ -5104,8 +5050,7 @@ export type GetOperationStatusErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetOperationStatusError =
|
||||
GetOperationStatusErrors[keyof GetOperationStatusErrors];
|
||||
export type GetOperationStatusError = GetOperationStatusErrors[keyof GetOperationStatusErrors];
|
||||
|
||||
export type GetOperationStatusResponses = {
|
||||
/**
|
||||
@@ -5146,8 +5091,7 @@ export type RetryOperationErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type RetryOperationError =
|
||||
RetryOperationErrors[keyof RetryOperationErrors];
|
||||
export type RetryOperationError = RetryOperationErrors[keyof RetryOperationErrors];
|
||||
|
||||
export type RetryOperationResponses = {
|
||||
/**
|
||||
@@ -5156,8 +5100,7 @@ export type RetryOperationResponses = {
|
||||
200: RetryOperationResponse;
|
||||
};
|
||||
|
||||
export type RetryOperationResponse2 =
|
||||
RetryOperationResponses[keyof RetryOperationResponses];
|
||||
export type RetryOperationResponse2 = RetryOperationResponses[keyof RetryOperationResponses];
|
||||
|
||||
export type GetBankProfileData = {
|
||||
body?: never;
|
||||
@@ -5184,8 +5127,7 @@ export type GetBankProfileErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetBankProfileError =
|
||||
GetBankProfileErrors[keyof GetBankProfileErrors];
|
||||
export type GetBankProfileError = GetBankProfileErrors[keyof GetBankProfileErrors];
|
||||
|
||||
export type GetBankProfileResponses = {
|
||||
/**
|
||||
@@ -5194,8 +5136,7 @@ export type GetBankProfileResponses = {
|
||||
200: BankProfileResponse;
|
||||
};
|
||||
|
||||
export type GetBankProfileResponse =
|
||||
GetBankProfileResponses[keyof GetBankProfileResponses];
|
||||
export type GetBankProfileResponse = GetBankProfileResponses[keyof GetBankProfileResponses];
|
||||
|
||||
export type UpdateBankDispositionData = {
|
||||
body: UpdateDispositionRequest;
|
||||
@@ -5260,8 +5201,7 @@ export type AddBankBackgroundErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type AddBankBackgroundError =
|
||||
AddBankBackgroundErrors[keyof AddBankBackgroundErrors];
|
||||
export type AddBankBackgroundError = AddBankBackgroundErrors[keyof AddBankBackgroundErrors];
|
||||
|
||||
export type AddBankBackgroundResponses = {
|
||||
/**
|
||||
@@ -5370,8 +5310,7 @@ export type CreateOrUpdateBankErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type CreateOrUpdateBankError =
|
||||
CreateOrUpdateBankErrors[keyof CreateOrUpdateBankErrors];
|
||||
export type CreateOrUpdateBankError = CreateOrUpdateBankErrors[keyof CreateOrUpdateBankErrors];
|
||||
|
||||
export type CreateOrUpdateBankResponses = {
|
||||
/**
|
||||
@@ -5415,8 +5354,7 @@ export type ImportBankTemplateErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ImportBankTemplateError =
|
||||
ImportBankTemplateErrors[keyof ImportBankTemplateErrors];
|
||||
export type ImportBankTemplateError = ImportBankTemplateErrors[keyof ImportBankTemplateErrors];
|
||||
|
||||
export type ImportBankTemplateResponses = {
|
||||
/**
|
||||
@@ -5453,8 +5391,7 @@ export type ExportBankTemplateErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ExportBankTemplateError =
|
||||
ExportBankTemplateErrors[keyof ExportBankTemplateErrors];
|
||||
export type ExportBankTemplateError = ExportBankTemplateErrors[keyof ExportBankTemplateErrors];
|
||||
|
||||
export type ExportBankTemplateResponses = {
|
||||
/**
|
||||
@@ -5505,8 +5442,7 @@ export type ClearObservationsErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ClearObservationsError =
|
||||
ClearObservationsErrors[keyof ClearObservationsErrors];
|
||||
export type ClearObservationsError = ClearObservationsErrors[keyof ClearObservationsErrors];
|
||||
|
||||
export type ClearObservationsResponses = {
|
||||
/**
|
||||
@@ -5623,8 +5559,7 @@ export type ResetBankConfigErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ResetBankConfigError =
|
||||
ResetBankConfigErrors[keyof ResetBankConfigErrors];
|
||||
export type ResetBankConfigError = ResetBankConfigErrors[keyof ResetBankConfigErrors];
|
||||
|
||||
export type ResetBankConfigResponses = {
|
||||
/**
|
||||
@@ -5633,8 +5568,7 @@ export type ResetBankConfigResponses = {
|
||||
200: BankConfigResponse;
|
||||
};
|
||||
|
||||
export type ResetBankConfigResponse =
|
||||
ResetBankConfigResponses[keyof ResetBankConfigResponses];
|
||||
export type ResetBankConfigResponse = ResetBankConfigResponses[keyof ResetBankConfigResponses];
|
||||
|
||||
export type GetBankConfigData = {
|
||||
body?: never;
|
||||
@@ -5670,8 +5604,7 @@ export type GetBankConfigResponses = {
|
||||
200: BankConfigResponse;
|
||||
};
|
||||
|
||||
export type GetBankConfigResponse =
|
||||
GetBankConfigResponses[keyof GetBankConfigResponses];
|
||||
export type GetBankConfigResponse = GetBankConfigResponses[keyof GetBankConfigResponses];
|
||||
|
||||
export type UpdateBankConfigData = {
|
||||
body: BankConfigUpdate;
|
||||
@@ -5698,8 +5631,7 @@ export type UpdateBankConfigErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type UpdateBankConfigError =
|
||||
UpdateBankConfigErrors[keyof UpdateBankConfigErrors];
|
||||
export type UpdateBankConfigError = UpdateBankConfigErrors[keyof UpdateBankConfigErrors];
|
||||
|
||||
export type UpdateBankConfigResponses = {
|
||||
/**
|
||||
@@ -5708,8 +5640,7 @@ export type UpdateBankConfigResponses = {
|
||||
200: BankConfigResponse;
|
||||
};
|
||||
|
||||
export type UpdateBankConfigResponse =
|
||||
UpdateBankConfigResponses[keyof UpdateBankConfigResponses];
|
||||
export type UpdateBankConfigResponse = UpdateBankConfigResponses[keyof UpdateBankConfigResponses];
|
||||
|
||||
export type TriggerConsolidationData = {
|
||||
body?: never;
|
||||
@@ -5783,8 +5714,7 @@ export type ListWebhooksResponses = {
|
||||
200: WebhookListResponse;
|
||||
};
|
||||
|
||||
export type ListWebhooksResponse =
|
||||
ListWebhooksResponses[keyof ListWebhooksResponses];
|
||||
export type ListWebhooksResponse = ListWebhooksResponses[keyof ListWebhooksResponses];
|
||||
|
||||
export type CreateWebhookData = {
|
||||
body: CreateWebhookRequest;
|
||||
@@ -5820,8 +5750,7 @@ export type CreateWebhookResponses = {
|
||||
201: WebhookResponse;
|
||||
};
|
||||
|
||||
export type CreateWebhookResponse =
|
||||
CreateWebhookResponses[keyof CreateWebhookResponses];
|
||||
export type CreateWebhookResponse = CreateWebhookResponses[keyof CreateWebhookResponses];
|
||||
|
||||
export type DeleteWebhookData = {
|
||||
body?: never;
|
||||
@@ -5861,8 +5790,7 @@ export type DeleteWebhookResponses = {
|
||||
200: DeleteResponse;
|
||||
};
|
||||
|
||||
export type DeleteWebhookResponse =
|
||||
DeleteWebhookResponses[keyof DeleteWebhookResponses];
|
||||
export type DeleteWebhookResponse = DeleteWebhookResponses[keyof DeleteWebhookResponses];
|
||||
|
||||
export type UpdateWebhookData = {
|
||||
body: UpdateWebhookRequest;
|
||||
@@ -5902,8 +5830,7 @@ export type UpdateWebhookResponses = {
|
||||
200: WebhookResponse;
|
||||
};
|
||||
|
||||
export type UpdateWebhookResponse =
|
||||
UpdateWebhookResponses[keyof UpdateWebhookResponses];
|
||||
export type UpdateWebhookResponse = UpdateWebhookResponses[keyof UpdateWebhookResponses];
|
||||
|
||||
export type ListWebhookDeliveriesData = {
|
||||
body?: never;
|
||||
@@ -5992,8 +5919,7 @@ export type ClearBankMemoriesErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ClearBankMemoriesError =
|
||||
ClearBankMemoriesErrors[keyof ClearBankMemoriesErrors];
|
||||
export type ClearBankMemoriesError = ClearBankMemoriesErrors[keyof ClearBankMemoriesErrors];
|
||||
|
||||
export type ClearBankMemoriesResponses = {
|
||||
/**
|
||||
@@ -6030,8 +5956,7 @@ export type RetainMemoriesErrors = {
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type RetainMemoriesError =
|
||||
RetainMemoriesErrors[keyof RetainMemoriesErrors];
|
||||
export type RetainMemoriesError = RetainMemoriesErrors[keyof RetainMemoriesErrors];
|
||||
|
||||
export type RetainMemoriesResponses = {
|
||||
/**
|
||||
@@ -6040,8 +5965,7 @@ export type RetainMemoriesResponses = {
|
||||
200: RetainResponse;
|
||||
};
|
||||
|
||||
export type RetainMemoriesResponse =
|
||||
RetainMemoriesResponses[keyof RetainMemoriesResponses];
|
||||
export type RetainMemoriesResponse = RetainMemoriesResponses[keyof RetainMemoriesResponses];
|
||||
|
||||
export type FileRetainData = {
|
||||
body: BodyFileRetain;
|
||||
@@ -6077,8 +6001,7 @@ export type FileRetainResponses = {
|
||||
200: FileRetainResponse;
|
||||
};
|
||||
|
||||
export type FileRetainResponse2 =
|
||||
FileRetainResponses[keyof FileRetainResponses];
|
||||
export type FileRetainResponse2 = FileRetainResponses[keyof FileRetainResponses];
|
||||
|
||||
export type ListAuditLogsData = {
|
||||
body?: never;
|
||||
@@ -6151,8 +6074,7 @@ export type ListAuditLogsResponses = {
|
||||
200: AuditLogListResponse;
|
||||
};
|
||||
|
||||
export type ListAuditLogsResponse =
|
||||
ListAuditLogsResponses[keyof ListAuditLogsResponses];
|
||||
export type ListAuditLogsResponse = ListAuditLogsResponses[keyof ListAuditLogsResponses];
|
||||
|
||||
export type AuditLogStatsData = {
|
||||
body?: never;
|
||||
@@ -6201,5 +6123,4 @@ export type AuditLogStatsResponses = {
|
||||
200: AuditLogStatsResponse;
|
||||
};
|
||||
|
||||
export type AuditLogStatsResponse2 =
|
||||
AuditLogStatsResponses[keyof AuditLogStatsResponses];
|
||||
export type AuditLogStatsResponse2 = AuditLogStatsResponses[keyof AuditLogStatsResponses];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/tests/**/*.test.ts'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
testTimeout: 120000,
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/tests/**/*.test.ts"],
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
},
|
||||
testTimeout: 120000,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { defineConfig } from '@hey-api/openapi-ts';
|
||||
import { defineConfig } from "@hey-api/openapi-ts";
|
||||
|
||||
export default defineConfig({
|
||||
client: '@hey-api/client-fetch',
|
||||
input: '../../hindsight-docs/static/openapi.json',
|
||||
output: {
|
||||
path: './generated',
|
||||
format: 'prettier',
|
||||
},
|
||||
plugins: [
|
||||
'@hey-api/typescript',
|
||||
'@hey-api/sdk',
|
||||
],
|
||||
client: "@hey-api/client-fetch",
|
||||
input: "../../hindsight-docs/static/openapi.json",
|
||||
output: {
|
||||
path: "./generated",
|
||||
format: "prettier",
|
||||
},
|
||||
plugins: ["@hey-api/typescript", "@hey-api/sdk"],
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,12 +12,12 @@ import { beforeAll, beforeEach, afterAll, afterEach, describe, it } from "jsr:@s
|
||||
import { expect } from "jsr:@std/expect";
|
||||
|
||||
Object.assign(globalThis, {
|
||||
describe,
|
||||
test: it,
|
||||
it,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterAll,
|
||||
afterEach,
|
||||
expect,
|
||||
describe,
|
||||
test: it,
|
||||
it,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterAll,
|
||||
afterEach,
|
||||
expect,
|
||||
});
|
||||
|
||||
@@ -4,467 +4,468 @@
|
||||
* These tests require a running Hindsight API server.
|
||||
*/
|
||||
|
||||
import { HindsightClient } from '../src';
|
||||
import { HindsightClient } from "../src";
|
||||
|
||||
// Test configuration
|
||||
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
||||
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || "http://localhost:8888";
|
||||
|
||||
let client: HindsightClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = new HindsightClient({ baseUrl: HINDSIGHT_API_URL });
|
||||
client = new HindsightClient({ baseUrl: HINDSIGHT_API_URL });
|
||||
});
|
||||
|
||||
function randomBankId(): string {
|
||||
return `test_bank_${Math.random().toString(36).slice(2, 14)}`;
|
||||
return `test_bank_${Math.random().toString(36).slice(2, 14)}`;
|
||||
}
|
||||
|
||||
describe('TestRetain', () => {
|
||||
test('retain single memory', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retain(
|
||||
bankId,
|
||||
'Alice loves artificial intelligence and machine learning'
|
||||
);
|
||||
describe("TestRetain", () => {
|
||||
test("retain single memory", async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retain(
|
||||
bankId,
|
||||
"Alice loves artificial intelligence and machine learning"
|
||||
);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
});
|
||||
|
||||
test("retain memory with context", async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retain(bankId, "Bob went hiking in the mountains", {
|
||||
timestamp: new Date("2024-01-15T10:30:00"),
|
||||
context: "outdoor activities",
|
||||
});
|
||||
|
||||
test('retain memory with context', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retain(bankId, 'Bob went hiking in the mountains', {
|
||||
timestamp: new Date('2024-01-15T10:30:00'),
|
||||
context: 'outdoor activities',
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
});
|
||||
test("retain batch memories", async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retainBatch(bankId, [
|
||||
{ content: "Charlie enjoys reading science fiction books" },
|
||||
{ content: "Diana is learning to play the guitar", context: "hobbies" },
|
||||
{ content: "Eve completed a marathon last month", timestamp: "2024-10-15" },
|
||||
]);
|
||||
|
||||
test('retain batch memories', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retainBatch(bankId, [
|
||||
{ content: 'Charlie enjoys reading science fiction books' },
|
||||
{ content: 'Diana is learning to play the guitar', context: 'hobbies' },
|
||||
{ content: 'Eve completed a marathon last month', timestamp: '2024-10-15' },
|
||||
]);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.items_count).toBe(3);
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.items_count).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestRecall', () => {
|
||||
let bankId: string;
|
||||
describe("TestRecall", () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories before recall tests
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice loves programming in Python' },
|
||||
{ content: 'Bob enjoys hiking and outdoor adventures' },
|
||||
{ content: 'Charlie is interested in quantum physics' },
|
||||
{ content: 'Diana plays the violin beautifully' },
|
||||
]);
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories before recall tests
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: "Alice loves programming in Python" },
|
||||
{ content: "Bob enjoys hiking and outdoor adventures" },
|
||||
{ content: "Charlie is interested in quantum physics" },
|
||||
{ content: "Diana plays the violin beautifully" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("recall basic", async () => {
|
||||
const response = await client.recall(bankId, "What does Alice like?");
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that at least one result contains relevant information
|
||||
const resultTexts = response.results!.map((r) => r.text || "");
|
||||
const hasRelevant = resultTexts.some(
|
||||
(text: string) =>
|
||||
text.includes("Alice") || text.includes("Python") || text.includes("programming")
|
||||
);
|
||||
expect(hasRelevant).toBe(true);
|
||||
});
|
||||
|
||||
test("recall with max tokens", async () => {
|
||||
const response = await client.recall(bankId, "outdoor activities", {
|
||||
maxTokens: 1024,
|
||||
});
|
||||
|
||||
test('recall basic', async () => {
|
||||
const response = await client.recall(bankId, 'What does Alice like?');
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
expect(Array.isArray(response.results)).toBe(true);
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that at least one result contains relevant information
|
||||
const resultTexts = response.results!.map((r) => r.text || '');
|
||||
const hasRelevant = resultTexts.some(
|
||||
(text: string) => text.includes('Alice') || text.includes('Python') || text.includes('programming')
|
||||
);
|
||||
expect(hasRelevant).toBe(true);
|
||||
test("recall with types filter", async () => {
|
||||
const response = await client.recall(bankId, "What are people's hobbies?", {
|
||||
types: ["world"],
|
||||
maxTokens: 2048,
|
||||
trace: true,
|
||||
});
|
||||
|
||||
test('recall with max tokens', async () => {
|
||||
const response = await client.recall(bankId, 'outdoor activities', {
|
||||
maxTokens: 1024,
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
expect(Array.isArray(response.results)).toBe(true);
|
||||
});
|
||||
|
||||
test('recall with types filter', async () => {
|
||||
const response = await client.recall(bankId, "What are people's hobbies?", {
|
||||
types: ['world'],
|
||||
maxTokens: 2048,
|
||||
trace: true,
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestReflect', () => {
|
||||
let bankId: string;
|
||||
describe("TestReflect", () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Create bank and store test memories
|
||||
await client.createBank(bankId, {
|
||||
background: 'I am a helpful AI assistant interested in technology and science.',
|
||||
});
|
||||
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'The Python programming language is great for data science' },
|
||||
{ content: 'Machine learning models can recognize patterns in data' },
|
||||
{ content: 'Neural networks are inspired by biological neurons' },
|
||||
]);
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Create bank and store test memories
|
||||
await client.createBank(bankId, {
|
||||
background: "I am a helpful AI assistant interested in technology and science.",
|
||||
});
|
||||
|
||||
test('reflect basic', async () => {
|
||||
const response = await client.reflect(
|
||||
bankId,
|
||||
'What do you think about artificial intelligence?'
|
||||
);
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: "The Python programming language is great for data science" },
|
||||
{ content: "Machine learning models can recognize patterns in data" },
|
||||
{ content: "Neural networks are inspired by biological neurons" },
|
||||
]);
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.text).toBeDefined();
|
||||
expect(response.text!.length).toBeGreaterThan(0);
|
||||
test("reflect basic", async () => {
|
||||
const response = await client.reflect(
|
||||
bankId,
|
||||
"What do you think about artificial intelligence?"
|
||||
);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.text).toBeDefined();
|
||||
expect(response.text!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("reflect with context", async () => {
|
||||
const response = await client.reflect(bankId, "Should I learn Python?", {
|
||||
context: "I'm interested in starting a career in data science",
|
||||
budget: "low",
|
||||
});
|
||||
|
||||
test('reflect with context', async () => {
|
||||
const response = await client.reflect(bankId, 'Should I learn Python?', {
|
||||
context: "I'm interested in starting a career in data science",
|
||||
budget: 'low',
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.text).toBeDefined();
|
||||
expect(response.text!.length).toBeGreaterThan(0);
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.text).toBeDefined();
|
||||
expect(response.text!.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestListMemories', () => {
|
||||
let bankId: string;
|
||||
describe("TestListMemories", () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories synchronously
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice likes topic number 0' },
|
||||
{ content: 'Alice likes topic number 1' },
|
||||
{ content: 'Alice likes topic number 2' },
|
||||
{ content: 'Alice likes topic number 3' },
|
||||
{ content: 'Alice likes topic number 4' },
|
||||
]);
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories synchronously
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: "Alice likes topic number 0" },
|
||||
{ content: "Alice likes topic number 1" },
|
||||
{ content: "Alice likes topic number 2" },
|
||||
{ content: "Alice likes topic number 3" },
|
||||
{ content: "Alice likes topic number 4" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("list all memories", async () => {
|
||||
const response = await client.listMemories(bankId);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.total).toBeDefined();
|
||||
expect(response.items!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("list with pagination", async () => {
|
||||
const response = await client.listMemories(bankId, {
|
||||
limit: 2,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
test('list all memories', async () => {
|
||||
const response = await client.listMemories(bankId);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.total).toBeDefined();
|
||||
expect(response.items!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('list with pagination', async () => {
|
||||
const response = await client.listMemories(bankId, {
|
||||
limit: 2,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.items!.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.items).toBeDefined();
|
||||
expect(response.items!.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestEndToEndWorkflow', () => {
|
||||
test('complete workflow', async () => {
|
||||
const workflowBankId = randomBankId();
|
||||
describe("TestEndToEndWorkflow", () => {
|
||||
test("complete workflow", async () => {
|
||||
const workflowBankId = randomBankId();
|
||||
|
||||
// 1. Create bank
|
||||
await client.createBank(workflowBankId, {
|
||||
background: 'I am a software engineer who loves Python programming.',
|
||||
});
|
||||
|
||||
// 2. Store memories
|
||||
const retainResponse = await client.retainBatch(workflowBankId, [
|
||||
{ content: 'I completed a project using FastAPI' },
|
||||
{ content: 'I learned about async programming in Python' },
|
||||
{ content: 'I enjoy working on open source projects' },
|
||||
]);
|
||||
expect(retainResponse.success).toBe(true);
|
||||
|
||||
// 3. Search for relevant memories
|
||||
const recallResponse = await client.recall(
|
||||
workflowBankId,
|
||||
'What programming technologies do I use?'
|
||||
);
|
||||
expect(recallResponse.results!.length).toBeGreaterThan(0);
|
||||
|
||||
// 4. Generate contextual answer
|
||||
const reflectResponse = await client.reflect(
|
||||
workflowBankId,
|
||||
'What are my professional interests?'
|
||||
);
|
||||
expect(reflectResponse.text).toBeDefined();
|
||||
expect(reflectResponse.text!.length).toBeGreaterThan(0);
|
||||
// 1. Create bank
|
||||
await client.createBank(workflowBankId, {
|
||||
background: "I am a software engineer who loves Python programming.",
|
||||
});
|
||||
|
||||
// 2. Store memories
|
||||
const retainResponse = await client.retainBatch(workflowBankId, [
|
||||
{ content: "I completed a project using FastAPI" },
|
||||
{ content: "I learned about async programming in Python" },
|
||||
{ content: "I enjoy working on open source projects" },
|
||||
]);
|
||||
expect(retainResponse.success).toBe(true);
|
||||
|
||||
// 3. Search for relevant memories
|
||||
const recallResponse = await client.recall(
|
||||
workflowBankId,
|
||||
"What programming technologies do I use?"
|
||||
);
|
||||
expect(recallResponse.results!.length).toBeGreaterThan(0);
|
||||
|
||||
// 4. Generate contextual answer
|
||||
const reflectResponse = await client.reflect(
|
||||
workflowBankId,
|
||||
"What are my professional interests?"
|
||||
);
|
||||
expect(reflectResponse.text).toBeDefined();
|
||||
expect(reflectResponse.text!.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestBankProfile', () => {
|
||||
test('get bank profile', async () => {
|
||||
const bankId = randomBankId();
|
||||
describe("TestBankProfile", () => {
|
||||
test("get bank profile", async () => {
|
||||
const bankId = randomBankId();
|
||||
|
||||
// Create bank with background
|
||||
await client.createBank(bankId, {
|
||||
name: 'Test Agent',
|
||||
background: 'I am a helpful assistant for testing.',
|
||||
});
|
||||
|
||||
// Get bank profile
|
||||
const profile = await client.getBankProfile(bankId);
|
||||
|
||||
expect(profile).not.toBeNull();
|
||||
expect(profile.bank_id).toBe(bankId);
|
||||
expect(profile.name).toBe('Test Agent');
|
||||
expect(profile.background).toBe('I am a helpful assistant for testing.');
|
||||
// Create bank with background
|
||||
await client.createBank(bankId, {
|
||||
name: "Test Agent",
|
||||
background: "I am a helpful assistant for testing.",
|
||||
});
|
||||
|
||||
// Get bank profile
|
||||
const profile = await client.getBankProfile(bankId);
|
||||
|
||||
expect(profile).not.toBeNull();
|
||||
expect(profile.bank_id).toBe(bankId);
|
||||
expect(profile.name).toBe("Test Agent");
|
||||
expect(profile.background).toBe("I am a helpful assistant for testing.");
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestBankStats', () => {
|
||||
let bankId: string;
|
||||
describe("TestBankStats", () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice likes Python programming' },
|
||||
{ content: 'Bob enjoys hiking in the mountains' },
|
||||
]);
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: "Alice likes Python programming" },
|
||||
{ content: "Bob enjoys hiking in the mountains" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("get bank stats", async () => {
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
|
||||
const { data: stats } = await sdk.getAgentStats({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
test('get bank stats', async () => {
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
|
||||
const { data: stats } = await sdk.getAgentStats({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
expect(stats).not.toBeNull();
|
||||
expect(stats!.bank_id).toBe(bankId);
|
||||
expect(stats!.total_nodes).toBeGreaterThanOrEqual(0);
|
||||
expect(stats!.total_links).toBeGreaterThanOrEqual(0);
|
||||
expect(stats!.total_documents).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof stats!.nodes_by_fact_type).toBe('object');
|
||||
expect(typeof stats!.links_by_link_type).toBe('object');
|
||||
});
|
||||
expect(stats).not.toBeNull();
|
||||
expect(stats!.bank_id).toBe(bankId);
|
||||
expect(stats!.total_nodes).toBeGreaterThanOrEqual(0);
|
||||
expect(stats!.total_links).toBeGreaterThanOrEqual(0);
|
||||
expect(stats!.total_documents).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof stats!.nodes_by_fact_type).toBe("object");
|
||||
expect(typeof stats!.links_by_link_type).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestOperations', () => {
|
||||
test('list operations', async () => {
|
||||
const bankId = randomBankId();
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
describe("TestOperations", () => {
|
||||
test("list operations", async () => {
|
||||
const bankId = randomBankId();
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
|
||||
// First create an async operation
|
||||
await client.retain(bankId, 'Test content for async operation', {
|
||||
async: true,
|
||||
});
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: response } = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.bank_id).toBe(bankId);
|
||||
expect(Array.isArray(response!.operations)).toBe(true);
|
||||
// First create an async operation
|
||||
await client.retain(bankId, "Test content for async operation", {
|
||||
async: true,
|
||||
});
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: response } = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.bank_id).toBe(bankId);
|
||||
expect(Array.isArray(response!.operations)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestDocuments', () => {
|
||||
test('delete document', async () => {
|
||||
const bankId = randomBankId();
|
||||
const docId = `test-doc-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
describe("TestDocuments", () => {
|
||||
test("delete document", async () => {
|
||||
const bankId = randomBankId();
|
||||
const docId = `test-doc-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
|
||||
// First create a document
|
||||
const retainResponse = await client.retain(bankId, 'Test document content for deletion', {
|
||||
documentId: docId,
|
||||
});
|
||||
expect(retainResponse.success).toBe(true);
|
||||
// First create a document
|
||||
const retainResponse = await client.retain(bankId, "Test document content for deletion", {
|
||||
documentId: docId,
|
||||
});
|
||||
expect(retainResponse.success).toBe(true);
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: response } = await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId, document_id: docId },
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.success).toBe(true);
|
||||
expect(response!.document_id).toBe(docId);
|
||||
expect(response!.memory_units_deleted).toBeGreaterThanOrEqual(0);
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: response } = await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId, document_id: docId },
|
||||
});
|
||||
|
||||
test('get document', async () => {
|
||||
const bankId = randomBankId();
|
||||
const docId = `test-doc-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.success).toBe(true);
|
||||
expect(response!.document_id).toBe(docId);
|
||||
expect(response!.memory_units_deleted).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
// First create a document
|
||||
await client.retain(bankId, 'Test document content for retrieval', {
|
||||
documentId: docId,
|
||||
});
|
||||
test("get document", async () => {
|
||||
const bankId = randomBankId();
|
||||
const docId = `test-doc-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: document } = await sdk.getDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId, document_id: docId },
|
||||
});
|
||||
|
||||
expect(document).not.toBeNull();
|
||||
expect(document!.id).toBe(docId);
|
||||
expect(document!.original_text).toContain('Test document content');
|
||||
// First create a document
|
||||
await client.retain(bankId, "Test document content for retrieval", {
|
||||
documentId: docId,
|
||||
});
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: document } = await sdk.getDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId, document_id: docId },
|
||||
});
|
||||
|
||||
expect(document).not.toBeNull();
|
||||
expect(document!.id).toBe(docId);
|
||||
expect(document!.original_text).toContain("Test document content");
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestEntities', () => {
|
||||
let bankId: string;
|
||||
describe("TestEntities", () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Create memories that will generate entities
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice works at Google as a software engineer' },
|
||||
{ content: 'Bob is friends with Alice and works at Microsoft' },
|
||||
]);
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
// Create memories that will generate entities
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: "Alice works at Google as a software engineer" },
|
||||
{ content: "Bob is friends with Alice and works at Microsoft" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("list entities", async () => {
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
|
||||
const { data: response } = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
test('list entities', async () => {
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.items).toBeDefined();
|
||||
expect(Array.isArray(response!.items)).toBe(true);
|
||||
});
|
||||
|
||||
const { data: response } = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
test("get entity", async () => {
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.items).toBeDefined();
|
||||
expect(Array.isArray(response!.items)).toBe(true);
|
||||
// First list entities to get an ID
|
||||
const { data: listResponse } = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
test('get entity', async () => {
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
if (listResponse?.items && listResponse.items.length > 0) {
|
||||
const entityId = listResponse.items[0].id;
|
||||
|
||||
// First list entities to get an ID
|
||||
const { data: listResponse } = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
const { data: entity } = await sdk.getEntity({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId, entity_id: entityId },
|
||||
});
|
||||
|
||||
if (listResponse?.items && listResponse.items.length > 0) {
|
||||
const entityId = listResponse.items[0].id;
|
||||
|
||||
const { data: entity } = await sdk.getEntity({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId, entity_id: entityId },
|
||||
});
|
||||
|
||||
expect(entity).not.toBeNull();
|
||||
expect(entity!.id).toBe(entityId);
|
||||
}
|
||||
});
|
||||
expect(entity).not.toBeNull();
|
||||
expect(entity!.id).toBe(entityId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestDeleteBank', () => {
|
||||
test('delete bank', async () => {
|
||||
const bankId = randomBankId();
|
||||
const { sdk, createClient, createConfig } = await import('../src');
|
||||
describe("TestDeleteBank", () => {
|
||||
test("delete bank", async () => {
|
||||
const bankId = randomBankId();
|
||||
const { sdk, createClient, createConfig } = await import("../src");
|
||||
|
||||
// First create a bank with some data
|
||||
await client.createBank(bankId, {
|
||||
name: 'Bank to delete',
|
||||
background: 'This bank will be deleted',
|
||||
});
|
||||
await client.retain(bankId, 'Some memory to store');
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: response } = await sdk.deleteBank({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.success).toBe(true);
|
||||
|
||||
// Verify bank data is deleted - memories should be gone
|
||||
const memories = await client.listMemories(bankId);
|
||||
expect(memories.total).toBe(0);
|
||||
// First create a bank with some data
|
||||
await client.createBank(bankId, {
|
||||
name: "Bank to delete",
|
||||
background: "This bank will be deleted",
|
||||
});
|
||||
await client.retain(bankId, "Some memory to store");
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: HINDSIGHT_API_URL }));
|
||||
const { data: response } = await sdk.deleteBank({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId },
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response!.success).toBe(true);
|
||||
|
||||
// Verify bank data is deleted - memories should be gone
|
||||
const memories = await client.listMemories(bankId);
|
||||
expect(memories.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestRecallIncludeOptions', () => {
|
||||
let bankId: string;
|
||||
describe("TestRecallIncludeOptions", () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice works at Google as a software engineer' },
|
||||
{ content: 'Bob is a researcher at OpenAI' },
|
||||
]);
|
||||
beforeAll(async () => {
|
||||
bankId = randomBankId();
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: "Alice works at Google as a software engineer" },
|
||||
{ content: "Bob is a researcher at OpenAI" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("entities included by default", async () => {
|
||||
const response = await client.recall(bankId, "Where does Alice work?");
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
// entities should be present when includeEntities is not specified (default: true)
|
||||
expect(response.entities).toBeDefined();
|
||||
});
|
||||
|
||||
test("entities excluded when includeEntities is false", async () => {
|
||||
const response = await client.recall(bankId, "Where does Alice work?", {
|
||||
includeEntities: false,
|
||||
});
|
||||
|
||||
test('entities included by default', async () => {
|
||||
const response = await client.recall(bankId, 'Where does Alice work?');
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
// entities should be absent when explicitly disabled
|
||||
expect(response.entities).toBeFalsy();
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
// entities should be present when includeEntities is not specified (default: true)
|
||||
expect(response.entities).toBeDefined();
|
||||
test("entities included when includeEntities is true", async () => {
|
||||
const response = await client.recall(bankId, "Where does Alice work?", {
|
||||
includeEntities: true,
|
||||
});
|
||||
|
||||
test('entities excluded when includeEntities is false', async () => {
|
||||
const response = await client.recall(bankId, 'Where does Alice work?', {
|
||||
includeEntities: false,
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
// entities should be absent when explicitly disabled
|
||||
expect(response.entities).toBeFalsy();
|
||||
});
|
||||
|
||||
test('entities included when includeEntities is true', async () => {
|
||||
const response = await client.recall(bankId, 'Where does Alice work?', {
|
||||
includeEntities: true,
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
expect(response.entities).toBeDefined();
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
expect(response.entities).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestMission', () => {
|
||||
test('set mission', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.setMission(
|
||||
bankId,
|
||||
'Be a helpful PM tracking sprint progress and team capacity'
|
||||
);
|
||||
describe("TestMission", () => {
|
||||
test("set mission", async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.setMission(
|
||||
bankId,
|
||||
"Be a helpful PM tracking sprint progress and team capacity"
|
||||
);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.bank_id).toBe(bankId);
|
||||
expect(response.mission).toBe('Be a helpful PM tracking sprint progress and team capacity');
|
||||
});
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.bank_id).toBe(bankId);
|
||||
expect(response.mission).toBe("Be a helpful PM tracking sprint progress and team capacity");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,107 +2,103 @@
|
||||
* Tests for recallResponseToPromptString.
|
||||
*/
|
||||
|
||||
import { recallResponseToPromptString } from '../src';
|
||||
import { recallResponseToPromptString } from "../src";
|
||||
|
||||
describe('recallResponseToPromptString', () => {
|
||||
test('facts only', () => {
|
||||
const response = {
|
||||
results: [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Alice works at Google',
|
||||
context: 'work',
|
||||
occurred_start: '2024-01-15T10:00:00Z',
|
||||
occurred_end: '2024-06-15T10:00:00Z',
|
||||
mentioned_at: '2024-03-01T09:00:00Z',
|
||||
},
|
||||
{ id: '2', text: 'The sky is blue' },
|
||||
],
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
expect(prompt.startsWith('FACTS:\n')).toBe(true);
|
||||
const facts = JSON.parse(prompt.slice('FACTS:\n'.length));
|
||||
expect(facts).toEqual([
|
||||
{
|
||||
text: 'Alice works at Google',
|
||||
context: 'work',
|
||||
occurred_start: '2024-01-15T10:00:00Z',
|
||||
occurred_end: '2024-06-15T10:00:00Z',
|
||||
mentioned_at: '2024-03-01T09:00:00Z',
|
||||
},
|
||||
{ text: 'The sky is blue' },
|
||||
]);
|
||||
});
|
||||
describe("recallResponseToPromptString", () => {
|
||||
test("facts only", () => {
|
||||
const response = {
|
||||
results: [
|
||||
{
|
||||
id: "1",
|
||||
text: "Alice works at Google",
|
||||
context: "work",
|
||||
occurred_start: "2024-01-15T10:00:00Z",
|
||||
occurred_end: "2024-06-15T10:00:00Z",
|
||||
mentioned_at: "2024-03-01T09:00:00Z",
|
||||
},
|
||||
{ id: "2", text: "The sky is blue" },
|
||||
],
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
expect(prompt.startsWith("FACTS:\n")).toBe(true);
|
||||
const facts = JSON.parse(prompt.slice("FACTS:\n".length));
|
||||
expect(facts).toEqual([
|
||||
{
|
||||
text: "Alice works at Google",
|
||||
context: "work",
|
||||
occurred_start: "2024-01-15T10:00:00Z",
|
||||
occurred_end: "2024-06-15T10:00:00Z",
|
||||
mentioned_at: "2024-03-01T09:00:00Z",
|
||||
},
|
||||
{ text: "The sky is blue" },
|
||||
]);
|
||||
});
|
||||
|
||||
test('with chunks', () => {
|
||||
const response = {
|
||||
results: [
|
||||
{ id: '1', text: 'Alice works at Google', chunk_id: 'chunk_1' },
|
||||
],
|
||||
chunks: {
|
||||
chunk_1: { id: 'chunk_1', text: 'Alice works at Google on the AI team since 2020.', chunk_index: 0 },
|
||||
},
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
const facts = JSON.parse(prompt.slice('FACTS:\n'.length));
|
||||
expect(facts[0].source_chunk).toBe('Alice works at Google on the AI team since 2020.');
|
||||
});
|
||||
test("with chunks", () => {
|
||||
const response = {
|
||||
results: [{ id: "1", text: "Alice works at Google", chunk_id: "chunk_1" }],
|
||||
chunks: {
|
||||
chunk_1: {
|
||||
id: "chunk_1",
|
||||
text: "Alice works at Google on the AI team since 2020.",
|
||||
chunk_index: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
const facts = JSON.parse(prompt.slice("FACTS:\n".length));
|
||||
expect(facts[0].source_chunk).toBe("Alice works at Google on the AI team since 2020.");
|
||||
});
|
||||
|
||||
test('with entities', () => {
|
||||
const response = {
|
||||
results: [
|
||||
{ id: '1', text: 'Alice works at Google' },
|
||||
],
|
||||
entities: {
|
||||
Alice: {
|
||||
entity_id: 'e1',
|
||||
canonical_name: 'Alice',
|
||||
observations: [{ text: 'Alice is a senior engineer at Google working on AI.' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
expect(prompt).toContain('ENTITIES:');
|
||||
expect(prompt).toContain('## Alice');
|
||||
expect(prompt).toContain('Alice is a senior engineer at Google working on AI.');
|
||||
});
|
||||
test("with entities", () => {
|
||||
const response = {
|
||||
results: [{ id: "1", text: "Alice works at Google" }],
|
||||
entities: {
|
||||
Alice: {
|
||||
entity_id: "e1",
|
||||
canonical_name: "Alice",
|
||||
observations: [{ text: "Alice is a senior engineer at Google working on AI." }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
expect(prompt).toContain("ENTITIES:");
|
||||
expect(prompt).toContain("## Alice");
|
||||
expect(prompt).toContain("Alice is a senior engineer at Google working on AI.");
|
||||
});
|
||||
|
||||
test('with chunks and entities', () => {
|
||||
const response = {
|
||||
results: [
|
||||
{ id: '1', text: 'Alice works at Google', chunk_id: 'c1' },
|
||||
],
|
||||
chunks: {
|
||||
c1: { id: 'c1', text: 'Full conversation about Alice at Google.', chunk_index: 0 },
|
||||
},
|
||||
entities: {
|
||||
Alice: {
|
||||
entity_id: 'e1',
|
||||
canonical_name: 'Alice',
|
||||
observations: [{ text: 'Alice is a senior engineer.' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
const factsSection = prompt.split('ENTITIES:')[0].trim();
|
||||
const facts = JSON.parse(factsSection.slice('FACTS:\n'.length));
|
||||
expect(facts[0].source_chunk).toBe('Full conversation about Alice at Google.');
|
||||
expect(prompt).toContain('## Alice\nAlice is a senior engineer.');
|
||||
});
|
||||
test("with chunks and entities", () => {
|
||||
const response = {
|
||||
results: [{ id: "1", text: "Alice works at Google", chunk_id: "c1" }],
|
||||
chunks: {
|
||||
c1: { id: "c1", text: "Full conversation about Alice at Google.", chunk_index: 0 },
|
||||
},
|
||||
entities: {
|
||||
Alice: {
|
||||
entity_id: "e1",
|
||||
canonical_name: "Alice",
|
||||
observations: [{ text: "Alice is a senior engineer." }],
|
||||
},
|
||||
},
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
const factsSection = prompt.split("ENTITIES:")[0].trim();
|
||||
const facts = JSON.parse(factsSection.slice("FACTS:\n".length));
|
||||
expect(facts[0].source_chunk).toBe("Full conversation about Alice at Google.");
|
||||
expect(prompt).toContain("## Alice\nAlice is a senior engineer.");
|
||||
});
|
||||
|
||||
test('empty results', () => {
|
||||
const response = { results: [] };
|
||||
expect(recallResponseToPromptString(response)).toBe('FACTS:\n[]');
|
||||
});
|
||||
test("empty results", () => {
|
||||
const response = { results: [] };
|
||||
expect(recallResponseToPromptString(response)).toBe("FACTS:\n[]");
|
||||
});
|
||||
|
||||
test('chunk_id not in chunks is ignored', () => {
|
||||
const response = {
|
||||
results: [
|
||||
{ id: '1', text: 'Some fact', chunk_id: 'missing_chunk' },
|
||||
],
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
const facts = JSON.parse(prompt.slice('FACTS:\n'.length));
|
||||
expect(facts[0]).not.toHaveProperty('source_chunk');
|
||||
});
|
||||
test("chunk_id not in chunks is ignored", () => {
|
||||
const response = {
|
||||
results: [{ id: "1", text: "Some fact", chunk_id: "missing_chunk" }],
|
||||
};
|
||||
const prompt = recallResponseToPromptString(response);
|
||||
const facts = JSON.parse(prompt.slice("FACTS:\n".length));
|
||||
expect(facts[0]).not.toHaveProperty("source_chunk");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"generated/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
"include": ["src/**/*", "generated/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
outDir: 'dist',
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
// Bundle all relative imports (src + generated) into the output
|
||||
bundle: true,
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
outDir: "dist",
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
// Bundle all relative imports (src + generated) into the output
|
||||
bundle: true,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { dataplaneBankUrl, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
|
||||
try {
|
||||
const { bankId } = await params;
|
||||
if (!bankId) {
|
||||
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const q = searchParams.get("q");
|
||||
const source = searchParams.get("source");
|
||||
const limit = searchParams.get("limit");
|
||||
const offset = searchParams.get("offset");
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
if (q) queryParams.append("q", q);
|
||||
if (source) queryParams.append("source", source);
|
||||
if (limit) queryParams.append("limit", limit);
|
||||
if (offset) queryParams.append("offset", offset);
|
||||
|
||||
const url = dataplaneBankUrl(bankId, `/tags${queryParams.toString() ? `?${queryParams}` : ""}`);
|
||||
const response = await fetch(url, { method: "GET", headers: getDataplaneHeaders() });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("API error listing tags:", errorText);
|
||||
return NextResponse.json({ error: "Failed to list tags" }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Error listing tags:", error);
|
||||
return NextResponse.json({ error: "Failed to list tags" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
Network,
|
||||
List,
|
||||
Search,
|
||||
Tag,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
@@ -47,6 +45,7 @@ import { MemoryDetailPanel } from "./memory-detail-panel";
|
||||
import { MemoryDetailModal } from "./memory-detail-modal";
|
||||
import { Graph2D, convertHindsightGraphData, GraphNode } from "./graph-2d";
|
||||
import { Constellation } from "./constellation";
|
||||
import { TagFilterInput } from "./tag-filter-input";
|
||||
import { ScatterChart, Plus, FileText } from "lucide-react";
|
||||
|
||||
type FactType = "world" | "experience" | "observation";
|
||||
@@ -74,7 +73,6 @@ export function DataView({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [tagFilters, setTagFilters] = useState<string[]>([]);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||
const [modalMemoryId, setModalMemoryId] = useState<string | null>(null);
|
||||
@@ -160,18 +158,6 @@ export function DataView({
|
||||
}
|
||||
};
|
||||
|
||||
const addTagFilter = (tag: string) => {
|
||||
const trimmed = tag.trim();
|
||||
if (trimmed && !tagFilters.includes(trimmed)) {
|
||||
setTagFilters((prev) => [...prev, trimmed]);
|
||||
}
|
||||
setTagInput("");
|
||||
};
|
||||
|
||||
const removeTagFilter = (tag: string) => {
|
||||
setTagFilters((prev) => prev.filter((t) => t !== tag));
|
||||
};
|
||||
|
||||
// Table rows are already filtered server-side
|
||||
const filteredTableRows = useMemo(() => {
|
||||
return data?.table_rows ?? [];
|
||||
@@ -418,46 +404,8 @@ export function DataView({
|
||||
/>
|
||||
</div>
|
||||
{/* Tag input */}
|
||||
<div className="relative max-w-xs flex-1">
|
||||
<Tag className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
addTagFilter(tagInput);
|
||||
} else if (e.key === "Backspace" && !tagInput && tagFilters.length > 0) {
|
||||
removeTagFilter(tagFilters[tagFilters.length - 1]);
|
||||
}
|
||||
}}
|
||||
placeholder="Filter by tag…"
|
||||
className="pl-8 h-9"
|
||||
/>
|
||||
</div>
|
||||
<TagFilterInput value={tagFilters} onChange={setTagFilters} bankId={currentBank} />
|
||||
</div>
|
||||
{/* Active tag chips */}
|
||||
{tagFilters.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{tagFilters.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-md bg-primary/10 text-primary border border-primary/20 font-medium leading-none"
|
||||
>
|
||||
<span className="opacity-50 select-none font-mono">#</span>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => removeTagFilter(tag)}
|
||||
className="opacity-50 hover:opacity-100 transition-opacity ml-0.5"
|
||||
aria-label={`Remove tag ${tag}`}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -38,14 +38,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Plus,
|
||||
Sparkles,
|
||||
@@ -57,9 +49,10 @@ import {
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
LayoutGrid,
|
||||
List,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
FolderOpen,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -69,6 +62,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MentalModelDetailModal } from "./mental-model-detail-modal";
|
||||
import { TagFilterInput } from "./tag-filter-input";
|
||||
|
||||
interface ReflectResponseBasedOnFact {
|
||||
id: string;
|
||||
@@ -107,19 +101,22 @@ interface MentalModel {
|
||||
reflect_response?: ReflectResponse;
|
||||
}
|
||||
|
||||
type ViewMode = "dashboard" | "table";
|
||||
type ViewMode = "dashboard" | "files";
|
||||
|
||||
export function MentalModelsView() {
|
||||
const { currentBank } = useBank();
|
||||
const [mentalModels, setMentalModels] = useState<MentalModel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("dashboard");
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("files");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 100;
|
||||
|
||||
const [showCreateMentalModel, setShowCreateMentalModel] = useState(false);
|
||||
const [selectedMentalModel, setSelectedMentalModel] = useState<MentalModel | null>(null);
|
||||
const [filesSelectedId, setFilesSelectedId] = useState<string | null>(null);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [tagsMatch, setTagsMatch] = useState<"any" | "all">("any");
|
||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||
const [mentalModelToUpdate, setMentalModelToUpdate] = useState<MentalModel | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<{
|
||||
@@ -128,7 +125,7 @@ export function MentalModelsView() {
|
||||
} | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// Filter mental models based on search query
|
||||
// Tag filtering happens server-side; only the search text is applied locally.
|
||||
const filteredMentalModels = mentalModels.filter((m) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
@@ -145,7 +142,11 @@ export function MentalModelsView() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const mentalModelsData = await client.listMentalModels(currentBank);
|
||||
const mentalModelsData = await client.listMentalModels(
|
||||
currentBank,
|
||||
selectedTags.length > 0 ? selectedTags : undefined,
|
||||
selectedTags.length > 0 ? tagsMatch : undefined
|
||||
);
|
||||
setMentalModels(mentalModelsData.items || []);
|
||||
} catch (error) {
|
||||
console.error("Error loading mental models:", error);
|
||||
@@ -205,7 +206,7 @@ export function MentalModelsView() {
|
||||
if (currentBank) {
|
||||
loadData();
|
||||
}
|
||||
}, [currentBank]);
|
||||
}, [currentBank, selectedTags, tagsMatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -217,10 +218,10 @@ export function MentalModelsView() {
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
// Reset to first page when search query changes
|
||||
// Reset to first page when search query or tag filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery]);
|
||||
}, [searchQuery, selectedTags, tagsMatch]);
|
||||
|
||||
if (!currentBank) {
|
||||
return (
|
||||
@@ -247,29 +248,53 @@ export function MentalModelsView() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Search filter */}
|
||||
<div className="mb-4">
|
||||
{/* Search + tag filter (single row) */}
|
||||
<div className="mb-4 flex items-center gap-3 flex-wrap">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Filter mental models by name, query, or content..."
|
||||
className="max-w-md"
|
||||
className="w-80 h-9"
|
||||
/>
|
||||
<TagFilterInput
|
||||
value={selectedTags}
|
||||
onChange={setSelectedTags}
|
||||
fetchSuggestions={async (q) => {
|
||||
if (!currentBank) return [];
|
||||
const pattern = q ? `${q}*` : undefined;
|
||||
const res = await client.listTags(currentBank, pattern, 20, "mental_models");
|
||||
return res.items.map((i) => i.tag);
|
||||
}}
|
||||
matchMode={tagsMatch}
|
||||
onMatchModeChange={setTagsMatch}
|
||||
className="flex-1 min-w-[260px]"
|
||||
/>
|
||||
<Button onClick={() => setShowCreateMentalModel(true)} size="sm">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Mental Model
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{searchQuery
|
||||
{searchQuery || selectedTags.length > 0
|
||||
? `${filteredMentalModels.length} of ${mentalModels.length} mental models`
|
||||
: `${mentalModels.length} mental model${mentalModels.length !== 1 ? "s" : ""}`}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={() => setShowCreateMentalModel(true)} size="sm">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Mental Model
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode("files")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
viewMode === "files"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
List
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("dashboard")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
@@ -281,17 +306,6 @@ export function MentalModelsView() {
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("table")}
|
||||
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-1.5 ${
|
||||
viewMode === "table"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -388,70 +402,25 @@ export function MentalModelsView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table View */}
|
||||
{viewMode === "table" && (
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[20%]">ID</TableHead>
|
||||
<TableHead className="w-[20%]">Name</TableHead>
|
||||
<TableHead className="w-[35%]">Source Query</TableHead>
|
||||
<TableHead className="w-[15%]">Last Refreshed</TableHead>
|
||||
<TableHead className="w-[10%]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedMentalModels.map((m) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={m.id}
|
||||
className={`cursor-pointer hover:bg-muted/50 ${
|
||||
selectedMentalModel?.id === m.id ? "bg-primary/10" : ""
|
||||
}`}
|
||||
onClick={() => setSelectedMentalModel(m)}
|
||||
>
|
||||
<TableCell className="py-2">
|
||||
<code className="text-xs font-mono text-muted-foreground truncate block">
|
||||
{m.id}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="font-medium text-foreground">{m.name}</div>
|
||||
</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<div className="text-sm text-muted-foreground truncate">
|
||||
{m.source_query}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="py-2 text-sm text-foreground"
|
||||
title={formatAbsoluteDateTime(m.last_refreshed_at)}
|
||||
>
|
||||
{formatRelativeTime(m.last_refreshed_at)}
|
||||
</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<RowActionsMenu
|
||||
m={m}
|
||||
refreshing={refreshingIds.has(m.id)}
|
||||
onEdit={(target) => {
|
||||
setMentalModelToUpdate(target);
|
||||
setShowUpdateDialog(true);
|
||||
}}
|
||||
onRefresh={handleRowRefresh}
|
||||
onDelete={(target) =>
|
||||
setDeleteTarget({ id: target.id, name: target.name })
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/* Files View */}
|
||||
{viewMode === "files" && (
|
||||
<FilesView
|
||||
mentalModels={filteredMentalModels}
|
||||
selectedId={filesSelectedId}
|
||||
onSelect={setFilesSelectedId}
|
||||
onOpenDetail={setSelectedMentalModel}
|
||||
refreshingIds={refreshingIds}
|
||||
onEdit={(target) => {
|
||||
setMentalModelToUpdate(target);
|
||||
setShowUpdateDialog(true);
|
||||
}}
|
||||
onRefresh={handleRowRefresh}
|
||||
onDelete={(target) => setDeleteTarget({ id: target.id, name: target.name })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
{viewMode !== "files" && totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{startIndex + 1}-{Math.min(endIndex, filteredMentalModels.length)} of{" "}
|
||||
@@ -1447,3 +1416,138 @@ function UpdateMentalModelDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function FilesView({
|
||||
mentalModels,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onOpenDetail,
|
||||
refreshingIds,
|
||||
onEdit,
|
||||
onRefresh,
|
||||
onDelete,
|
||||
}: {
|
||||
mentalModels: MentalModel[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onOpenDetail: (m: MentalModel) => void;
|
||||
refreshingIds: Set<string>;
|
||||
onEdit: (m: MentalModel) => void;
|
||||
onRefresh: (m: MentalModel) => void;
|
||||
onDelete: (m: MentalModel) => void;
|
||||
}) {
|
||||
const effectiveId =
|
||||
selectedId && mentalModels.some((m) => m.id === selectedId)
|
||||
? selectedId
|
||||
: (mentalModels[0]?.id ?? null);
|
||||
const selected = mentalModels.find((m) => m.id === effectiveId) ?? null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-[320px_1fr] gap-0 overflow-hidden min-h-[600px]">
|
||||
<aside className="border-r border-border bg-muted/30 overflow-y-auto max-h-[calc(100vh-260px)]">
|
||||
<ul className="py-1">
|
||||
{mentalModels.map((m) => {
|
||||
const isActive = m.id === effectiveId;
|
||||
return (
|
||||
<li key={m.id}>
|
||||
<button
|
||||
onClick={() => onSelect(m.id)}
|
||||
className={`w-full flex items-start gap-2 px-3 py-2 text-left transition-colors border-l-2 ${
|
||||
isActive
|
||||
? "bg-primary/10 border-primary text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
}`}
|
||||
title={`${m.name}\n${m.source_query}`}
|
||||
>
|
||||
<FileText className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`truncate text-sm font-medium ${isActive ? "text-foreground" : ""}`}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
<span
|
||||
className="ml-auto text-[10px] text-muted-foreground flex-shrink-0"
|
||||
title={formatAbsoluteDateTime(m.last_refreshed_at)}
|
||||
>
|
||||
{formatRelativeTime(m.last_refreshed_at)}
|
||||
</span>
|
||||
</div>
|
||||
{m.source_query && (
|
||||
<div className="text-xs text-muted-foreground/80 truncate italic mt-0.5">
|
||||
{m.source_query}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section className="overflow-y-auto max-h-[calc(100vh-260px)]">
|
||||
{selected ? (
|
||||
<article className="p-6">
|
||||
<header className="mb-4 pb-4 border-b border-border">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-xl font-semibold text-foreground">{selected.name}</h2>
|
||||
{selected.source_query && (
|
||||
<p className="text-sm text-muted-foreground mt-1 italic">
|
||||
“{selected.source_query}”
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
|
||||
<span title={formatAbsoluteDateTime(selected.last_refreshed_at)}>
|
||||
Refreshed {formatRelativeTime(selected.last_refreshed_at)}
|
||||
</span>
|
||||
{selected.tags.length > 0 && (
|
||||
<div className="flex gap-1">
|
||||
{selected.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-1.5 py-0.5 rounded text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenDetail(selected)}>
|
||||
Open
|
||||
</Button>
|
||||
<RowActionsMenu
|
||||
m={selected}
|
||||
refreshing={refreshingIds.has(selected.id)}
|
||||
onEdit={onEdit}
|
||||
onRefresh={onRefresh}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{selected.content ? (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<CompactMarkdown>{selected.content}</CompactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No content yet. Refresh this mental model to generate content.
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
) : (
|
||||
<div className="p-10 text-center text-muted-foreground">
|
||||
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">Select a mental model to view its content.</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tag, X } from "lucide-react";
|
||||
import { client } from "@/lib/api";
|
||||
|
||||
type FetchSuggestions = (q: string) => Promise<string[]>;
|
||||
|
||||
interface TagFilterInputProps {
|
||||
value: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
bankId?: string | null;
|
||||
fetchSuggestions?: FetchSuggestions;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
matchMode?: "any" | "all";
|
||||
onMatchModeChange?: (mode: "any" | "all") => void;
|
||||
showMatchToggleAt?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SHOW_MATCH_TOGGLE_AT = 2;
|
||||
|
||||
export function TagFilterInput({
|
||||
value,
|
||||
onChange,
|
||||
bankId,
|
||||
fetchSuggestions,
|
||||
placeholder = "Filter by tag…",
|
||||
className,
|
||||
matchMode,
|
||||
onMatchModeChange,
|
||||
showMatchToggleAt = DEFAULT_SHOW_MATCH_TOGGLE_AT,
|
||||
}: TagFilterInputProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState(-1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Default suggestion source uses the memory-units `list_tags` endpoint when a
|
||||
// bankId is given. Memoize so the effect below doesn't refire on every render.
|
||||
const defaultFetcher = useMemo<FetchSuggestions | undefined>(() => {
|
||||
if (!bankId) return undefined;
|
||||
return async (q: string) => {
|
||||
const pattern = q ? `${q}*` : undefined;
|
||||
const res = await client.listTags(bankId, pattern, 20);
|
||||
return res.items.map((i) => i.tag);
|
||||
};
|
||||
}, [bankId]);
|
||||
|
||||
// Caller-supplied fetchSuggestions is typically defined inline (new identity per
|
||||
// render), which would refire the debounce effect after each fetch and create an
|
||||
// infinite suggestion-fetch loop. Hold it via a ref so the effect's dep list
|
||||
// only tracks input/value — the latest closure is used at fire time.
|
||||
const fetcherRef = useRef<FetchSuggestions | undefined>(undefined);
|
||||
fetcherRef.current = fetchSuggestions ?? defaultFetcher;
|
||||
|
||||
// Debounced fetch of suggestions when typing
|
||||
useEffect(() => {
|
||||
const fetcher = fetcherRef.current;
|
||||
if (!fetcher) return;
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
const results = await fetcher(input.trim());
|
||||
if (cancelled) return;
|
||||
const filtered = results.filter((t) => !value.includes(t));
|
||||
setSuggestions(filtered);
|
||||
setActiveIndex(filtered.length > 0 ? 0 : -1);
|
||||
} catch {
|
||||
if (!cancelled) setSuggestions([]);
|
||||
}
|
||||
}, 150);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [input, value]);
|
||||
|
||||
// Close suggestions on outside click
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
if (!containerRef.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, []);
|
||||
|
||||
const addTag = (tag: string) => {
|
||||
const trimmed = tag.trim();
|
||||
if (!trimmed || value.includes(trimmed)) {
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
onChange([...value, trimmed]);
|
||||
setInput("");
|
||||
setActiveIndex(-1);
|
||||
};
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
onChange(value.filter((t) => t !== tag));
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "ArrowDown" && suggestions.length > 0) {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
setActiveIndex((i) => (i + 1) % suggestions.length);
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowUp" && suggestions.length > 0) {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
setActiveIndex((i) => (i <= 0 ? suggestions.length - 1 : i - 1));
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
if (open && activeIndex >= 0 && suggestions[activeIndex]) {
|
||||
addTag(suggestions[activeIndex]);
|
||||
} else if (input.trim()) {
|
||||
addTag(input);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Backspace" && !input && value.length > 0) {
|
||||
removeTag(value[value.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const showMatchToggle =
|
||||
matchMode != null && onMatchModeChange != null && value.length >= showMatchToggleAt;
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 flex-wrap ${className ?? ""}`}>
|
||||
<div ref={containerRef} className="relative w-56">
|
||||
<Tag className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className="pl-8 h-9"
|
||||
/>
|
||||
{open && suggestions.length > 0 && (
|
||||
<div className="absolute z-20 mt-1 w-full bg-popover border border-border rounded-md shadow-md max-h-60 overflow-y-auto">
|
||||
{suggestions.map((tag, idx) => (
|
||||
<button
|
||||
key={tag}
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
addTag(tag);
|
||||
}}
|
||||
onMouseEnter={() => setActiveIndex(idx)}
|
||||
className={`w-full text-left px-3 py-1.5 text-sm flex items-center gap-2 ${
|
||||
idx === activeIndex
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<Tag className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="truncate">{tag}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{value.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{value.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-md bg-primary/10 text-primary border border-primary/20 font-medium leading-none"
|
||||
>
|
||||
<span className="opacity-50 select-none font-mono">#</span>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="opacity-50 hover:opacity-100 transition-opacity ml-0.5"
|
||||
aria-label={`Remove tag ${tag}`}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange([])}
|
||||
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showMatchToggle && (
|
||||
<div className="flex items-center gap-1 bg-muted rounded-md p-0.5 h-9 ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMatchModeChange!("any")}
|
||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
matchMode === "any"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
title="Match any selected tag"
|
||||
>
|
||||
any
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onMatchModeChange!("all")}
|
||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
matchMode === "all"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
title="Match all selected tags"
|
||||
>
|
||||
all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -981,6 +981,31 @@ export class ControlPlaneClient {
|
||||
}>(bankApi(bankId, `/observations/${encodeURIComponent(observationId)}`));
|
||||
}
|
||||
|
||||
// ============= TAGS =============
|
||||
|
||||
/**
|
||||
* List unique tags in a bank with usage counts. Supports wildcard '*' in q.
|
||||
* Pass `source: "mental_models"` to read tags from mental_models instead of memory_units.
|
||||
*/
|
||||
async listTags(
|
||||
bankId: string,
|
||||
q?: string,
|
||||
limit?: number,
|
||||
source?: "memories" | "mental_models"
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.append("q", q);
|
||||
if (limit != null) params.append("limit", String(limit));
|
||||
if (source) params.append("source", source);
|
||||
const query = params.toString();
|
||||
return this.fetchApi<{
|
||||
items: Array<{ tag: string; count: number }>;
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}>(bankApi(bankId, `/tags${query ? `?${query}` : ""}`));
|
||||
}
|
||||
|
||||
// ============= MENTAL MODELS (stored reflect responses) =============
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,6 +47,7 @@ VALID_INTEGRATIONS = [
|
||||
"opencode",
|
||||
"cloudflare-oauth-proxy",
|
||||
"openai-agents",
|
||||
"pipecat",
|
||||
]
|
||||
|
||||
|
||||
@@ -630,6 +631,7 @@ def _get_package_name(integration: str) -> str:
|
||||
"opencode": "@vectorize-io/opencode-hindsight",
|
||||
"cloudflare-oauth-proxy": "hindsight-cloudflare-oauth-proxy",
|
||||
"openai-agents": "hindsight-openai-agents",
|
||||
"pipecat": "hindsight-pipecat",
|
||||
}
|
||||
return packages[integration]
|
||||
|
||||
@@ -659,6 +661,7 @@ def _integration_display_name(integration: str) -> str:
|
||||
"autogen": "AutoGen",
|
||||
"paperclip": "Paperclip",
|
||||
"opencode": "OpenCode",
|
||||
"pipecat": "Pipecat",
|
||||
}
|
||||
return names.get(integration, integration)
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ Here's the lifecycle of a session with the plugin active:
|
||||
4. **Session idles** — the `session.idle` event triggers auto-retain of the conversation transcript
|
||||
5. **Context compaction** — if the context window fills up, the plugin retains the current conversation and injects recalled memories into the compaction context, so nothing important is lost
|
||||
|
||||
The auto-retain uses a sliding window controlled by `retainEveryNTurns` (default: 10). This prevents redundant storage while keeping recent context fresh.
|
||||
The auto-retain uses a sliding window controlled by `retainEveryNTurns` (default: 3). This prevents redundant storage while keeping recent context fresh.
|
||||
|
||||
---
|
||||
|
||||
@@ -167,7 +167,7 @@ You can configure the plugin at three levels (later wins):
|
||||
| `autoRecall` | `true` | Inject memories on session start |
|
||||
| `autoRetain` | `true` | Capture conversation on idle |
|
||||
| `recallBudget` | `mid` | How many memories to retrieve: `low`, `mid`, `high` |
|
||||
| `retainEveryNTurns` | `10` | Auto-retain frequency (user turns) |
|
||||
| `retainEveryNTurns` | `3` | Auto-retain frequency (user turns) |
|
||||
| `retainMode` | `full-session` | `full-session` upserts the whole conversation; `last-turn` creates chunked windows |
|
||||
| `bankMission` | *(none)* | Guides what Hindsight extracts and how it reflects |
|
||||
| `dynamicBankId` | `false` | Derive bank ID from project/agent context |
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
---
|
||||
title: "Connect ChatGPT and Perplexity to Hindsight for Long-Term Memory"
|
||||
authors: [benfrank241]
|
||||
date: 2026-04-24
|
||||
tags: [hindsight-cloud, mcp, oauth, chatgpt, perplexity, memory, connectors]
|
||||
description: "Use Hindsight's MCP integration to add persistent memory to ChatGPT and Perplexity. Store conversations, knowledge, and insights, then recall them across future sessions with OAuth-secured connections."
|
||||
image: /img/blog/openai-perplexity-mcp-memory.png
|
||||
---
|
||||
|
||||
# Connect ChatGPT and Perplexity to Hindsight for Long-Term Memory
|
||||
|
||||

|
||||
|
||||
[ChatGPT](https://chatgpt.com) and [Perplexity](https://www.perplexity.ai) are powerful AI tools, but conversation history doesn't persist across separate chats. ChatGPT has built-in memory for personal preferences, but knowledge from specific conversations (research, decisions, code) is lost when you start a new thread. [Hindsight](https://ui.hindsight.vectorize.io/signup) adds persistent, searchable memory that carries context forward through the Model Context Protocol (MCP).
|
||||
|
||||
This guide walks you through connecting ChatGPT and Perplexity to Hindsight for persistent memory across sessions. You'll learn how to set up OAuth-secured connections, store knowledge from your conversations, and automatically recall it in future sessions, all with a no-code setup.
|
||||
|
||||
<!-- truncate -->
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **Two-click memory setup** — add Hindsight to ChatGPT or Perplexity, approve OAuth, done
|
||||
- **Persistent across sessions** — knowledge you build in one conversation lives in the next
|
||||
- **Search-powered recall** — Hindsight automatically retrieves relevant memories when you need them
|
||||
- **End-to-end encrypted** — your data stays in your Hindsight Cloud account
|
||||
- **No API keys** — OAuth handles authentication automatically
|
||||
|
||||
## Why Connect ChatGPT and Perplexity to Hindsight for Memory?
|
||||
|
||||
Both ChatGPT and Perplexity excel at answering questions, brainstorming, and reasoning. But they operate in isolation:
|
||||
|
||||
- **Context resets**, each new chat loses the context from previous conversations
|
||||
- **Repeated explanations**, you re-explain your goals, preferences, or domain knowledge
|
||||
- **No learning over time**, the AI doesn't improve its understanding of you or your projects
|
||||
- **Redundant research**, you re-discover facts, sources, and context you've already explored
|
||||
|
||||
Hindsight solves this with persistent memory. It's a semantic memory system that learns what matters to you: your preferences, projects, knowledge, and past discoveries.
|
||||
|
||||
When you ask ChatGPT or Perplexity a question, Hindsight retrieves related memories and includes them in the conversation context. The AI uses these memories to give more informed, personalized answers. Over time, both tools become **better informed, more personalized, and more effective** because they're building on what they've learned about you.
|
||||
|
||||
## How It Works
|
||||
|
||||
Hindsight uses MCP (Model Context Protocol), an open standard that lets AI tools access external services like memory banks.
|
||||
|
||||
When you connect Hindsight to ChatGPT or Perplexity, three things happen:
|
||||
|
||||
1. **Retain**, You ask Hindsight to store knowledge from your conversations (facts, discoveries, decisions, context)
|
||||
2. **Recall**, When you ask a new question, Hindsight searches your memory bank and retrieves related context
|
||||
3. **Reflect**, ChatGPT or Perplexity uses those memories to give more informed, personalized answers
|
||||
|
||||
The LLM decides whether to use Hindsight's tools based on your prompts. You can optimize this by mentioning Hindsight explicitly ("Using Hindsight, recall...") or through system prompts that encourage memory use. The more intentionally you integrate memory into your workflow, the more valuable it becomes.
|
||||
|
||||
## Real-World Use Cases: When to Connect ChatGPT and Perplexity to Hindsight
|
||||
|
||||
**Software developers** can store architecture decisions and code patterns from ChatGPT brainstorming sessions. When they ask a follow-up question weeks later, Hindsight recalls the original design constraints and trade-offs, enabling ChatGPT to give more coherent guidance without re-explaining context.
|
||||
|
||||
**Researchers** can connect Perplexity to Hindsight to build persistent research knowledge. You store important findings and insights as you discover them. When fact-checking or building on prior work, you ask Perplexity to recall earlier findings, avoiding redundant searches and building on what you've already learned.
|
||||
|
||||
**Product managers and strategists** benefit from storing competitive insights, user feedback themes, and market research in Hindsight. When ChatGPT analyzes roadmap priorities, it has immediate access to months of accumulated context instead of starting fresh each conversation.
|
||||
|
||||
**Students and learners** use the combination to review past lessons and explanations. Ask ChatGPT a complex concept once; store the explanation. Later, when working on related material, ChatGPT builds on that prior explanation rather than giving a generic overview.
|
||||
|
||||
The magic happens when memory accumulates. A single session's insight becomes context for tomorrow's work, building an evolving understanding that both tools personalize to your needs.
|
||||
|
||||
## How to Connect ChatGPT and Perplexity to Hindsight
|
||||
|
||||
### Setting Up Hindsight with ChatGPT (Desktop & Web)
|
||||
|
||||
ChatGPT Plus and Team accounts support MCP via **Connectors**, a secure way to link external tools like Hindsight.
|
||||
|
||||
**Requirements:**
|
||||
- ChatGPT Plus or Team subscription
|
||||
- [Enable Developer Mode](https://platform.openai.com/account/api-keys) for beta features (optional; not needed for basic Connector use)
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Go to [ChatGPT Settings](https://chatgpt.com/settings)
|
||||
2. Navigate to **Apps & Connectors → Connectors**
|
||||
3. Click **Create connector**
|
||||
4. Fill in:
|
||||
- **Name:** `Hindsight` (or your preferred name)
|
||||
- **URL:** `https://api.hindsight.vectorize.io/mcp/YOUR_BANK_ID/`
|
||||
- Replace `YOUR_BANK_ID` with your memory bank name (or `default`)
|
||||
5. Click **Create**, a browser window opens for Hindsight Cloud login
|
||||
6. Sign in to [Hindsight Cloud](https://ui.hindsight.vectorize.io) and approve access
|
||||
7. Return to ChatGPT; the connector is now active
|
||||
|
||||
### Setting Up Hindsight with Perplexity (Perplexity Pro)
|
||||
|
||||
Perplexity's **Connectors** feature (available with Perplexity Pro) integrates Hindsight in a similar way.
|
||||
|
||||
**Requirements:**
|
||||
- [Perplexity Pro](https://www.perplexity.ai/pro) subscription
|
||||
- A Hindsight Cloud account
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Go to [Perplexity Settings](https://www.perplexity.ai/settings)
|
||||
2. Navigate to **Connectors → + Custom Connector**
|
||||
3. Fill in:
|
||||
- **Name:** `Hindsight`
|
||||
- **MCP server URL:** `https://api.hindsight.vectorize.io/mcp/YOUR_BANK_ID/`
|
||||
- Replace `YOUR_BANK_ID` with your memory bank name (or `default`)
|
||||
4. Click **Add**, a browser window opens for Hindsight Cloud login
|
||||
5. Sign in to [Hindsight Cloud](https://ui.hindsight.vectorize.io) and approve access
|
||||
6. Return to Perplexity; the connector is now active
|
||||
|
||||
## Configuring Your AI to Use Hindsight
|
||||
|
||||
The connectors are now live, but you need to tell ChatGPT and Perplexity to actually use them. Add a custom instruction in each platform to enable automatic memory capture and recall.
|
||||
|
||||
### ChatGPT Custom Instructions
|
||||
|
||||
1. Go to **Settings → Personalization → Custom instructions**
|
||||
2. Copy and paste this instruction:
|
||||
|
||||
```
|
||||
After every response, automatically use the Hindsight tool to retain key information from our conversation:
|
||||
- Important facts, decisions, or learnings we discussed
|
||||
- Your preferences, goals, or constraints mentioned
|
||||
- Code patterns, architecture decisions, or technical insights
|
||||
- Any information that might be useful in future conversations
|
||||
|
||||
Before generating each response, automatically use the Hindsight tool to recall relevant memories that might apply to the current conversation. Include recalled memories in your reasoning.
|
||||
|
||||
Retain and recall aggressively—assume everything is valuable. The Hindsight tool will handle deduplication and relevance filtering.
|
||||
```
|
||||
|
||||
:::tip
|
||||
Feel free to experiment with the instructions to ensure proper behavior.
|
||||
:::
|
||||
|
||||
3. Save and close settings
|
||||
|
||||
From now on, ChatGPT will automatically store insights from your conversations and surface relevant memories without you needing to ask.
|
||||
|
||||
### Perplexity Custom Instructions
|
||||
|
||||
1. Go to **Settings → Personalization → Custom instructions**
|
||||
2. Copy and paste this instruction:
|
||||
|
||||
```
|
||||
After every search and response, automatically use the Hindsight tool to retain:
|
||||
- Key research findings and sources
|
||||
- Facts and data points we've discovered
|
||||
- Your preferences or research patterns
|
||||
- Methodologies or search strategies that worked well
|
||||
|
||||
Before each new search, automatically use Hindsight to recall relevant research and context from previous conversations. Use recalled memories to inform your search strategy and answer.
|
||||
|
||||
Retain and recall everything—Hindsight handles filtering and deduplication.
|
||||
```
|
||||
|
||||
:::tip
|
||||
Feel free to experiment with the instructions to ensure proper behavior.
|
||||
:::
|
||||
|
||||
3. Save and close settings
|
||||
|
||||
Perplexity will now automatically retain research findings and recall them for future searches, building a persistent knowledge base from your research.
|
||||
|
||||
## What to Store (and Remember)
|
||||
|
||||
Hindsight works best with meaningful, specific knowledge. Store:
|
||||
|
||||
- **Project context**, goals, requirements, architecture decisions
|
||||
- **Personal preferences**, coding style, communication preferences, learning style
|
||||
- **Discoveries**, research findings, useful resources, lessons learned
|
||||
- **Domain knowledge**, industry facts, patterns, techniques you want to reference
|
||||
- **Decision history**, why you chose A over B, constraints you're working within
|
||||
|
||||
**Example of what to store:**
|
||||
```
|
||||
"We're building a real-time collaboration tool. Constraints:
|
||||
- <500ms latency for cursor updates
|
||||
- Support 10k concurrent users
|
||||
- GDPR-compliant data storage
|
||||
- Team prefers WebSockets over polling"
|
||||
```
|
||||
|
||||
Later, when you ask Perplexity *"How should we structure our database?"*, Hindsight recalls these constraints automatically. Perplexity's answer becomes tailored to your actual situation, not generic advice.
|
||||
|
||||
## Architecture: Single-Bank vs. Multi-Bank
|
||||
|
||||
By default, you use **single-bank mode**, each connector accesses one memory bank.
|
||||
|
||||
| Aspect | Single-Bank | Multi-Bank |
|
||||
|--------|------------|-----------|
|
||||
| **URL** | `https://api.hindsight.vectorize.io/mcp/YOUR_BANK_ID/` | `https://api.hindsight.vectorize.io/mcp` |
|
||||
| **Scope** | One bank per connector | Multiple banks via `bank_id` parameter |
|
||||
| **Best for** | Dedicated memory per tool | Sharing memory across tools |
|
||||
| **Complexity** | Simpler; implicit bank | More setup; requires bank specification |
|
||||
|
||||
**Single-bank mode** is simpler for most users: one bank per tool, fewer decisions, clear separation.
|
||||
|
||||
Example: ChatGPT uses a `writing` bank for your writing projects. Perplexity uses a `research` bank for research queries. Each tool's memories stay isolated.
|
||||
|
||||
**Multi-bank mode** is for workflows where both tools need shared context.
|
||||
|
||||
Example: Both ChatGPT and Perplexity → `https://api.hindsight.vectorize.io/mcp`. Both tools access the same memory banks. Great for when ChatGPT and Perplexity collaborate on the same project.
|
||||
|
||||
## Comparing ChatGPT and Perplexity as Memory Tools
|
||||
|
||||
| Aspect | ChatGPT | Perplexity |
|
||||
|--------|---------|-----------|
|
||||
| **Best for** | Deep conversations, reasoning with memory | Research with memory, fact-checking |
|
||||
| **Memory recall** | Hindsight tools in tools menu | Hindsight in Sources menu |
|
||||
| **Session continuity** | Good for multi-turn problem-solving | Good for iterative research |
|
||||
| **Web integration** | Limited (beta) | Integrated; can combine memory + web search |
|
||||
| **Memory context limit** | Depends on conversation length | Depends on search result count |
|
||||
|
||||
**ChatGPT + Hindsight works best for:**
|
||||
- Building projects (remembers your architecture decisions)
|
||||
- Learning (builds on previous lessons)
|
||||
- Creative work (recalls your style preferences)
|
||||
|
||||
**Perplexity + Hindsight works best for:**
|
||||
- Research (combines web search with your past research)
|
||||
- Fact-checking (verifies against what you've learned)
|
||||
- Competitive analysis (recalls market context)
|
||||
|
||||
**Ideal setup**: Use both tools together. ChatGPT handles reasoning with context. Perplexity handles research with context. Let them share Hindsight banks for coordinated workflows.
|
||||
|
||||
## Data Privacy and Security
|
||||
|
||||
- **OAuth-secured**, no API keys, no copy-paste secrets
|
||||
- **Your account**, memories live in your Hindsight Cloud account
|
||||
- **Encrypted in transit**, HTTPS + TLS for all connections
|
||||
- **No vendor lock-in**, export your memories anytime
|
||||
- **Scoped access**, each connector can only read/write to its assigned bank
|
||||
|
||||
When you approve OAuth in the browser, you're authorizing ChatGPT or Perplexity to:
|
||||
- **Read** your memory banks (to recall relevant facts)
|
||||
- **Write** to your memory banks (to store new discoveries)
|
||||
- **Search** your memories (to find context)
|
||||
|
||||
You can revoke access anytime by removing the connector in ChatGPT or Perplexity settings.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Connector failed to load"**
|
||||
- Verify the URL is correct (no typos in your bank ID)
|
||||
- Check that your Hindsight Cloud account is active
|
||||
- Try re-creating the connector
|
||||
|
||||
**"Authorization failed" or "Access denied"**
|
||||
- Make sure you're signing in with the same Hindsight Cloud account where you want to store memories
|
||||
- If using a team account, verify you have permission to access the bank
|
||||
- Try logging out and back in
|
||||
|
||||
**"Memory tools appear but don't return results"**
|
||||
- Give Hindsight a few seconds to index memories (processing is async)
|
||||
- Make sure you've stored relevant memories using the `retain` operation
|
||||
- Check the memory bank name matches your connector URL
|
||||
|
||||
**Memories aren't being stored**
|
||||
- Use the Hindsight Cloud dashboard to verify memories are being created
|
||||
- In ChatGPT or Perplexity, explicitly ask Hindsight to store something: *"Hindsight, remember that we use Vue. js for frontend"*
|
||||
- Check that your bank isn't full (unlikely, but possible with very large memory sets)
|
||||
|
||||
## Best Practices for Memory Management
|
||||
|
||||
Once you connect ChatGPT and Perplexity to Hindsight, a few practices maximize value:
|
||||
|
||||
**Be intentional about what you store.** Not every conversation needs retention. Focus on storing insights that will be useful later: lessons learned, architectural decisions, research findings, and personal preferences. Storing trivial facts clutters your memory bank and makes retrieval less useful.
|
||||
|
||||
**Use consistent terminology.** If you call your project "ProjectX" in one session and "Project X" in another, Hindsight's semantic search may miss the connection. Establish naming conventions for projects, tools, and concepts, and stick to them.
|
||||
|
||||
**Review and refine.** The Hindsight Cloud dashboard lets you browse your memory bank. Periodically review what you've stored. Delete outdated information and consolidate similar insights. A curated memory bank is more valuable than an exhaustive one.
|
||||
|
||||
**Structure multi-part decisions.** When storing complex decisions (like architecture trade-offs), include context: the constraints, alternatives considered, and why you chose one path. Future-you will thank present-you for the context.
|
||||
|
||||
**Cross-reference between tools.** If using both ChatGPT and Perplexity on related tasks, store context in a shared Hindsight bank (multi-bank mode) so both tools access the same knowledge base. This creates a unified context across your workflow.
|
||||
|
||||
## Getting Started with ChatGPT and Perplexity Memory
|
||||
|
||||
1. **Create a Hindsight Cloud account**, [Sign up free](https://ui.hindsight.vectorize.io/signup)
|
||||
2. **Add the connector**, follow the ChatGPT or Perplexity setup steps above
|
||||
3. **Set up your memory bank structure**, decide whether single-bank mode (separate memories per tool) or multi-bank mode (shared memory) fits your workflow
|
||||
4. **Store your first memory**, ask ChatGPT or Perplexity to remember something important to you
|
||||
5. **Test recall**, start a new session, ask a related question, and watch Hindsight retrieve your stored memory
|
||||
6. **Build the habit**, over time, proactively store meaningful insights from your conversations
|
||||
|
||||
Over weeks and months, as your memory bank grows, you'll notice ChatGPT and Perplexity give increasingly personalized, informed answers, because they're now working with your accumulated context instead of starting fresh every time.
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: "ChatGPT Persistent Memory with Hindsight | Integration"
|
||||
description: "Add long-term memory to ChatGPT with Hindsight. Use OAuth-secured MCP connectors to store conversations, recall relevant context, and build persistent knowledge across sessions."
|
||||
---
|
||||
|
||||
# ChatGPT
|
||||
|
||||
Add persistent, searchable memory to [ChatGPT](https://chatgpt.com) using [Hindsight](https://vectorize.io/hindsight). Store insights from your conversations and automatically recall relevant context in future sessions—all secured with OAuth.
|
||||
|
||||
## Overview
|
||||
|
||||
ChatGPT's built-in memory helps with preferences, but knowledge from specific conversations (research, code, decisions) is lost when you start a new chat. Hindsight solves this by providing:
|
||||
|
||||
- **Cross-session memory** — Knowledge from one conversation persists to the next
|
||||
- **Smart recall** — Hindsight automatically retrieves relevant memories when you need them
|
||||
- **No API keys** — OAuth handles authentication securely
|
||||
- **Custom instructions** — Aggressive auto-retain/recall via system prompts
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Hindsight Cloud Account
|
||||
|
||||
[Sign up free](https://ui.hindsight.vectorize.io/signup) for Hindsight Cloud.
|
||||
|
||||
### 2. Add Hindsight as a Connector in ChatGPT
|
||||
|
||||
1. Go to [ChatGPT Settings](https://chatgpt.com/settings)
|
||||
2. Navigate to **Apps & Connectors → Connectors**
|
||||
3. Click **Create connector**
|
||||
4. Fill in:
|
||||
- **Name:** `Hindsight` (or your preferred name)
|
||||
- **URL:** `https://api.hindsight.vectorize.io/mcp/default/`
|
||||
5. Click **Create** — a browser window opens for Hindsight Cloud login
|
||||
6. Sign in to [Hindsight Cloud](https://ui.hindsight.vectorize.io) and approve access
|
||||
7. Return to ChatGPT; the connector is now active
|
||||
|
||||
To use in a chat: click **+** in the message composer → **More** → select **Hindsight**
|
||||
|
||||
### 3. Configure Custom Instructions for Automatic Retention
|
||||
|
||||
The connector is now active, but you need to tell ChatGPT to actually use it. Add custom instructions to enable automatic memory capture and recall:
|
||||
|
||||
1. Go to **Settings → Personalization → Custom instructions**
|
||||
2. Copy and paste this instruction:
|
||||
|
||||
```
|
||||
After every response, automatically use the Hindsight tool to retain key information from our conversation:
|
||||
- Important facts, decisions, or learnings we discussed
|
||||
- Your preferences, goals, or constraints mentioned
|
||||
- Code patterns, architecture decisions, or technical insights
|
||||
- Any information that might be useful in future conversations
|
||||
|
||||
Before generating each response, automatically use the Hindsight tool to recall relevant memories that might apply to the current conversation. Include recalled memories in your reasoning.
|
||||
|
||||
Retain and recall aggressively—assume everything is valuable. The Hindsight tool will handle deduplication and relevance filtering.
|
||||
```
|
||||
|
||||
3. Save and close settings
|
||||
|
||||
From now on, ChatGPT will automatically store insights from your conversations and surface relevant memories without you needing to ask.
|
||||
|
||||
:::tip
|
||||
Feel free to experiment with the instructions to ensure proper behavior.
|
||||
:::
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic retention** — Custom instructions tell ChatGPT to store insights after every response
|
||||
- **Intelligent recall** — ChatGPT queries Hindsight before generating each response to include relevant memories as context
|
||||
- **OAuth-secured** — No API keys to copy-paste; sign in once via browser
|
||||
- **Bank isolation** — Separate memory banks for different projects or purposes
|
||||
- **Semantic search** — Hindsight understands context, not just keyword matching
|
||||
|
||||
## What to Store
|
||||
|
||||
Store meaningful, specific knowledge for best results:
|
||||
|
||||
- **Project context** — goals, requirements, architecture decisions, constraints
|
||||
- **Personal preferences** — coding style, communication preferences, learning style
|
||||
- **Discoveries** — research findings, useful resources, lessons learned
|
||||
- **Domain knowledge** — industry facts, patterns, techniques you reference
|
||||
- **Decision history** — why you chose A over B, trade-offs considered
|
||||
|
||||
**Example of what to store:**
|
||||
```
|
||||
"We're building a real-time collaboration tool. Constraints:
|
||||
- <500ms latency for cursor updates
|
||||
- Support 10k concurrent users
|
||||
- GDPR-compliant data storage
|
||||
- Team prefers WebSockets over polling"
|
||||
```
|
||||
|
||||
Later, when you ask ChatGPT *"How should we structure our database?"*, Hindsight recalls these constraints. ChatGPT's answer becomes tailored to your actual situation, not generic advice.
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Be intentional about what you store.** Not every conversation needs retention. Focus on storing insights that will be useful later: lessons learned, architectural decisions, research findings, and personal preferences. Storing trivial facts clutters your memory bank and makes retrieval less useful.
|
||||
|
||||
**Use consistent terminology.** If you call your project "ProjectX" in one session and "Project X" in another, Hindsight's semantic search may miss the connection. Establish naming conventions and stick to them.
|
||||
|
||||
**Review and refine.** The Hindsight Cloud dashboard lets you browse your memory bank. Periodically review what you've stored. Delete outdated information and consolidate similar insights. A curated memory bank is more valuable than an exhaustive one.
|
||||
|
||||
**Structure multi-part decisions.** When storing complex decisions (like architecture trade-offs), include context: the constraints, alternatives considered, and why you chose one path. Future-you will thank present-you for the context.
|
||||
|
||||
## Architecture: Single-Bank vs. Multi-Bank
|
||||
|
||||
### Single-Bank Mode (Recommended)
|
||||
|
||||
Each connector accesses one memory bank. Simpler for most users.
|
||||
|
||||
- **URL:** `https://api.hindsight.vectorize.io/mcp/YOUR_BANK_ID/`
|
||||
- **Setup:** Just enter the URL in the Connector settings
|
||||
- **Best for:** Dedicated memory per tool (e.g., ChatGPT uses a `writing` bank)
|
||||
|
||||
### Multi-Bank Mode
|
||||
|
||||
Both tools access multiple banks via bank_id parameter.
|
||||
|
||||
- **URL:** `https://api.hindsight.vectorize.io/mcp`
|
||||
- **Setup:** Requires additional configuration in Hindsight Cloud
|
||||
- **Best for:** When ChatGPT and Perplexity collaborate on the same project
|
||||
|
||||
## Data Privacy and Security
|
||||
|
||||
- **OAuth-secured** — no API keys, no copy-paste secrets
|
||||
- **Your account** — memories live in your Hindsight Cloud account
|
||||
- **Encrypted in transit** — HTTPS + TLS for all connections
|
||||
- **No vendor lock-in** — export your memories anytime
|
||||
- **Scoped access** — the connector can only read/write to its assigned bank
|
||||
|
||||
When you approve OAuth in the browser, you're authorizing ChatGPT to:
|
||||
- **Read** your memory banks (to recall relevant facts)
|
||||
- **Write** to your memory banks (to store new discoveries)
|
||||
- **Search** your memories (to find context)
|
||||
|
||||
You can revoke access anytime by removing the connector in ChatGPT settings.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Connector failed to load"**
|
||||
- Verify the URL is correct (no typos in your bank ID)
|
||||
- Check that your Hindsight Cloud account is active
|
||||
- Try re-creating the connector
|
||||
|
||||
**"Authorization failed" or "Access denied"**
|
||||
- Make sure you're signing in with the same Hindsight Cloud account where you want to store memories
|
||||
- If using a team account, verify you have permission to access the bank
|
||||
- Try logging out and back in
|
||||
|
||||
**"Memory tools appear but don't return results"**
|
||||
- Give Hindsight a few seconds to index memories (processing is async)
|
||||
- Make sure you've stored relevant memories using the `retain` operation
|
||||
- Check the memory bank name matches your connector URL
|
||||
|
||||
**Memories aren't being stored**
|
||||
- Use the Hindsight Cloud dashboard to verify memories are being created
|
||||
- In ChatGPT, explicitly ask Hindsight to store something: *"Hindsight, remember that we use Vue.js for frontend"*
|
||||
- Check that your bank isn't full (unlikely, but possible with very large memory sets)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create your Hindsight Cloud account
|
||||
2. Add the Hindsight connector to ChatGPT
|
||||
3. Set up your custom instructions
|
||||
4. Store your first memory — ask ChatGPT to remember something important to you
|
||||
5. Start a new chat and ask a related question — watch Hindsight retrieve your stored memory
|
||||
6. Build the habit of storing meaningful insights over time
|
||||
|
||||
Over weeks and months, as your memory bank grows, ChatGPT will give increasingly personalized, informed answers because it's working with your accumulated context instead of starting fresh every time.
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: "Perplexity Persistent Memory with Hindsight | Integration"
|
||||
description: "Add long-term memory to Perplexity with Hindsight. Use OAuth-secured MCP connectors to retain research findings, recall relevant context, and build a persistent knowledge base across searches."
|
||||
---
|
||||
|
||||
# Perplexity
|
||||
|
||||
Add persistent, searchable memory to [Perplexity](https://www.perplexity.ai) using [Hindsight](https://vectorize.io/hindsight). Store research findings and automatically recall relevant context in future searches—all secured with OAuth.
|
||||
|
||||
## Overview
|
||||
|
||||
Perplexity excels at research and fact-checking, but you have to re-discover the same facts across sessions. Hindsight solves this by providing:
|
||||
|
||||
- **Research knowledge base** — Discoveries, findings, and sources persist across searches
|
||||
- **Smart recall** — Hindsight automatically retrieves relevant research from your history
|
||||
- **No API keys** — OAuth handles authentication securely
|
||||
- **Custom instructions** — Aggressive auto-retain/recall via system prompts
|
||||
- **Web search + memory** — Combine Perplexity's web research with your accumulated knowledge
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Hindsight Cloud Account
|
||||
|
||||
[Sign up free](https://ui.hindsight.vectorize.io/signup) for Hindsight Cloud.
|
||||
|
||||
### 2. Add Hindsight as a Connector in Perplexity
|
||||
|
||||
Requires **Perplexity Pro** subscription. Remote MCP connectors are a Pro feature.
|
||||
|
||||
1. Go to [Perplexity Settings](https://www.perplexity.ai/settings)
|
||||
2. Navigate to **Connectors → + Custom Connector**
|
||||
3. Fill in:
|
||||
- **Name:** `Hindsight`
|
||||
- **MCP server URL:** `https://api.hindsight.vectorize.io/mcp/default/`
|
||||
4. Click **Add** — a browser window opens for Hindsight Cloud login
|
||||
5. Sign in to [Hindsight Cloud](https://ui.hindsight.vectorize.io) and approve access
|
||||
6. Return to Perplexity; the connector is now active
|
||||
|
||||
OAuth auto-discovery handles authentication automatically. That's it!
|
||||
|
||||
### 3. Configure Custom Instructions for Automatic Retention
|
||||
|
||||
The connector is now active, but you need to tell Perplexity to actually use it. Add custom instructions to enable automatic memory capture and recall:
|
||||
|
||||
1. Go to **Settings → Personalization → Custom instructions**
|
||||
2. Copy and paste this instruction:
|
||||
|
||||
```
|
||||
After every search and response, automatically use the Hindsight tool to retain:
|
||||
- Key research findings and sources
|
||||
- Facts and data points we've discovered
|
||||
- Your preferences or research patterns
|
||||
- Methodologies or search strategies that worked well
|
||||
|
||||
Before each new search, automatically use Hindsight to recall relevant research and context from previous conversations. Use recalled memories to inform your search strategy and answer.
|
||||
|
||||
Retain and recall everything—Hindsight handles filtering and deduplication.
|
||||
```
|
||||
|
||||
3. Save and close settings
|
||||
|
||||
Perplexity will now automatically retain research findings and recall them for future searches, building a persistent knowledge base from your research.
|
||||
|
||||
:::tip
|
||||
Feel free to experiment with the instructions to ensure proper behavior.
|
||||
:::
|
||||
|
||||
## Features
|
||||
|
||||
- **Auto-retain research** — Custom instructions tell Perplexity to store findings after every search
|
||||
- **Intelligent recall** — Perplexity queries Hindsight before each search to include relevant research as context
|
||||
- **OAuth-secured** — No API keys to copy-paste; sign in once via browser
|
||||
- **Bank isolation** — Separate memory banks for different research projects or domains
|
||||
- **Semantic search** — Hindsight understands context, not just keyword matching
|
||||
- **Web + memory** — Combine Perplexity's web search with your accumulated knowledge
|
||||
|
||||
## What to Store
|
||||
|
||||
Store meaningful research data for best results:
|
||||
|
||||
- **Research findings** — Key discoveries, trends, statistics you've found
|
||||
- **Source collection** — Useful articles, papers, resources you've discovered
|
||||
- **Research patterns** — Topics you frequently research, methodologies that work
|
||||
- **Preferences** — Information sources you prefer, reporting styles you like
|
||||
- **Domain knowledge** — Industry facts, terminology, context you've learned
|
||||
- **Decision context** — Why you chose one approach over another, constraints considered
|
||||
|
||||
**Example of what to store:**
|
||||
```
|
||||
"Q3 2026 AI benchmarks:
|
||||
- Claude Opus 4.7: Best reasoning, ~$15/1M input tokens
|
||||
- GPT-4o: Fastest inference, multimodal, ~$5/1M input
|
||||
- Llama 3.1: Open source, good for on-device, varies by host
|
||||
- Performance metrics: [include relevant benchmark links]
|
||||
- Use cases I care about: [your priorities]"
|
||||
```
|
||||
|
||||
Later, when you research *"What's the best LLM for my use case?"*, Hindsight recalls your preferences and benchmarks. Perplexity's answer becomes tailored to your situation, not generic.
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Be intentional about what you store.** Not every search needs retention. Focus on storing insights that will be useful later: research findings, source collections, methodologies, and personal preferences. Storing trivial facts clutters your memory bank and makes retrieval less useful.
|
||||
|
||||
**Use consistent terminology.** If you call a topic "machine learning" in one search and "deep learning" in another, Hindsight's semantic search may miss the connection. Establish naming conventions and stick to them.
|
||||
|
||||
**Review and refine.** The Hindsight Cloud dashboard lets you browse your memory bank. Periodically review what you've stored. Delete outdated information (e.g., last year's benchmark data) and consolidate similar insights. A curated memory bank is more valuable than an exhaustive one.
|
||||
|
||||
**Structure multi-part findings.** When storing complex research (like technology comparisons), include context: what you were evaluating, the criteria, alternatives considered, and your conclusions. Future-you will thank present-you for the context.
|
||||
|
||||
**Cross-reference with other tools.** If using both ChatGPT and Perplexity on related tasks, store research context in a shared Hindsight bank (multi-bank mode) so both tools access the same knowledge base. This creates unified context across your workflow.
|
||||
|
||||
## Comparison with ChatGPT
|
||||
|
||||
| Aspect | ChatGPT | Perplexity |
|
||||
|--------|---------|-----------|
|
||||
| **Best for** | Deep conversations, reasoning with memory | Research with memory, fact-checking |
|
||||
| **Hindsight integration** | Retain reasoning, insights, preferences | Retain research findings, sources |
|
||||
| **Session continuity** | Good for multi-turn problem-solving | Good for iterative research |
|
||||
| **Web integration** | Limited (beta) | Integrated; combines memory + web search |
|
||||
| **Memory context limit** | Depends on conversation length | Depends on search result count |
|
||||
|
||||
**Recommended use:**
|
||||
- **ChatGPT + Hindsight** — Build projects, learn complex topics, creative work
|
||||
- **Perplexity + Hindsight** — Research, fact-checking, competitive analysis, news tracking
|
||||
|
||||
**Ideal setup:** Use both tools together. ChatGPT handles reasoning with context. Perplexity handles research with context. Let them share Hindsight banks for coordinated workflows.
|
||||
|
||||
## Architecture: Single-Bank vs. Multi-Bank
|
||||
|
||||
### Single-Bank Mode (Recommended)
|
||||
|
||||
Each connector accesses one memory bank. Simpler for most users.
|
||||
|
||||
- **URL:** `https://api.hindsight.vectorize.io/mcp/default/`
|
||||
- **Setup:** Just enter the URL in the Connector settings
|
||||
- **Best for:** Dedicated memory per tool (e.g., Perplexity uses a `research` bank)
|
||||
|
||||
### Multi-Bank Mode
|
||||
|
||||
Both tools access multiple banks via bank_id parameter.
|
||||
|
||||
- **URL:** `https://api.hindsight.vectorize.io/mcp`
|
||||
- **Setup:** Requires additional configuration in Hindsight Cloud
|
||||
- **Best for:** When ChatGPT and Perplexity collaborate on the same project
|
||||
|
||||
## Data Privacy and Security
|
||||
|
||||
- **OAuth-secured** — no API keys, no copy-paste secrets
|
||||
- **Your account** — memories live in your Hindsight Cloud account
|
||||
- **Encrypted in transit** — HTTPS + TLS for all connections
|
||||
- **No vendor lock-in** — export your memories anytime
|
||||
- **Scoped access** — the connector can only read/write to its assigned bank
|
||||
|
||||
When you approve OAuth in the browser, you're authorizing Perplexity to:
|
||||
- **Read** your memory banks (to recall relevant research)
|
||||
- **Write** to your memory banks (to store new findings)
|
||||
- **Search** your memories (to find context)
|
||||
|
||||
You can revoke access anytime by removing the connector in Perplexity settings.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Connector failed to load"**
|
||||
- Verify you have Perplexity Pro (required for Remote MCP connectors)
|
||||
- Verify the URL is correct (no typos in your bank ID)
|
||||
- Check that your Hindsight Cloud account is active
|
||||
- Try re-creating the connector
|
||||
|
||||
**"Authorization failed" or "Access denied"**
|
||||
- Make sure you're signing in with the same Hindsight Cloud account where you want to store memories
|
||||
- If using a team account, verify you have permission to access the bank
|
||||
- Try logging out and back in
|
||||
|
||||
**"Memory tools appear but don't return results"**
|
||||
- Give Hindsight a few seconds to index memories (processing is async)
|
||||
- Make sure you've stored relevant memories using the `retain` operation
|
||||
- Check the memory bank name matches your connector URL
|
||||
|
||||
**Memories aren't being stored**
|
||||
- Use the Hindsight Cloud dashboard to verify memories are being created
|
||||
- In Perplexity, explicitly ask Hindsight to store something: *"Hindsight, remember that Q3 2026 benchmarks show Claude Opus is best for reasoning"*
|
||||
- Check that your bank isn't full (unlikely, but possible with very large memory sets)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Verify you have Perplexity Pro
|
||||
2. Create your Hindsight Cloud account
|
||||
3. Add the Hindsight connector to Perplexity
|
||||
4. Set up your custom instructions
|
||||
5. Store your first research finding
|
||||
6. Start a new search on a related topic — watch Hindsight retrieve your stored research
|
||||
7. Build the habit of storing meaningful research insights over time
|
||||
|
||||
Over weeks and months, as your memory bank grows, Perplexity will give increasingly informed answers because it's combining current web research with your accumulated knowledge base instead of starting fresh every time.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
sidebar_position: 22
|
||||
title: "Pipecat Persistent Memory with Hindsight | Integration"
|
||||
description: "Add persistent long-term memory to Pipecat voice AI pipelines via Hindsight. A single FrameProcessor slots between the user aggregator and LLM to recall context before each turn and retain conversation content after."
|
||||
---
|
||||
|
||||
# Pipecat
|
||||
|
||||
Persistent long-term memory for [Pipecat](https://github.com/pipecat-ai/pipecat) voice AI pipelines via [Hindsight](https://vectorize.io/hindsight). A single `FrameProcessor` slots between your user context aggregator and LLM service — recalling relevant memories before each turn and retaining conversation content after.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Start Hindsight (self-hosted)
|
||||
pip install hindsight-all
|
||||
export HINDSIGHT_API_LLM_API_KEY=your-openai-key
|
||||
hindsight-api
|
||||
|
||||
# 2. Install the integration
|
||||
pip install hindsight-pipecat
|
||||
```
|
||||
|
||||
```python
|
||||
from pipecat.pipeline.pipeline import Pipeline
|
||||
from hindsight_pipecat import HindsightMemoryService
|
||||
|
||||
memory = HindsightMemoryService(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
pipeline = Pipeline([
|
||||
transport.input(),
|
||||
stt_service,
|
||||
user_aggregator,
|
||||
memory, # ← add between user_aggregator and LLM
|
||||
llm_service,
|
||||
assistant_aggregator,
|
||||
tts_service,
|
||||
transport.output(),
|
||||
])
|
||||
```
|
||||
|
||||
Or with [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup):
|
||||
|
||||
```python
|
||||
memory = HindsightMemoryService(
|
||||
bank_id="user-123",
|
||||
hindsight_api_url="https://api.hindsight.vectorize.io",
|
||||
api_key="hsk_your_token_here",
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
New turn starts
|
||||
└─ OpenAILLMContextFrame arrives
|
||||
├─ Retain previous complete turn (user+assistant) — fire-and-forget
|
||||
└─ Recall relevant memories for current user query
|
||||
└─ Inject as <hindsight_memories> system message
|
||||
└─ Forward enriched context to LLM
|
||||
```
|
||||
|
||||
On each `OpenAILLMContextFrame`:
|
||||
|
||||
1. **Retain** — any new complete user+assistant turn pairs are sent to Hindsight asynchronously (non-blocking)
|
||||
2. **Recall** — the latest user message is used as the search query; results are injected as a system message before the LLM sees the context
|
||||
3. **Forward** — the enriched context frame is pushed downstream
|
||||
|
||||
Memory accumulates across calls. By the third or fourth turn, recall starts surfacing useful context that the pipeline didn't have to re-establish.
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
HindsightMemoryService(
|
||||
bank_id="user-123", # Required: memory bank to use
|
||||
hindsight_api_url="...", # Hindsight API URL
|
||||
api_key="hsk_...", # API key (Hindsight Cloud)
|
||||
recall_budget="mid", # "low", "mid", or "high"
|
||||
recall_max_tokens=4096, # Max tokens for recall results
|
||||
enable_recall=True, # Inject memories before LLM
|
||||
enable_retain=True, # Store turns after each exchange
|
||||
memory_prefix="Relevant memories from past conversations:\n",
|
||||
)
|
||||
```
|
||||
|
||||
### Global Configuration
|
||||
|
||||
```python
|
||||
from hindsight_pipecat import configure
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="hsk_...",
|
||||
recall_budget="mid",
|
||||
)
|
||||
|
||||
# Now create services without repeating connection details
|
||||
memory = HindsightMemoryService(bank_id="user-123")
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
Tested with Pipecat `v0.0.108`. The processor handles both the new `LLMContextFrame` and the deprecated `OpenAILLMContextFrame` for forward compatibility.
|
||||
|
||||
## Manual Testing
|
||||
|
||||
The `examples/` directory includes an interactive text-based chat simulator for testing memory recall/retain without requiring Daily/Deepgram/Cartesia API keys:
|
||||
|
||||
```bash
|
||||
python examples/interactive_chat.py --bank demo-user
|
||||
```
|
||||
|
||||
The `examples/basic_pipeline.py` shows the full voice pipeline with Daily + Deepgram + OpenAI + Cartesia.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A running Hindsight instance:
|
||||
|
||||
**Self-hosted:**
|
||||
```bash
|
||||
pip install hindsight-all
|
||||
export HINDSIGHT_API_LLM_API_KEY=your-api-key
|
||||
hindsight-api # starts on http://localhost:8888
|
||||
```
|
||||
|
||||
**Hindsight Cloud:** [Sign up](https://ui.hindsight.vectorize.io/signup) — no self-hosting required.
|
||||
@@ -162,7 +162,7 @@ To switch between backends:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `volcano`, `openrouter`, `none` | `openai` |
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `deepseek`, `ollama`, `lmstudio`, `llamacpp`, `vertexai`, `bedrock`, `litellm`, `volcano`, `openrouter`, `none` | `openai` |
|
||||
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - |
|
||||
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` |
|
||||
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default |
|
||||
@@ -174,6 +174,7 @@ To switch between backends:
|
||||
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
|
||||
| `HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER` | OpenAI service tier: `flex` for 50% cost savings (OpenAI Flex Processing) | None (default) |
|
||||
| `HINDSIGHT_API_LLM_EXTRA_BODY` | JSON dict merged into `extra_body` for all OpenAI-compatible API calls. Useful for custom model servers (e.g., vLLM `chat_template_kwargs`). | `null` |
|
||||
| `HINDSIGHT_API_LLM_SIMPLIFY_JSON_SCHEMA` | Flatten `$ref`/`$defs`/`anyOf` in JSON schemas before sending to LLMs. Required for Ollama structured output; improves compliance for all providers. Set to `false` to send raw Pydantic schemas. | `true` |
|
||||
| `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` | JSON-encoded list of `{category, threshold}` dicts for Gemini/VertexAI content safety filtering | `null` |
|
||||
|
||||
**Provider Examples**
|
||||
@@ -254,6 +255,18 @@ export HINDSIGHT_API_LLM_PROVIDER=openrouter
|
||||
export HINDSIGHT_API_LLM_API_KEY=your-openrouter-api-key
|
||||
export HINDSIGHT_API_LLM_MODEL=qwen/qwen3.5-9b
|
||||
|
||||
# DeepSeek (OpenAI-compatible, https://api.deepseek.com)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=deepseek
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash
|
||||
# Notes:
|
||||
# - `deepseek-v4-flash` defaults to thinking mode at the API level (treated as
|
||||
# `deepseek-reasoner`). Hindsight handles this transparently; the reflect
|
||||
# agent will not crash with "deepseek-reasoner does not support this
|
||||
# tool_choice".
|
||||
# - Use `deepseek-v4-pro` for the higher-quality reasoning route.
|
||||
# - Use `deepseek-chat` for the non-thinking alias (faster, cheaper).
|
||||
|
||||
# AWS Bedrock (native support - no API key needed, uses AWS credentials)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=bedrock
|
||||
export HINDSIGHT_API_LLM_MODEL=us.amazon.nova-2-lite-v1:0
|
||||
@@ -561,6 +574,7 @@ Google's `gemini-embedding-001` produces 3072 dimensions natively but supports c
|
||||
| `HINDSIGHT_API_RERANKER_GOOGLE_SERVICE_ACCOUNT_KEY` | Path to service account JSON key (falls back to `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY`). If unset, uses ADC. | - |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_MODEL` | FlashRank model for fast CPU-based reranking | `ms-marco-MiniLM-L-12-v2` |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR` | Cache directory for FlashRank models | System default |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_CPU_MEM_ARENA` | Enable ONNX Runtime CPU memory arena for FlashRank. When `true`, ONNX pre-allocates a memory arena that never shrinks, causing RSS to grow monotonically. `false` trades slightly slower per-call allocation for bounded RSS. | `false` |
|
||||
| `HINDSIGHT_API_RERANKER_JINA_MLX_MODEL_PATH` | Local path to downloaded `jina-reranker-v3-mlx` model (auto-downloads from HuggingFace if unset) | - |
|
||||
|
||||
```bash
|
||||
@@ -919,6 +933,25 @@ export HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID=your-org-id
|
||||
export HINDSIGHT_API_FILE_PARSER=iris,markitdown
|
||||
```
|
||||
|
||||
#### Parser: llama_parse
|
||||
|
||||
Cloud-based extraction via [LlamaParse](https://docs.cloud.llamaindex.ai/llamaparse) (LlamaIndex). Strong extraction for complex layouts — tables, charts, multi-column PDFs.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY` | LlamaCloud API key (typically starts with `llx-`) | — |
|
||||
|
||||
**Supported formats:** PDF, DOCX, PPTX, XLSX, HTML, EPUB, RTF, TXT, and many more — see the [LlamaParse docs](https://docs.cloud.llamaindex.ai/llamaparse/features/supported_document_types) for the full list.
|
||||
|
||||
```bash
|
||||
# Use llama_parse as the only parser
|
||||
export HINDSIGHT_API_FILE_PARSER=llama_parse
|
||||
export HINDSIGHT_API_FILE_PARSER_LLAMA_PARSE_API_KEY=llx-your-api-key
|
||||
|
||||
# Or: try llama_parse first, fall back to markitdown
|
||||
export HINDSIGHT_API_FILE_PARSER=llama_parse,markitdown
|
||||
```
|
||||
|
||||
```bash
|
||||
# Increase batch limits for large file imports
|
||||
export HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE=20
|
||||
@@ -1026,7 +1059,8 @@ Observations are deduplicated, evidence-grounded knowledge consolidated from mul
|
||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND` | Maximum memories processed per consolidation round. When the limit is reached, the job yields its worker slot and re-queues itself so other banks get fair scheduling. Mental model refreshes only run on the final round. `0` = unlimited. Configurable per bank. | `100` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE` | Number of facts sent to the LLM in a single consolidation call. Higher values reduce LLM calls and improve throughput at the cost of larger prompts. Set to `1` to disable batching. Configurable per bank. | `8` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` | Total token budget for source facts included with observations in the consolidation prompt. `-1` = unlimited. Configurable per bank. | `-1` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_RECALL_BUDGET` | Budget level for the recall pass inside consolidation (`low`, `mid`, `high`). Lower budgets fetch fewer candidate rows, reducing peak memory usage on large banks. | `low` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` | Total token budget for source facts included with observations in the consolidation prompt. `-1` = unlimited. Configurable per bank. | `4096` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION` | Per-observation token cap for source facts in the consolidation prompt. Each observation independently gets at most this many tokens of source facts. `-1` = unlimited. Configurable per bank. | `256` |
|
||||
| `HINDSIGHT_API_OBSERVATIONS_MISSION` | What this bank should synthesise into durable observations. Replaces the built-in consolidation rules — leave unset to use the server default. | - |
|
||||
| `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE` | Maximum number of observations allowed per tag scope. When the limit is reached, consolidation will only update or delete existing observations — no new ones are created. Applies per tag scope (e.g., per-tag when using `per_tag` observation scopes). Observations with no tags are not subject to this limit. `-1` = unlimited. Configurable per bank. | `-1` |
|
||||
|
||||
@@ -45,6 +45,24 @@ Configure which one to use with `HINDSIGHT_API_VECTOR_EXTENSION`. See [Configura
|
||||
|
||||
You need an LLM API key for fact extraction, entity resolution, and answer generation. See [Models](./models) for supported providers, model recommendations, and configuration.
|
||||
|
||||
### Hardware
|
||||
|
||||
Hindsight is designed to run on commodity hardware. The footprint depends mainly on whether the **full** image (which bundles local embedding and reranker models) or the **slim** image (which delegates those to external providers) is used.
|
||||
|
||||
| Component | Minimum RAM | Recommended RAM | Notes |
|
||||
|-----------|-------------|-----------------|-------|
|
||||
| **API — Full image** | 1.5 GB | 2 GB | Loads local BGE embedder (~130 MB) and MiniLM cross-encoder (~90 MB) into memory, plus PyTorch/ONNX runtime arenas. Idle RSS settles around 0.8–1.0 GB; expect 1.2–1.5 GB under load. |
|
||||
| **API — Slim image** | 512 MB | 1 GB | No local models. Steady-state RSS is dominated by Python runtime and DB connections. Requires [external embedding and reranker providers](./configuration#embeddings) (e.g. TEI, OpenAI, Cohere). |
|
||||
| **Control Plane (UI)** | 128 MB | 256 MB | Next.js process, lightweight. |
|
||||
| **Worker** (if separated) | Same as API image variant | Same as API image variant | Workers load the same models as the API server. |
|
||||
| **PostgreSQL** | 512 MB | 1 GB+ | Scales with the number of memories and indexes. |
|
||||
|
||||
:::tip Reducing the footprint
|
||||
The bulk of the full image's memory comes from the bundled embedding and reranker models and their PyTorch/ONNX runtimes. To shrink the deployment to a few hundred MB of RAM, switch to the **slim** image and configure [external embedding and reranker providers](./configuration#embeddings).
|
||||
:::
|
||||
|
||||
CPU vs GPU: 2 vCPUs on CPU-only is fine for development and basic workloads. For production traffic, the local reranker (cross-encoder) is the main bottleneck and typically benefits from a GPU to keep recall latency reasonable; alternatively, offload reranking to an [external reranker provider](./configuration#embeddings) (e.g. TEI, Cohere) on dedicated GPU hardware.
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {LLMProvidersGrid} from '@site/src/components/SupportedGrids';
|
||||
import {LLMProvidersTable} from '@site/src/components/LLMProvidersTable';
|
||||
|
||||
# Models
|
||||
|
||||
@@ -79,23 +80,7 @@ The following models have been tested and verified to work correctly with Hindsi
|
||||
|
||||
Each provider has a recommended default model that's used when `HINDSIGHT_API_LLM_MODEL` is not explicitly set. This makes configuration simpler - just specify the provider and get a sensible default:
|
||||
|
||||
| Provider | Default Model |
|
||||
|----------|--------------|
|
||||
| `openai` | `gpt-4o-mini` |
|
||||
| `anthropic` | `claude-haiku-4-5-20251001` |
|
||||
| `gemini` | `gemini-2.5-flash` |
|
||||
| `groq` | `openai/gpt-oss-120b` |
|
||||
| `minimax` | `MiniMax-M2.7` |
|
||||
| `ollama` | `gemma3:12b` |
|
||||
| `llamacpp` | `gemma-4-e2b-it` (auto-downloaded GGUF) |
|
||||
| `lmstudio` | `local-model` |
|
||||
| `vertexai` | `gemini-2.0-flash-001` |
|
||||
| `openai-codex` | `gpt-5.2-codex` |
|
||||
| `claude-code` | `claude-sonnet-4-5-20250929` |
|
||||
| `bedrock` | `us.amazon.nova-2-lite-v1:0` |
|
||||
| `volcano` | `doubao-pro-32k` |
|
||||
| `openrouter` | `qwen/qwen3.5-9b` |
|
||||
| `litellm` | `gpt-4o-mini` |
|
||||
<LLMProvidersTable />
|
||||
|
||||
**Example:** Setting just the provider uses its default model:
|
||||
```bash
|
||||
@@ -177,6 +162,11 @@ export HINDSIGHT_API_LLM_PROVIDER=minimax
|
||||
export HINDSIGHT_API_LLM_API_KEY=your-minimax-api-key
|
||||
export HINDSIGHT_API_LLM_MODEL=MiniMax-M2.7
|
||||
|
||||
# DeepSeek (https://api.deepseek.com)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=deepseek
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=deepseek-v4-flash # or deepseek-v4-pro / deepseek-chat / deepseek-reasoner
|
||||
|
||||
# Vertex AI (Google Cloud)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=vertexai
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001
|
||||
|
||||
@@ -178,6 +178,18 @@ const sidebars: SidebarsConfig = {
|
||||
label: 'Local MCP Server',
|
||||
customProps: { icon: '/img/icons/mcp.png' },
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/sdks/integrations/chatgpt',
|
||||
label: 'ChatGPT',
|
||||
customProps: { icon: '/img/icons/chatgpt.png' },
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/sdks/integrations/perplexity',
|
||||
label: 'Perplexity',
|
||||
customProps: { icon: '/img/icons/perplexity.png' },
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: '/sdks/integrations/litellm',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import {LLM_PROVIDERS} from '../data/llmProviders';
|
||||
|
||||
/**
|
||||
* Renders the "Provider Default Models" table from the single-source-of-truth
|
||||
* provider list in `src/data/llmProviders.tsx`. Skips entries without a default
|
||||
* model (e.g. the "OpenAI Compatible" pseudo-entry).
|
||||
*/
|
||||
export function LLMProvidersTable() {
|
||||
const rows = LLM_PROVIDERS.filter(p => p.id && p.defaultModel);
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>Default Model</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(({id, defaultModel, defaultModelNote}) => (
|
||||
<tr key={id}>
|
||||
<td><code>{id}</code></td>
|
||||
<td>
|
||||
<code>{defaultModel}</code>
|
||||
{defaultModelNote && <> ({defaultModelNote})</>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,8 @@
|
||||
import React from 'react';
|
||||
import type {IconType} from 'react-icons';
|
||||
import {IconGrid} from './IconGrid';
|
||||
import {SiPython, SiGo, SiOpenai, SiAnthropic, SiGooglegemini, SiOllama} from 'react-icons/si';
|
||||
import {LuTerminal, LuPlug, LuZap, LuBrainCog, LuSparkles, LuGlobe, LuLayers, LuCloud} from 'react-icons/lu';
|
||||
|
||||
const OpenAICompatibleIcon: IconType = ({size = 28, ...props}) => (
|
||||
<span style={{position: 'relative', display: 'inline-flex'}}>
|
||||
<SiOpenai size={size} {...props} />
|
||||
<span style={{
|
||||
position: 'absolute', bottom: -3, right: -6,
|
||||
fontSize: Math.round((size as number) * 0.5), fontWeight: 900, lineHeight: 1,
|
||||
color: 'currentColor',
|
||||
}}>+</span>
|
||||
</span>
|
||||
);
|
||||
import {SiPython, SiGo} from 'react-icons/si';
|
||||
import {LuTerminal, LuGlobe} from 'react-icons/lu';
|
||||
import {LLM_PROVIDERS} from '../data/llmProviders';
|
||||
|
||||
export function ClientsGrid() {
|
||||
return (
|
||||
@@ -29,20 +18,5 @@ export function ClientsGrid() {
|
||||
|
||||
|
||||
export function LLMProvidersGrid() {
|
||||
return (
|
||||
<IconGrid items={[
|
||||
{ label: 'OpenAI', icon: SiOpenai },
|
||||
{ label: 'Anthropic', icon: SiAnthropic },
|
||||
{ label: 'Google Gemini', icon: SiGooglegemini },
|
||||
{ label: 'Groq', icon: LuZap },
|
||||
{ label: 'Ollama', icon: SiOllama },
|
||||
{ label: 'LM Studio', icon: LuBrainCog },
|
||||
{ label: 'llama.cpp', icon: LuTerminal },
|
||||
{ label: 'MiniMax', icon: LuSparkles },
|
||||
{ label: 'Volcano Engine', icon: LuZap },
|
||||
{ label: 'OpenAI Compatible', icon: OpenAICompatibleIcon },
|
||||
{ label: 'AWS Bedrock', icon: LuCloud },
|
||||
{ label: 'LiteLLM (100+)', icon: LuLayers },
|
||||
]} />
|
||||
);
|
||||
return <IconGrid items={LLM_PROVIDERS.map(({label, icon}) => ({label, icon}))} />;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,26 @@
|
||||
"link": "/sdks/integrations/local-mcp",
|
||||
"icon": "/img/icons/mcp.png"
|
||||
},
|
||||
{
|
||||
"id": "chatgpt",
|
||||
"name": "ChatGPT",
|
||||
"description": "Add persistent long-term memory to ChatGPT via OAuth-secured MCP connectors. Store insights from conversations and recall relevant context across sessions.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "tool",
|
||||
"link": "/sdks/integrations/chatgpt",
|
||||
"icon": "/img/icons/chatgpt.png"
|
||||
},
|
||||
{
|
||||
"id": "perplexity",
|
||||
"name": "Perplexity",
|
||||
"description": "Add persistent long-term memory to Perplexity via OAuth-secured MCP connectors. Store research findings and recall relevant context across searches.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "tool",
|
||||
"link": "/sdks/integrations/perplexity",
|
||||
"icon": "/img/icons/perplexity.png"
|
||||
},
|
||||
{
|
||||
"id": "claude-code",
|
||||
"name": "Claude Code",
|
||||
@@ -200,6 +220,16 @@
|
||||
"link": "/sdks/integrations/openai-agents",
|
||||
"icon": "/img/icons/openai-agents.svg"
|
||||
},
|
||||
{
|
||||
"id": "pipecat",
|
||||
"name": "Pipecat",
|
||||
"description": "Persistent memory for Pipecat voice AI pipelines via a FrameProcessor that recalls context before each turn and retains conversation content after.",
|
||||
"type": "official",
|
||||
"by": "hindsight",
|
||||
"category": "tool",
|
||||
"link": "/sdks/integrations/pipecat",
|
||||
"icon": "/img/icons/pipecat.png"
|
||||
},
|
||||
{
|
||||
"id": "hindclaw",
|
||||
"name": "HindClaw",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{"id": "openai", "label": "OpenAI", "iconKey": "openai", "defaultModel": "gpt-4o-mini"},
|
||||
{"id": "anthropic", "label": "Anthropic", "iconKey": "anthropic", "defaultModel": "claude-haiku-4-5-20251001"},
|
||||
{"id": "gemini", "label": "Google Gemini", "iconKey": "gemini", "defaultModel": "gemini-2.5-flash"},
|
||||
{"id": "vertexai", "label": "Vertex AI", "iconKey": "gemini", "defaultModel": "gemini-2.0-flash-001"},
|
||||
{"id": "groq", "label": "Groq", "iconKey": "zap", "defaultModel": "openai/gpt-oss-120b"},
|
||||
{"id": "ollama", "label": "Ollama", "iconKey": "ollama", "defaultModel": "gemma3:12b"},
|
||||
{"id": "lmstudio", "label": "LM Studio", "iconKey": "brain", "defaultModel": "local-model"},
|
||||
{"id": "llamacpp", "label": "llama.cpp", "iconKey": "terminal", "defaultModel": "gemma-4-e2b-it", "defaultModelNote": "auto-downloaded GGUF"},
|
||||
{"id": "minimax", "label": "MiniMax", "iconKey": "sparkles", "defaultModel": "MiniMax-M2.7"},
|
||||
{"id": "deepseek", "label": "DeepSeek", "iconKey": "brain", "defaultModel": "deepseek-v4-flash"},
|
||||
{"id": "volcano", "label": "Volcano Engine", "iconKey": "zap", "defaultModel": "doubao-pro-32k"},
|
||||
{"id": "openrouter", "label": "OpenRouter", "iconKey": "globe", "defaultModel": "qwen/qwen3.5-9b"},
|
||||
{"id": "openai-codex", "label": "OpenAI Codex", "iconKey": "openai", "defaultModel": "gpt-5.2-codex"},
|
||||
{"id": "claude-code", "label": "Claude Code", "iconKey": "anthropic", "defaultModel": "claude-sonnet-4-5-20250929"},
|
||||
{"id": "bedrock", "label": "AWS Bedrock", "iconKey": "cloud", "defaultModel": "us.amazon.nova-2-lite-v1:0"},
|
||||
{"id": "", "label": "OpenAI Compatible", "iconKey": "openai-compatible"},
|
||||
{"id": "litellm", "label": "LiteLLM (100+)", "iconKey": "layers", "defaultModel": "gpt-4o-mini"}
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
import type {IconType} from 'react-icons';
|
||||
import React from 'react';
|
||||
import {SiOpenai, SiAnthropic, SiGooglegemini, SiOllama} from 'react-icons/si';
|
||||
import {LuTerminal, LuZap, LuBrainCog, LuSparkles, LuGlobe, LuLayers, LuCloud} from 'react-icons/lu';
|
||||
import providersJson from './llmProviders.json';
|
||||
|
||||
const OpenAICompatibleIcon: IconType = ({size = 28, ...props}) => (
|
||||
<span style={{position: 'relative', display: 'inline-flex'}}>
|
||||
<SiOpenai size={size} {...props} />
|
||||
<span style={{
|
||||
position: 'absolute', bottom: -3, right: -6,
|
||||
fontSize: Math.round((size as number) * 0.5), fontWeight: 900, lineHeight: 1,
|
||||
color: 'currentColor',
|
||||
}}>+</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
const ICON_REGISTRY: Record<string, IconType> = {
|
||||
openai: SiOpenai,
|
||||
anthropic: SiAnthropic,
|
||||
gemini: SiGooglegemini,
|
||||
ollama: SiOllama,
|
||||
terminal: LuTerminal,
|
||||
zap: LuZap,
|
||||
brain: LuBrainCog,
|
||||
sparkles: LuSparkles,
|
||||
globe: LuGlobe,
|
||||
layers: LuLayers,
|
||||
cloud: LuCloud,
|
||||
'openai-compatible': OpenAICompatibleIcon,
|
||||
};
|
||||
|
||||
export interface LLMProvider {
|
||||
/** HINDSIGHT_API_LLM_PROVIDER value, e.g. "deepseek". Empty string for the
|
||||
* "OpenAI Compatible" pseudo-entry which is not a real provider id. */
|
||||
id: string;
|
||||
/** Display name shown in the grid tile and table. */
|
||||
label: string;
|
||||
/** Icon component rendered in the grid tile. */
|
||||
icon: IconType;
|
||||
/** Provider default model. Undefined = no entry in the default-models table. */
|
||||
defaultModel?: string;
|
||||
/** Optional note rendered in the default-models table. */
|
||||
defaultModelNote?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single source of truth for the supported LLM providers shown in the docs.
|
||||
*
|
||||
* Adding a provider means editing `llmProviders.json` ONLY. That file is
|
||||
* consumed by:
|
||||
* - this module (resolves iconKey -> IconType for the icon grid)
|
||||
* - LLMProvidersTable React component (renders the default-models table)
|
||||
* - scripts/generate-docs-skill.sh (renders <LLMProvidersTable /> as
|
||||
* markdown when copying MDX docs into the agent-facing skill)
|
||||
*
|
||||
* Keep aligned with PROVIDER_DEFAULT_MODELS in
|
||||
* hindsight-api-slim/hindsight_api/config.py.
|
||||
*/
|
||||
export const LLM_PROVIDERS: LLMProvider[] = (providersJson as Array<{
|
||||
id: string; label: string; iconKey: string; defaultModel?: string; defaultModelNote?: string;
|
||||
}>).map(({iconKey, ...rest}) => {
|
||||
const icon = ICON_REGISTRY[iconKey];
|
||||
if (!icon) throw new Error(`Unknown iconKey "${iconKey}" for provider "${rest.id || rest.label}"`);
|
||||
return {...rest, icon};
|
||||
});
|
||||
@@ -8,6 +8,12 @@ import PageHero from '@site/src/components/PageHero';
|
||||
|
||||
[← OpenClaw integration](/sdks/integrations/openclaw)
|
||||
|
||||
## [0.6.6](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.6)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- Retention is now applied correctly for default agent main:main sessions instead of being silently skipped.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/nicoloboschi" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}><img src="https://github.com/nicoloboschi.png?size=40" alt="@nicoloboschi" width="18" height="18" style={{borderRadius: "50%"}} />@nicoloboschi</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/70677457" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>70677457</a>
|
||||
|
||||
## [0.6.5](https://github.com/vectorize-io/hindsight/tree/integrations/openclaw/v0.6.5)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||
# Pipecat Integration Changelog
|
||||
|
||||
Changelog for [`hindsight-pipecat`](https://pypi.org/project/hindsight-pipecat/).
|
||||
|
||||
For the source code, see [`hindsight-integrations/pipecat`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/pipecat).
|
||||
|
||||
← [Back to main changelog](/changelog)
|
||||
|
||||
## [0.1.1](https://github.com/vectorize-io/hindsight/tree/integrations/pipecat/v0.1.1)
|
||||
|
||||
**Features**
|
||||
|
||||
- Added Pipecat voice AI pipeline integration to store and use memories in Hindsight.<span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/benfrank241" target="_blank" rel="noopener noreferrer" style={{color: "var(--ifm-color-primary)", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "4px", verticalAlign: "middle"}}><img src="https://github.com/benfrank241.png?size=40" alt="@benfrank241" width="18" height="18" style={{borderRadius: "50%"}} />@benfrank241</a><span style={{color: "var(--ifm-color-emphasis-500)", margin: "0 0.3em"}}>·</span><a href="https://github.com/vectorize-io/hindsight/commit/f7cc9ad6" target="_blank" rel="noopener noreferrer" style={{fontFamily: "var(--ifm-font-family-monospace, monospace)", fontSize: "0.85em", color: "var(--ifm-color-emphasis-600)"}}>f7cc9ad6</a>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 329 B |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -2594,7 +2594,7 @@
|
||||
"Memory"
|
||||
],
|
||||
"summary": "List tags",
|
||||
"description": "List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive.",
|
||||
"description": "List all unique tags in a memory bank with usage counts. Supports wildcard search using '*' (e.g., 'user:*', '*-fred', 'tag*-2'). Case-insensitive. Use `source=mental_models` to list tags used on mental models instead of memories.",
|
||||
"operationId": "list_tags",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2624,6 +2624,22 @@
|
||||
},
|
||||
"description": "Wildcard pattern to filter tags (e.g., 'user:*' for user:alice, '*-admin' for role-admin). Use '*' as wildcard. Case-insensitive."
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"memories",
|
||||
"mental_models"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Where to read tags from: 'memories' (memory_units, default) or 'mental_models'.",
|
||||
"default": "memories",
|
||||
"title": "Source"
|
||||
},
|
||||
"description": "Where to read tags from: 'memories' (memory_units, default) or 'mental_models'."
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
@@ -3102,7 +3118,7 @@
|
||||
"Banks"
|
||||
],
|
||||
"summary": "Get memory bank profile",
|
||||
"description": "Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.",
|
||||
"description": "Get disposition traits and mission for a memory bank. Returns 404 if the bank does not exist.",
|
||||
"operationId": "get_bank_profile",
|
||||
"deprecated": true,
|
||||
"parameters": [
|
||||
|
||||
@@ -232,6 +232,11 @@ hindsight document list my-bank --tags team-a --tags team-b
|
||||
"original_text": "Alice presented the Q4 roadmap...",
|
||||
"content_hash": "abc123def456",
|
||||
"memory_unit_count": 12,
|
||||
"nodes_by_fact_type": {
|
||||
"world": 5,
|
||||
"experience": 4,
|
||||
"observation": 3
|
||||
},
|
||||
"created_at": "2024-03-15T14:00:00Z",
|
||||
"updated_at": "2024-03-15T14:00:00Z"
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ An allowlist of MCP tool names that are enabled for this bank. When set, only th
|
||||
["recall", "reflect"]
|
||||
```
|
||||
|
||||
Available tool names: `retain`, `recall`, `reflect`, `list_banks`, `create_bank`, `list_mental_models`, `get_mental_model`, `create_mental_model`, `update_mental_model`, `delete_mental_model`, `refresh_mental_model`, `list_directives`, `create_directive`, `delete_directive`, `list_memories`, `get_memory`, `delete_memory`, `list_documents`, `get_document`, `delete_document`, `list_operations`, `get_operation`, `cancel_operation`, `list_tags`, `get_bank`, `get_bank_stats`, `update_bank`, `delete_bank`, `clear_memories`.
|
||||
Available tool names: `retain`, `recall`, `reflect`, `list_banks`, `create_bank`, `list_mental_models`, `get_mental_model`, `create_mental_model`, `update_mental_model`, `delete_mental_model`, `refresh_mental_model`, `list_directives`, `create_directive`, `delete_directive`, `list_memories`, `get_memory`, `list_documents`, `get_document`, `delete_document`, `list_operations`, `get_operation`, `cancel_operation`, `list_tags`, `get_bank`, `get_bank_stats`, `update_bank`, `delete_bank`, `clear_memories`.
|
||||
|
||||
### llm_gemini_safety_settings
|
||||
|
||||
|
||||
@@ -85,8 +85,10 @@ Response:
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `pending` | Operation is queued and waiting to be processed |
|
||||
| `processing` | Operation is actively being processed by a worker |
|
||||
| `completed` | Operation finished successfully |
|
||||
| `failed` | Operation failed (check `error_message` for details) |
|
||||
| `cancelled` | Operation was cancelled via the DELETE endpoint before processing |
|
||||
|
||||
## Managing Operations
|
||||
|
||||
@@ -113,7 +115,7 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
The operation status resets to `pending` and the worker picks it up again. Returns `409` if the operation is not in `failed` state.
|
||||
The operation status resets to `pending` and the worker picks it up again. Returns `409` if the operation is not in `failed` or `cancelled` state.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ Migrations will automatically create the schema if it doesn't exist and create a
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_DB_POOL_MIN_SIZE` | Minimum connections in the pool | `5` |
|
||||
| `HINDSIGHT_API_DB_POOL_MAX_SIZE` | Maximum connections in the pool | `100` |
|
||||
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds | `60` |
|
||||
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds (asyncpg client-side) | `60` |
|
||||
| `HINDSIGHT_API_DB_ACQUIRE_TIMEOUT` | Connection acquisition timeout in seconds | `30` |
|
||||
| `HINDSIGHT_API_DB_STATEMENT_TIMEOUT` | Postgres `statement_timeout` applied to every pool connection, in seconds. Server-side safety net for runaway queries. Does **not** apply to Alembic migrations (which run on a separate psycopg2 engine). Set to `0` to disable. | `600` |
|
||||
|
||||
For high-concurrency workloads, increase `DB_POOL_MAX_SIZE`. Each concurrent recall/think operation can use 2-4 connections.
|
||||
|
||||
@@ -407,6 +408,7 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY` | Cohere API key for embeddings | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL` | Cohere embedding model | `embed-english-v3.0` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_OUTPUT_DIMENSIONS` | Output embedding dimensions for Cohere (e.g., `256`, `512`, `1024`). When set, overrides the model's default dimension. | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE` | LiteLLM proxy base URL for embeddings | `http://localhost:4000` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY` | LiteLLM proxy API key for embeddings (optional, depends on proxy config) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL` | LiteLLM embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `text-embedding-3-small` |
|
||||
@@ -418,6 +420,7 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
|
||||
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_API_KEY` | Gemini API key for embeddings (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_MODEL` | Gemini embedding model | `gemini-embedding-001` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_OUTPUT_DIMENSIONALITY` | Output embedding dimensions (Gemini supports configurable dimensionality) | `768` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_GEMINI_FORCE_IPV4` | Force the Gemini embeddings client to use an IPv4-only HTTP transport. Useful in environments where IPv6 egress is broken (e.g. some Docker/VPC setups) and AAAA DNS records cause long hangs. | `false` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_VERTEXAI_PROJECT_ID` | Vertex AI project ID for embeddings (falls back to `HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID`) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_VERTEXAI_REGION` | Vertex AI region for embeddings (falls back to `HINDSIGHT_API_LLM_VERTEXAI_REGION`) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_VERTEXAI_SERVICE_ACCOUNT_KEY` | Service account key for Vertex AI embeddings (falls back to `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY`) | - |
|
||||
@@ -456,6 +459,8 @@ export HINDSIGHT_API_EMBEDDINGS_OPENROUTER_MODEL=perplexity/pplx-embed-v1-0.6b
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions
|
||||
# Optional: override output dimensions (for Matryoshka-capable models)
|
||||
# export HINDSIGHT_API_EMBEDDINGS_COHERE_OUTPUT_DIMENSIONS=512
|
||||
|
||||
# Azure-hosted Cohere - embeddings via custom endpoint
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
@@ -556,6 +561,7 @@ Google's `gemini-embedding-001` produces 3072 dimensions natively but supports c
|
||||
| `HINDSIGHT_API_RERANKER_GOOGLE_SERVICE_ACCOUNT_KEY` | Path to service account JSON key (falls back to `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY`). If unset, uses ADC. | - |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_MODEL` | FlashRank model for fast CPU-based reranking | `ms-marco-MiniLM-L-12-v2` |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR` | Cache directory for FlashRank models | System default |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_CPU_MEM_ARENA` | Enable ONNX Runtime CPU memory arena for FlashRank. When `true`, ONNX pre-allocates a memory arena that never shrinks, causing RSS to grow monotonically. `false` trades slightly slower per-call allocation for bounded RSS. | `false` |
|
||||
| `HINDSIGHT_API_RERANKER_JINA_MLX_MODEL_PATH` | Local path to downloaded `jina-reranker-v3-mlx` model (auto-downloads from HuggingFace if unset) | - |
|
||||
|
||||
```bash
|
||||
@@ -1021,7 +1027,8 @@ Observations are deduplicated, evidence-grounded knowledge consolidated from mul
|
||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND` | Maximum memories processed per consolidation round. When the limit is reached, the job yields its worker slot and re-queues itself so other banks get fair scheduling. Mental model refreshes only run on the final round. `0` = unlimited. Configurable per bank. | `100` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE` | Number of facts sent to the LLM in a single consolidation call. Higher values reduce LLM calls and improve throughput at the cost of larger prompts. Set to `1` to disable batching. Configurable per bank. | `8` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` | Total token budget for source facts included with observations in the consolidation prompt. `-1` = unlimited. Configurable per bank. | `-1` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_RECALL_BUDGET` | Budget level for the recall pass inside consolidation (`low`, `mid`, `high`). Lower budgets fetch fewer candidate rows, reducing peak memory usage on large banks. | `low` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` | Total token budget for source facts included with observations in the consolidation prompt. `-1` = unlimited. Configurable per bank. | `4096` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION` | Per-observation token cap for source facts in the consolidation prompt. Each observation independently gets at most this many tokens of source facts. `-1` = unlimited. Configurable per bank. | `256` |
|
||||
| `HINDSIGHT_API_OBSERVATIONS_MISSION` | What this bank should synthesise into durable observations. Replaces the built-in consolidation rules — leave unset to use the server default. | - |
|
||||
| `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE` | Maximum number of observations allowed per tag scope. When the limit is reached, consolidation will only update or delete existing observations — no new ones are created. Applies per tag scope (e.g., per-tag when using `per_tag` observation scopes). Observations with no tags are not subject to this limit. `-1` = unlimited. Configurable per bank. | `-1` |
|
||||
@@ -1120,7 +1127,7 @@ export HINDSIGHT_API_MCP_ENABLED_TOOLS=recall
|
||||
export HINDSIGHT_API_MCP_ENABLED_TOOLS=recall,reflect
|
||||
```
|
||||
|
||||
Available tool names: `retain`, `recall`, `reflect`, `list_banks`, `create_bank`, `list_mental_models`, `get_mental_model`, `create_mental_model`, `update_mental_model`, `delete_mental_model`, `refresh_mental_model`, `list_directives`, `create_directive`, `delete_directive`, `list_memories`, `get_memory`, `delete_memory`, `list_documents`, `get_document`, `delete_document`, `list_operations`, `get_operation`, `cancel_operation`, `list_tags`, `get_bank`, `get_bank_stats`, `update_bank`, `delete_bank`, `clear_memories`.
|
||||
Available tool names: `retain`, `recall`, `reflect`, `list_banks`, `create_bank`, `list_mental_models`, `get_mental_model`, `create_mental_model`, `update_mental_model`, `delete_mental_model`, `refresh_mental_model`, `list_directives`, `create_directive`, `delete_directive`, `list_memories`, `get_memory`, `list_documents`, `get_document`, `delete_document`, `list_operations`, `get_operation`, `cancel_operation`, `list_tags`, `get_bank`, `get_bank_stats`, `update_bank`, `delete_bank`, `clear_memories`.
|
||||
|
||||
This can also be overridden per bank via the [config API](#hierarchical-configuration):
|
||||
|
||||
@@ -1162,10 +1169,17 @@ Configuration for background task processing. By default, the API processes task
|
||||
| `HINDSIGHT_API_WORKER_MAX_RETRIES` | Max retries before marking task failed | `3` |
|
||||
| `HINDSIGHT_API_WORKER_HTTP_PORT` | HTTP port for worker metrics/health (worker CLI only) | `8889` |
|
||||
| `HINDSIGHT_API_WORKER_MAX_SLOTS` | Maximum concurrent tasks per worker (total across all operation types) | `10` |
|
||||
| `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS` | Slots reserved for consolidation tasks within `WORKER_MAX_SLOTS` | `2` |
|
||||
| `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS` | Reserved slots for consolidation tasks within `WORKER_MAX_SLOTS` (bank-serialization preserved) | `2` |
|
||||
| `HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS` | Reserved slots for retain tasks within `WORKER_MAX_SLOTS` | `0` |
|
||||
| `HINDSIGHT_API_WORKER_FILE_CONVERT_RETAIN_MAX_SLOTS` | Reserved slots for file_convert_retain tasks within `WORKER_MAX_SLOTS` | `0` |
|
||||
| `HINDSIGHT_API_WORKER_REFRESH_MENTAL_MODEL_MAX_SLOTS` | Reserved slots for refresh_mental_model tasks within `WORKER_MAX_SLOTS` | `0` |
|
||||
|
||||
:::note Slot reservation
|
||||
`WORKER_CONSOLIDATION_MAX_SLOTS` is a **reservation within** `WORKER_MAX_SLOTS`, not an additive pool. With the defaults (`MAX_SLOTS=10`, `CONSOLIDATION_MAX_SLOTS=2`), retain and other non-consolidation tasks may use at most `10 - 2 = 8` concurrent slots, leaving 2 always available for consolidation. This prevents consolidation from being starved when retain throughput continuously saturates the queue. Set `CONSOLIDATION_MAX_SLOTS=0` to give all slots to non-consolidation work.
|
||||
:::note Slot reservations and shared pool
|
||||
Per-operation `*_MAX_SLOTS` values are **reservations within** `WORKER_MAX_SLOTS`, not additive pools. The sum of all reservations must not exceed `WORKER_MAX_SLOTS` (startup raises `ValueError` otherwise). Remaining capacity (`WORKER_MAX_SLOTS - sum of reservations`) forms a **shared pool** usable by any operation type on a first-come basis; operation types whose reserved capacity is full can also overflow into the shared pool. Consolidation's bank-serialization constraint (no two consolidation tasks for the same bank concurrently) is preserved regardless of which pool claims the slot.
|
||||
|
||||
Example: `MAX_SLOTS=10, CONSOLIDATION=2, RETAIN=3, REFRESH_MENTAL_MODEL=2` → shared pool = `10 - (2+3+2) = 3`.
|
||||
|
||||
With the defaults (`MAX_SLOTS=10`, `CONSOLIDATION_MAX_SLOTS=2`, all other reservations `0`), 2 slots are always reserved for consolidation and the remaining 8 form the shared pool for any operation type. Set `CONSOLIDATION_MAX_SLOTS=0` to release consolidation's reserved capacity into the shared pool.
|
||||
:::
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
@@ -45,6 +45,24 @@ Configure which one to use with `HINDSIGHT_API_VECTOR_EXTENSION`. See [Configura
|
||||
|
||||
You need an LLM API key for fact extraction, entity resolution, and answer generation. See [Models](./models) for supported providers, model recommendations, and configuration.
|
||||
|
||||
### Hardware
|
||||
|
||||
Hindsight is designed to run on commodity hardware. The footprint depends mainly on whether the **full** image (which bundles local embedding and reranker models) or the **slim** image (which delegates those to external providers) is used.
|
||||
|
||||
| Component | Minimum RAM | Recommended RAM | Notes |
|
||||
|-----------|-------------|-----------------|-------|
|
||||
| **API — Full image** | 1.5 GB | 2 GB | Loads local BGE embedder (~130 MB) and MiniLM cross-encoder (~90 MB) into memory, plus PyTorch/ONNX runtime arenas. Idle RSS settles around 0.8–1.0 GB; expect 1.2–1.5 GB under load. |
|
||||
| **API — Slim image** | 512 MB | 1 GB | No local models. Steady-state RSS is dominated by Python runtime and DB connections. Requires [external embedding and reranker providers](./configuration#embeddings) (e.g. TEI, OpenAI, Cohere). |
|
||||
| **Control Plane (UI)** | 128 MB | 256 MB | Next.js process, lightweight. |
|
||||
| **Worker** (if separated) | Same as API image variant | Same as API image variant | Workers load the same models as the API server. |
|
||||
| **PostgreSQL** | 512 MB | 1 GB+ | Scales with the number of memories and indexes. |
|
||||
|
||||
:::tip Reducing the footprint
|
||||
The bulk of the full image's memory comes from the bundled embedding and reranker models and their PyTorch/ONNX runtimes. To shrink the deployment to a few hundred MB of RAM, switch to the **slim** image and configure [external embedding and reranker providers](./configuration#embeddings).
|
||||
:::
|
||||
|
||||
CPU vs GPU: 2 vCPUs on CPU-only is fine for development and basic workloads. For production traffic, the local reranker (cross-encoder) is the main bottleneck and typically benefits from a GPU to keep recall latency reasonable; alternatively, offload reranking to an [external reranker provider](./configuration#embeddings) (e.g. TEI, Cohere) on dedicated GPU hardware.
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -371,16 +371,6 @@ Retrieve a specific memory by ID.
|
||||
|
||||
---
|
||||
|
||||
### delete_memory
|
||||
|
||||
Permanently delete a specific memory.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `memory_id` | string | Yes | The ID of the memory to delete |
|
||||
|
||||
---
|
||||
|
||||
### list_documents
|
||||
|
||||
List documents that have been ingested into the memory bank.
|
||||
|
||||
@@ -227,6 +227,22 @@
|
||||
"icon": "/img/icons/mcp.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"href": "/sdks/integrations/chatgpt",
|
||||
"label": "ChatGPT",
|
||||
"customProps": {
|
||||
"icon": "/img/icons/chatgpt.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"href": "/sdks/integrations/perplexity",
|
||||
"label": "Perplexity",
|
||||
"customProps": {
|
||||
"icon": "/img/icons/perplexity.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"href": "/sdks/integrations/litellm",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user