Skip to content

feat: add graphile-llm plugin — embedding, RAG, and text-to-vector for PostGraphile#1008

Merged
pyramation merged 5 commits intomainfrom
devin/1776579383-graphile-llm-plugin
Apr 19, 2026
Merged

feat: add graphile-llm plugin — embedding, RAG, and text-to-vector for PostGraphile#1008
pyramation merged 5 commits intomainfrom
devin/1776579383-graphile-llm-plugin

Conversation

@pyramation
Copy link
Copy Markdown
Contributor

@pyramation pyramation commented Apr 19, 2026

Summary

Adds the graphile-llm package under graphile/graphile-llm/. This is a standalone PostGraphile v5 plugin (not added to ConstructivePreset) that brings server-side text-to-vector embedding, resolve-time vector injection, and RAG (Retrieval-Augmented Generation) capabilities for pgvector columns using @agentic-kit/ollama.

What's included:

  • EmbedderFunction / EmbedderConfig types and a provider-based buildEmbedder() abstraction (Ollama built-in, extensible for OpenAI etc.)
  • ChatFunction / ChatConfig types and a provider-based buildChatCompleter() abstraction for chat/completion (Ollama via @agentic-kit/ollama)
  • LlmModulePlugin — resolves both embedder and chat completer from preset options or env vars and stores them on the Graphile build context
  • LlmTextSearchPlugin — adds text: String to VectorNearbyInput with resolve-time embedding via v4-style resolver wrapping (intercepts where args, embeds text, injects vector before pgvector processes the query)
  • LlmTextMutationPlugin — adds {column}Text: String companion fields on create/update inputs for vector columns with resolve-time embedding (wraps mutation resolvers, intercepts *Text fields, embeds and injects vectors)
  • LlmRagPlugin — detects @hasChunks smart tags on tables, adds ragQuery(prompt, contextLimit, minSimilarity, systemPrompt) and embedText(text) root query fields. RAG flow: embed prompt → search chunks across all @hasChunks tables → assemble context → call chat LLM → return answer with sources
  • GraphileLlmPreset() — factory function that bundles all plugins with enableTextSearch / enableTextMutations / enableRag toggles
  • Token tracking deferred to future metering work (debug logging only)

Tests (31 total across 6 suites):

  • Embedder abstraction unit tests (7 tests) — buildEmbedder(), buildEmbedderFromModule(), buildEmbedderFromEnv()
  • Schema enrichment tests (4 tests) — PostGraphile schema with all LLM plugins against real PostgreSQL + pgvector; asserts text on VectorNearbyInput and embeddingText on mutation inputs. Requires PostgreSQL.
  • Real Ollama embedding tests (4 tests) — nomic-embed-text via OllamaClient, verifies 768-dim vectors and semantic similarity. Requires Ollama.
  • Chat completion abstraction unit tests (7 tests) — buildChatCompleter(), buildChatCompleterFromModule(), buildChatCompleterFromEnv()
  • RAG plugin integration tests (5 tests) — builds schema with @hasChunks smart tags, mock embedder/chat, executes ragQuery and embedText GraphQL queries against real PostgreSQL with seeded chunks table. Verifies schema types (RagResponse, RagSource, EmbedTextResponse), answer content, and source metadata. Requires PostgreSQL.
  • Preset toggle tests (6 tests) — verifies enableTextSearch, enableTextMutations, enableRag toggles correctly include/exclude plugins

Tests do not gracefully skip. If PostgreSQL or Ollama are missing, the relevant suites will fail — this is intentional per project convention.

Refs: constructive-io/constructive-planning#743

Updates since last revision

  • Added RAG plugin (rag-plugin.ts) — discovers @hasChunks tables via pgRegistry scanning, adds ragQuery and embedText root query fields using extendSchema + grafast lambda pattern for async operations at execution time
  • Added chat completion provider (chat.ts) — mirrors embedder abstraction; creates Ollama chat completers via OllamaClient.generate() with messages array
  • Wired resolve-time embedding in text-search-plugin — v4-style resolver wrapping on root query fields; recursively walks where/filter args to find VectorNearbyInput values with text, embeds them, and replaces with vectors before plan execution
  • Wired resolve-time embedding in text-mutation-plugin — v4-style resolver wrapping on create/update mutations; intercepts *Text companion field values, embeds them, injects vectors, removes consumed *Text keys
  • Updated llm-module-plugin — now resolves both embedder and chat completer on build, stores as build.llmEmbedder and build.llmChatCompleter
  • Added enableRag toggle to GraphileLlmPreset (default false; text search and mutations default true)
  • Added graphile-utils peer dependency (optional, for makeExtendSchemaPlugin)
  • Added RAG typesChatFunction, ChatConfig, ChatMessage, ChatOptions, RagDefaults, ChunkTableInfo
  • Expanded test seedarticles_chunks table with 6 chunks linked to articles for RAG integration tests
  • Added 16 new tests across 3 new suites (chat completion, RAG integration, preset toggles)

