Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
25ca320
feat(text-embedding): add text embedding function with Ollama
theothersideofgod May 16, 2026
88ed26e
fix: update lockfile for text-embedding generated package
theothersideofgod May 16, 2026
2b462b5
feat(rag-embedding): add RAG embedding function with chunking strategies
theothersideofgod May 16, 2026
97ba6b6
fix(ci): add SDK utilities and constructive-server port-forward for e…
theothersideofgod May 16, 2026
8807a07
fix(fn-runtime): cast graphql-request client types for compatibility
theothersideofgod May 20, 2026
9022c8e
feat(rag-embedding): support extractedText field from ProcessFileEmbe…
theothersideofgod May 20, 2026
e2d725d
feat(text-embedding): implement full pipeline with GraphQL introspection
theothersideofgod May 20, 2026
1e50f1a
fix(e2e): use node http module for Host header in GraphQL requests
theothersideofgod May 20, 2026
2cab451
test(e2e): add simplified RAG embedding integration test
theothersideofgod May 20, 2026
8ff7a43
chore(deps): update lockfile for text-embedding dependencies
theothersideofgod May 20, 2026
1b0c250
fix(docker): add pnpm install after generate in Dockerfile.dev
theothersideofgod May 20, 2026
235a8be
test(e2e): add text-embedding test for SearchVector embedding flow
theothersideofgod May 20, 2026
dcd26d7
chore(e2e): remove unstable rag-embedding tests
theothersideofgod May 20, 2026
4b5548a
test(text-embedding): update unit tests for SearchVector handler API
theothersideofgod May 21, 2026
80c6eef
chore(deps): add @constructive-io/node SDK
theothersideofgod May 22, 2026
4ba4da8
feat(e2e): add endpoint config
theothersideofgod May 22, 2026
31d85e2
feat(e2e): add SDK-based provision utilities
theothersideofgod May 22, 2026
7700972
feat(e2e): add waitForJobCreated helper
theothersideofgod May 22, 2026
74b8e54
test(e2e): add combined embedding test
theothersideofgod May 22, 2026
19c4037
fix(k8s): increase admin server memory to 1Gi
theothersideofgod May 22, 2026
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: 3 additions & 1 deletion .github/workflows/test-k8s-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,17 @@ jobs:
kubectl rollout restart deploy/knative-job-service -n constructive-functions
kubectl rollout status deploy/knative-job-service -n constructive-functions --timeout=120s

- name: Port-forward postgres and run e2e tests
- name: Port-forward services and run e2e tests
env:
PGHOST: localhost
PGPORT: "5432"
PGUSER: postgres
PGPASSWORD: ${{ env.PG_PASSWORD }}
PGDATABASE: constructive
SERVER_PORT: "3002"
run: |
kubectl port-forward -n constructive-functions svc/postgres 5432:5432 &
kubectl port-forward -n constructive-functions svc/constructive-server 3002:3000 &
sleep 3
pnpm jest tests/e2e/__tests__/job-queue.test.ts tests/e2e/__tests__/${{ matrix.name }}.e2e.test.ts

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ RUN node --experimental-strip-types scripts/generate.ts --packages-only && pnpm

# Copy source and build
COPY . .
RUN pnpm generate && pnpm build
RUN pnpm generate && pnpm install --frozen-lockfile && pnpm build

ENV NODE_ENV=development
172 changes: 172 additions & 0 deletions functions/rag-embedding/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
const mockRequest = jest.fn();

jest.mock('graphql-request', () => ({
gql: jest.fn((strings: TemplateStringsArray) => strings.join('')),
}));

jest.mock('@agentic-kit/ollama', () => {
return jest.fn().mockImplementation(() => ({
generateEmbedding: jest.fn().mockResolvedValue(Array(768).fill(0.1)),
}));
});

jest.mock('@constructive-io/graphql-query', () => ({
SCHEMA_INTROSPECTION_QUERY: 'query { __schema { types { name } } }',
inferTablesFromIntrospection: jest.fn().mockReturnValue([
{
name: 'article',
query: { all: 'articles', create: 'createArticle' },
inflection: { allRows: 'articles', tableFieldName: 'article' },
primaryKey: 'id',
relations: { belongsTo: [], hasMany: [] },
},
{
name: 'article_chunk',
query: { all: 'articleChunks', create: 'createArticleChunk' },
inflection: { allRows: 'articleChunks', tableFieldName: 'articleChunk', createField: 'createArticleChunk' },
primaryKey: 'id',
relations: {
belongsTo: [{ referencesTable: 'article', fieldName: 'article' }],
hasMany: [],
},
},
]),
buildSelect: jest.fn().mockReturnValue({ toString: () => 'query { articles { nodes { id content } } }' }),
buildPostGraphileCreate: jest.fn().mockReturnValue({ toString: () => 'mutation { createArticleChunk { articleChunk { id } } }' }),
buildPostGraphileDelete: jest.fn().mockReturnValue({ toString: () => 'mutation { deleteArticleChunks { deletedCount } }' }),
}));

