Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions recipes/x-to-brain.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ secrets:
where: https://developer.x.com/en/portal/dashboard — create a project + app, copy the Bearer Token from "Keys and tokens"
health_checks:
- type: http
url: "https://api.x.com/2/users/me"
url: "https://api.x.com/2/users/by/username/X"
auth: bearer
auth_token: "$X_BEARER_TOKEN"
label: "X API"
label: "X API bearer auth"
setup_time: 15 min
cost_estimate: "$0-200/mo (Free tier: 1 app, read-only. Basic: $200/mo for search + higher limits)"
---
Expand Down
5 changes: 3 additions & 2 deletions src/core/postgres-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
} from './types.ts';
import { GBrainError } from './types.ts';
import * as db from './db.ts';
import { validateSlug, contentHash, rowToPage, rowToChunk, rowToSearchResult } from './utils.ts';
import { validateSlug, contentHash, parseEmbedding, rowToPage, rowToChunk, rowToSearchResult } from './utils.ts';

export class PostgresEngine implements BrainEngine {
private _sql: ReturnType<typeof postgres> | null = null;
Expand Down Expand Up @@ -275,7 +275,8 @@ export class PostgresEngine implements BrainEngine {
`;
const result = new Map<number, Float32Array>();
for (const row of rows) {
if (row.embedding) result.set(row.id as number, row.embedding as Float32Array);
const embedding = parseEmbedding(row.embedding);
if (embedding) result.set(row.id as number, embedding);
}
return result;
}
Expand Down
22 changes: 21 additions & 1 deletion src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { createHash } from 'crypto';
import type { Page, PageInput, PageType, Chunk, SearchResult } from './types.ts';

export function parseEmbedding(value: unknown): Float32Array | null {
if (!value) return null;

if (value instanceof Float32Array) return value;

if (Array.isArray(value)) {
return new Float32Array(value.map(Number));
}

if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return null;
const body = trimmed.slice(1, -1).trim();
if (!body) return new Float32Array();
return new Float32Array(body.split(',').map(part => Number(part.trim())));
}

return null;
}

/**
* Validate and normalize a slug. Slugs are lowercased repo-relative paths.
* Rejects empty slugs, path traversal (..), and leading /.
Expand Down Expand Up @@ -50,7 +70,7 @@ export function rowToChunk(row: Record<string, unknown>, includeEmbedding = fals
chunk_index: row.chunk_index as number,
chunk_text: row.chunk_text as string,
chunk_source: row.chunk_source as 'compiled_truth' | 'timeline',
embedding: includeEmbedding && row.embedding ? row.embedding as Float32Array : null,
embedding: includeEmbedding ? parseEmbedding(row.embedding) : null,
model: row.model as string,
token_count: row.token_count as number | null,
embedded_at: row.embedded_at ? new Date(row.embedded_at as string) : null,
Expand Down
36 changes: 35 additions & 1 deletion test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect } from 'bun:test';
import { validateSlug, contentHash, rowToPage, rowToChunk, rowToSearchResult } from '../src/core/utils.ts';
import { validateSlug, contentHash, parseEmbedding, rowToPage, rowToChunk, rowToSearchResult } from '../src/core/utils.ts';

describe('validateSlug', () => {
test('accepts valid slugs', () => {
Expand Down Expand Up @@ -98,6 +98,40 @@ describe('rowToChunk', () => {
}, true);
expect(chunk.embedding).not.toBeNull();
});

test('parses pgvector string embeddings when requested', () => {
const chunk = rowToChunk({
id: 1, page_id: 1, chunk_index: 0, chunk_text: 'text',
chunk_source: 'compiled_truth', embedding: '[0.1, 0.2, 0.3]',
model: 'test', token_count: 5, embedded_at: '2024-01-01',
}, true);
expect(chunk.embedding).toBeInstanceOf(Float32Array);
expect(Array.from(chunk.embedding || [])).toHaveLength(3);
expect(chunk.embedding?.[0]).toBeCloseTo(0.1, 6);
expect(chunk.embedding?.[1]).toBeCloseTo(0.2, 6);
expect(chunk.embedding?.[2]).toBeCloseTo(0.3, 6);
});
});

describe('parseEmbedding', () => {
test('returns Float32Array unchanged', () => {
const emb = new Float32Array([0.1, 0.2]);
expect(parseEmbedding(emb)).toBe(emb);
});

test('parses pgvector text into Float32Array', () => {
const parsed = parseEmbedding('[0.1, 0.2, 0.3]');
expect(parsed).toBeInstanceOf(Float32Array);
expect(Array.from(parsed || [])).toHaveLength(3);
expect(parsed?.[0]).toBeCloseTo(0.1, 6);
expect(parsed?.[1]).toBeCloseTo(0.2, 6);
expect(parsed?.[2]).toBeCloseTo(0.3, 6);
});

test('returns null for unsupported embedding values', () => {
expect(parseEmbedding(null)).toBeNull();
expect(parseEmbedding('not-a-vector')).toBeNull();
});
});

describe('rowToSearchResult', () => {
Expand Down