Review & Testing Checklist for Human

  • Resolver wrapping approach (v4-style) for both search and mutation plugins. The GraphQLObjectType_fields_field hook wraps resolve functions and mutates args objects before calling the original resolver. This is the pattern used by graphile-upload-plugin and graphile-bucket-provisioner-plugin, but verify it works correctly with Grafast v5's plan-based execution. If Grafast compiles plans before resolve is called, the injected vectors may not reach the SQL layer.
  • RAG plugin SQL query construction. buildChunkSearchSql() constructs raw SQL with table/column names from @hasChunks smart tag config. The names come from trusted codec metadata (not user input), but review for correctness — especially the 1 - (embedding <=> $1::vector) distance calculation and ORDER BY embedding <=> $1::vector ordering.
  • embedTextInWhere heuristic for detecting VectorNearbyInput. The function detects objects with shape { text: string, !vector } and assumes they are VectorNearbyInput values. This could false-positive on unrelated filter objects that happen to have a text key but no vector key. Verify this doesn't cause issues with other filter types.
  • RAG integration tests use mock embedder/chat. The end-to-end flow (real embedding → real vector search → real chat completion) is not tested. The mocks prove the wiring and schema, but not that actual embeddings produce meaningful RAG answers. Consider whether a real Ollama RAG test is needed.
  • Direct process.env access in buildEmbedderFromEnv() and buildChatCompleterFromEnv() — repo convention (AGENTS.md) says to use getEnvOptions(). Should these be updated?

Suggested test plan:

  1. Run cd graphile/graphile-llm && pnpm test in an environment with PostgreSQL + pgvector and Ollama + nomic-embed-text — all 31 tests should pass
  2. Load the preset into a PostGraphile schema with a table that has @hasChunks and verify ragQuery and embedText appear in the schema
  3. Execute a ragQuery with a real Ollama embedder + chat model and verify the response includes relevant source chunks and a synthesized answer
  4. Toggle enableRag: false and verify ragQuery disappears from the schema while text on VectorNearbyInput remains
  5. Execute a mutation with embeddingText: "some text" and verify the vector column is populated

Notes

  • enableRag defaults to false (opt-in) while enableTextSearch and enableTextMutations default to true. This is intentional since RAG requires both an embedder and a chat completer to be configured.
  • Heavy as any casting in plugins for scope access and build context — consistent with other graphile plugins in this repo (graphile-upload-plugin, graphile-bucket-provisioner-plugin).
  • Lint script fails identically to sibling packages (graphile-postgis, graphile-search) — pre-existing ESLint v9 config issue, not introduced by this PR.
  • The pnpm-lock.yaml diff is large due to formatting normalization by pnpm, not dependency changes.

Link to Devin session: https://app.devin.ai/sessions/720693f565cc4b8fb4b198ddf75814cc
Requested by: @pyramation


Open in Devin Review

…for PostGraphile

Foundation bundle of the graphile-llm plugin:
- Embedder abstraction with provider resolution (@agentic-kit/ollama)
- LlmModulePlugin: resolves embedder from llm_module api_modules config, env vars, or preset options
- LlmTextSearchPlugin: adds text field to VectorNearbyInput for text-based vector search
- LlmTextMutationPlugin: adds *Text companion fields on mutation inputs for vector columns
- GraphileLlmPreset: bundles all plugins with configurable options
- Debug logging for token counts (metering deferred to billing system)

Refs: constructive-io/constructive-planning#743
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

…ment, Ollama integration

Three test suites:
1. Embedder abstraction: unit tests for buildEmbedder, buildEmbedderFromModule, buildEmbedderFromEnv
2. Schema enrichment: verifies text field on VectorNearbyInput and embeddingText on mutation inputs (requires PostgreSQL + pgvector)
3. Real Ollama embedding: tests nomic-embed-text produces correct 768-dim vectors and semantic similarity (requires Ollama)

Tests gracefully skip when PostgreSQL or Ollama are not available.
…tead of raw fetch