const createMockContext = () => ({
job: {
jobId: 'test-job-id',
workerId: 'test-worker',
databaseId: 'test-db',
},
client: {
request: mockRequest,
},
meta: {
request: jest.fn().mockResolvedValue({
schemas: { nodes: [{ schemaName: 'test-db-app-public' }] },
}),
},
log: {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
},
env: {
RAG_EMBEDDING_DRY_RUN: 'true',
GRAPHQL_URL: 'http://localhost:3000/graphql',
GRAPHQL_API_NAME: 'private',
},
});

const loadHandler = () => {
const mod = require('../handler');
return mod.default ?? mod;
};

describe('rag-embedding handler', () => {
beforeEach(() => {
jest.clearAllMocks();
mockRequest.mockReset();
});

it('should return early when content is empty', async () => {
const handler = loadHandler();
const context = createMockContext();

mockRequest
.mockResolvedValueOnce({ __schema: { types: [] } }) // introspection
.mockResolvedValueOnce({
articles: { nodes: [{ id: 'test-id', content: '' }] },
});

const result = await handler(
{
table: 'article',
schema: 'test-db-app-public',
id: 'test-id',
chunks_table: 'article_chunk',
},
context
);
Comment on lines +86 to +94
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests are invoking the handler with legacy param names (e.g. table_name, record_id, content_field) but functions/rag-embedding/handler.ts currently requires the trigger payload shape (table, schema, id, chunks_table, etc.). As written, these tests will fail early with "Missing required params". Update the test inputs (and mocked GraphQL responses) to match the handler’s expected payload, or add a backward-compatible param mapping in the handler.

Copilot uses AI. Check for mistakes.

expect(result.complete).toBe(true);
expect(result.chunks).toBe(0);
});

it('should chunk content and create embeddings', async () => {
const handler = loadHandler();
const context = createMockContext();

mockRequest
.mockResolvedValueOnce({ __schema: { types: [] } }) // introspection
.mockResolvedValueOnce({
articles: { nodes: [{ id: 'test-id', content: 'This is test content for chunking.' }] },
})
.mockResolvedValueOnce({ deleteArticleChunks: { deletedCount: 0 } })
.mockResolvedValueOnce({
createArticleChunk: { articleChunk: { id: 'chunk-1' } },
});

const result = await handler(
{
table: 'article',
schema: 'test-db-app-public',
id: 'test-id',
chunks_table: 'article_chunk',
chunk_size: '1000',
},
context
);

expect(result.complete).toBe(true);
expect(result.chunks).toBe(1);
expect(result.chunk_ids).toHaveLength(1);
});

it('should throw error when databaseId is missing', async () => {
const handler = loadHandler();
const context = createMockContext();
context.job.databaseId = undefined;

await expect(
handler({ table: 'article', schema: 'test-db-app-public', id: 'test-id', chunks_table: 'article_chunk' }, context)
).rejects.toThrow('Missing X-Database-Id');
});

it('should throw error when required params are missing', async () => {
const handler = loadHandler();
const context = createMockContext();

await expect(
handler({ table: '', schema: 'test-db-app-public', id: 'test-id', chunks_table: 'article_chunk' }, context)
).rejects.toThrow('Missing required params');
});

it('should return early when content is empty with trigger format', async () => {
const handler = loadHandler();
const context = createMockContext();

mockRequest
.mockResolvedValueOnce({ __schema: { types: [] } }) // introspection
.mockResolvedValueOnce({
articles: { nodes: [{ id: 'test-id', content: '' }] },
});

const result = await handler(
{
table: 'article',
schema: 'test-db-app-public',
id: 'test-id',
chunks_table: 'article_chunk',
},
context
);

expect(result.complete).toBe(true);
expect(result.chunks).toBe(0);
});
});
15 changes: 15 additions & 0 deletions functions/rag-embedding/handler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "rag-embedding",
"version": "1.0.0",
"type": "node-graphql",
"port": 8085,
"taskIdentifier": "generate_chunks",
"description": "RAG embedding function - chunks text and generates embeddings using Ollama nomic-embed-text",
"dependencies": {
"@agentic-kit/ollama": "^1.0.3",
"@constructive-io/graphql-query": "^3.12.14",
"@pgpmjs/env": "^2.11.0",
"@pgpmjs/logger": "^2.1.0",
"graphql-request": "^7.1.2"
}
}
Loading
Loading