- Tests now WILL fail if PostgreSQL or Ollama are unavailable (no more silent skipping)
- Replaced raw fetch() calls with OllamaClient from @agentic-kit/ollama (listModels, pullModel, generateEmbedding)
- Added direct OllamaClient.generateEmbedding test
- Stricter assertions on mutation input types (expect defined, not soft if-checks)
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +69 to +81
const {
scope: {
isPgRowType,
isPgPatch,
isPgBaseInput,
pgCodec,
},
} = context as any;

// Only intercept create/update input types for table rows
if (!pgCodec?.attributes || (!isPgRowType && !isPgPatch && !isPgBaseInput)) {
return fields;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 text-mutation-plugin uses isPgRowType instead of isMutationInput, deviating from established repo pattern

The scope-flag check at graphile/graphile-llm/src/plugins/text-mutation-plugin.ts:79 uses isPgRowType to identify mutation input types, but the established pattern throughout the repo uses isMutationInput. See graphile/graphile-upload-plugin/src/plugin.ts:184-193 and graphile/graphile-settings/src/plugins/required-input-plugin.ts:42-51, which both use isPgPatch || isPgBaseInput || isMutationInput. isPgRowType is the scope flag for output row types, not mutation input types. This means: (1) mutation input types that only have isMutationInput: true (but not isPgBaseInput or isPgPatch) will silently not receive text companion fields, and (2) if any input type has isPgRowType set on its scope, it would incorrectly receive companion fields.

Suggested change
const {
scope: {
isPgRowType,
isPgPatch,
isPgBaseInput,
pgCodec,
},
} = context as any;
// Only intercept create/update input types for table rows
if (!pgCodec?.attributes || (!isPgRowType && !isPgPatch && !isPgBaseInput)) {
return fields;
}
const {
scope: {
isPgPatch,
isPgBaseInput,
isMutationInput,
pgCodec,
},
} = context as any;
// Only intercept create/update input types for table rows
if (!pgCodec?.attributes || (!isPgPatch && !isPgBaseInput && !isMutationInput)) {
return fields;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

teardown = connections.teardown;
query = connections.query;

await db.client.query('BEGIN');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Test uses manual BEGIN/ROLLBACK that interferes with pgsql-test savepoint-based transaction management

The test suite at graphile/graphile-llm/src/__tests__/graphile-llm.test.ts:167 calls await db.client.query('BEGIN') in beforeAll and await db.client.query('ROLLBACK') in afterAll, while also using db.beforeEach()/db.afterEach(). The db.beforeEach() internally calls this.begin() (postgres/pgsql-client/src/client.ts:53-55), which issues a second BEGIN inside the already-open transaction, generating a PostgreSQL warning. The first db.afterEach() call then COMMITs the outer manual transaction, making the ROLLBACK in afterAll a no-op (no open transaction). The canonical graphile-test pattern (see graphile/graphile-test/__tests__/graphile-test.test.ts:35-37) does not use manual BEGIN/ROLLBACK — the framework handles transactions via savepoints automatically.

Suggested change
await db.client.query('BEGIN');
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Replace isPgRowType with isMutationInput in text-mutation-plugin (matches repo pattern in graphile-upload-plugin, required-input-plugin)
- Remove manual BEGIN/ROLLBACK from test suite — let pgsql-test handle transactions via savepoints
…dding

- Add chat.ts: chat completion provider abstraction (Ollama via @agentic-kit/ollama)
- Add rag-plugin.ts: LlmRagPlugin with ragQuery and embedText root query fields
  - Detects @hasChunks smart tags to discover chunk tables
  - RAG flow: embed prompt → search chunks → assemble context → chat LLM → return answer
  - Adds RagResponse, RagSource, EmbedTextResponse types to schema
- Wire resolve-time embedding in text-search-plugin (resolver wrapper on queries)
- Wire resolve-time embedding in text-mutation-plugin (resolver wrapper on mutations)
- Update llm-module-plugin to resolve chat completer on build
- Add enableRag toggle to GraphileLlmPreset (default false)
- Add chat completion types (ChatFunction, ChatConfig, ChatMessage, etc.)
- Add RagDefaults, ChunkTableInfo types
- Add integration tests: RAG schema enrichment, ragQuery execution, embedText,
  chat completion abstraction unit tests, preset toggle tests
- Add articles_chunks seed table for RAG testing
@devin-ai-integration devin-ai-integration Bot changed the title feat: add graphile-llm plugin — server-side text-to-vector embedding for PostGraphile feat: add graphile-llm plugin — embedding, RAG, and text-to-vector for PostGraphile Apr 19, 2026
@pyramation pyramation merged commit 955a9dd into main Apr 19, 2026
51 checks passed
@pyramation pyramation deleted the devin/1776579383-graphile-llm-plugin branch April 19, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant