@cloudgrid-io/mcp 0.20.6 → 0.20.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ // Simple semantic search — single-service reference for the CloudGrid managed vector
4
+ // substrate (pgvector) + the platform embeddings endpoint.
5
+ //
6
+ // On startup it embeds the bundled document (one vector per passage)
7
+ // via runtime.ai.embeddings and stores the vectors in the entity's own pgvector
8
+ // schema. POST /search embeds the query and returns the nearest passages by
9
+ // cosine distance. No keyword matching — relevance comes entirely from the
10
+ // embeddings.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const express = require('express');
15
+ const { Client } = require('pg');
16
+ const { runtime } = require('@cloudgrid-io/runtime');
17
+
18
+ const PORT = process.env.PORT || 8080;
19
+ const PGVECTOR_URL = process.env.VECTOR_PGVECTOR_URL || process.env.PGVECTOR_URL;
20
+
21
+ // One passage per non-empty paragraph in the article.
22
+ function loadChunks() {
23
+ const raw = fs.readFileSync(path.join(__dirname, 'document.txt'), 'utf8');
24
+ return raw
25
+ .split(/\n\s*\n/)
26
+ .map((p) => p.replace(/\s+/g, ' ').trim())
27
+ .filter((p) => p.length > 0);
28
+ }
29
+
30
+ // Format a JS number[] as a pgvector literal: '[0.1,0.2,...]'.
31
+ function toVectorLiteral(values) {
32
+ return '[' + values.join(',') + ']';
33
+ }
34
+
35
+ let seeded = false;
36
+ let seedError = null;
37
+ let chunkCount = 0;
38
+ let embeddingDim = 0;
39
+
40
+ async function connect() {
41
+ if (!PGVECTOR_URL) {
42
+ throw new Error('VECTOR_PGVECTOR_URL is not set — the vector need did not provision');
43
+ }
44
+ const client = new Client({ connectionString: PGVECTOR_URL });
45
+ await client.connect();
46
+ return client;
47
+ }
48
+
49
+ // Idempotent seed: ensure the extension + table exist, sized to the embedding
50
+ // model's actual output length, and populate the table once.
51
+ async function seed() {
52
+ const chunks = loadChunks();
53
+ const client = await connect();
54
+ try {
55
+ await client.query('CREATE EXTENSION IF NOT EXISTS vector');
56
+
57
+ // Size the column to the model's real output length — embed a probe first.
58
+ const probe = await runtime.ai.embeddings({ text: chunks[0] });
59
+ embeddingDim = probe.dim;
60
+ console.log(`[seed] embedding model=${probe.model} dim=${embeddingDim}`);
61
+
62
+ await client.query(
63
+ `CREATE TABLE IF NOT EXISTS chunks (
64
+ id integer PRIMARY KEY,
65
+ chunk text NOT NULL,
66
+ embedding vector(${embeddingDim}) NOT NULL
67
+ )`,
68
+ );
69
+
70
+ const { rows } = await client.query('SELECT count(*)::int AS n FROM chunks');
71
+ const existing = rows[0].n;
72
+ if (existing >= chunks.length) {
73
+ chunkCount = existing;
74
+ console.log(`[seed] already populated with ${existing} chunks — skipping embed`);
75
+ seeded = true;
76
+ return;
77
+ }
78
+
79
+ console.log(`[seed] embedding ${chunks.length} chunks`);
80
+ // Batch-embed every passage in input order.
81
+ const vectors = await runtime.ai.embeddings({ text: chunks });
82
+ for (let i = 0; i < chunks.length; i++) {
83
+ const literal = toVectorLiteral(vectors[i].values);
84
+ await client.query(
85
+ `INSERT INTO chunks (id, chunk, embedding)
86
+ VALUES ($1, $2, $3::vector)
87
+ ON CONFLICT (id) DO UPDATE SET chunk = EXCLUDED.chunk, embedding = EXCLUDED.embedding`,
88
+ [i, chunks[i], literal],
89
+ );
90
+ console.log(`[seed] inserted chunk ${i + 1}/${chunks.length}`);
91
+ }
92
+ chunkCount = chunks.length;
93
+ seeded = true;
94
+ console.log(`[seed] done — ${chunkCount} chunks stored`);
95
+ } finally {
96
+ await client.end().catch(() => {});
97
+ }
98
+ }
99
+
100
+ async function search(query, limit = 5) {
101
+ const q = await runtime.ai.embeddings({ text: query });
102
+ const literal = toVectorLiteral(q.values);
103
+ const client = await connect();
104
+ try {
105
+ const { rows } = await client.query(
106
+ `SELECT chunk, 1 - (embedding <=> $1::vector) AS score
107
+ FROM chunks
108
+ ORDER BY embedding <=> $1::vector
109
+ LIMIT $2`,
110
+ [literal, limit],
111
+ );
112
+ return rows.map((r) => ({ chunk: r.chunk, score: Number(r.score) }));
113
+ } finally {
114
+ await client.end().catch(() => {});
115
+ }
116
+ }
117
+
118
+ const app = express();
119
+ app.use(express.json());
120
+
121
+ app.get('/health', (_req, res) => {
122
+ // Return not-ready until the seed completes so the platform's readiness probe
123
+ // holds the pod out of traffic until search actually works.
124
+ if (seedError) return res.status(500).json({ status: 'seed_failed', error: seedError });
125
+ if (!seeded) return res.status(503).json({ status: 'seeding' });
126
+ res.json({ status: 'ok', chunks: chunkCount, dim: embeddingDim });
127
+ });
128
+
129
+ app.post('/search', async (req, res) => {
130
+ const query = req.body && typeof req.body.query === 'string' ? req.body.query.trim() : '';
131
+ if (!query) return res.status(400).json({ error: 'Provide a query string.' });
132
+ if (!seeded) return res.status(503).json({ error: 'The index is still loading. Try again shortly.' });
133
+ try {
134
+ const results = await search(query);
135
+ res.json({ query, results });
136
+ } catch (err) {
137
+ console.error('[search] failed', err);
138
+ res.status(500).json({ error: 'Search failed.' });
139
+ }
140
+ });
141
+
142
+ app.get('/', (_req, res) => {
143
+ res.type('html').send(PAGE);
144
+ });
145
+
146
+ const PAGE = `<!doctype html>
147
+ <html lang="en">
148
+ <head>
149
+ <meta charset="utf-8" />
150
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
151
+ <title>Semantic search</title>
152
+ <style>
153
+ :root { color-scheme: light; }
154
+ * { box-sizing: border-box; }
155
+ body { margin: 0; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #1a1a1a; background: #fafafa; }
156
+ main { max-width: 680px; margin: 0 auto; padding: 48px 20px 80px; }
157
+ h1 { font-size: 24px; margin: 0 0 4px; }
158
+ p.lede { color: #555; margin: 0 0 28px; }
159
+ form { display: flex; gap: 8px; margin-bottom: 24px; }
160
+ input { flex: 1; padding: 12px 14px; font-size: 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; }
161
+ input:focus { outline: none; border-color: #888; }
162
+ button { padding: 12px 20px; font-size: 16px; border: none; border-radius: 8px; background: #1a1a1a; color: #fff; cursor: pointer; }
163
+ button:disabled { opacity: .5; cursor: default; }
164
+ .result { padding: 16px 0; border-top: 1px solid #eee; }
165
+ .result .score { font-size: 13px; color: #888; margin-bottom: 4px; font-variant-numeric: tabular-nums; }
166
+ .result .text { margin: 0; }
167
+ .status { color: #888; font-size: 14px; }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <main>
172
+ <h1>Semantic search</h1>
173
+ <p class="lede">Search the document by meaning. Results are ranked by embedding similarity, not keyword match.</p>
174
+ <form id="f">
175
+ <input id="q" type="text" placeholder="Ask a question about the document" autocomplete="off" />
176
+ <button id="b" type="submit">Search</button>
177
+ </form>
178
+ <div id="out"></div>
179
+ </main>
180
+ <script>
181
+ var f = document.getElementById('f');
182
+ var q = document.getElementById('q');
183
+ var b = document.getElementById('b');
184
+ var out = document.getElementById('out');
185
+ f.addEventListener('submit', function (e) {
186
+ e.preventDefault();
187
+ var query = q.value.trim();
188
+ if (!query) return;
189
+ b.disabled = true;
190
+ out.innerHTML = '<p class="status">Searching.</p>';
191
+ fetch('/search', {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({ query: query }),
195
+ })
196
+ .then(function (r) { return r.json(); })
197
+ .then(function (data) {
198
+ b.disabled = false;
199
+ if (!data.results || !data.results.length) {
200
+ out.innerHTML = '<p class="status">No results.</p>';
201
+ return;
202
+ }
203
+ out.innerHTML = data.results.map(function (r) {
204
+ return '<div class="result"><div class="score">score ' + r.score.toFixed(3) +
205
+ '</div><p class="text">' + r.chunk.replace(/</g, '&lt;') + '</p></div>';
206
+ }).join('');
207
+ })
208
+ .catch(function () {
209
+ b.disabled = false;
210
+ out.innerHTML = '<p class="status">Search failed. Try again.</p>';
211
+ });
212
+ });
213
+ </script>
214
+ </body>
215
+ </html>`;
216
+
217
+ app.listen(PORT, () => {
218
+ console.log(`simple-semantic-search listening on ${PORT}`);
219
+ // Seed in the background so the HTTP server binds immediately (the platform's
220
+ // startup probe wants the port open). /health reports seeding progress.
221
+ seed().catch((err) => {
222
+ seedError = String((err && err.message) || err);
223
+ console.error('[seed] FAILED', err);
224
+ });
225
+ });
@@ -4,7 +4,7 @@ when: chatbot, AI assistant, Q&A bot, conversational app, support bot, ask-me-an
4
4
  needs: ai, database
5
5
  deploy: runtime
6
6
  editions: local
7
- capabilities_note: AI chatbot — needs ai (the grid AI gateway) + a database (Mongo) for chat history. Next.js App Router app. Uses @cloudgrid-io/ai createClient().chat() with zero-config in-grid identity auth (no API key). Runtime app, async build, local edition only. Declare `needs: { ai: true, database: true }`. (RAG over your own docs needs `needs: { vector: pgvector }` — not yet available, platform issue #1545.)
7
+ capabilities_note: AI chatbot — needs ai (the grid AI gateway) + a database (Mongo) for chat history. Next.js App Router app. Uses @cloudgrid-io/ai createClient().chat() with zero-config in-grid identity auth (no API key). Runtime app, async build, local edition only. Declare `needs: { ai: true, database: true }`. (RAG over your own docs needs `needs: { vector: pgvector }` — now available, #1545 shipped (verified live 2026-07-16); this template ships the plain chatbot without it.)
8
8
  summary: Build an AI chatbot on the grid — a Next.js app that calls @cloudgrid-io/ai createClient().chat({messages}) (zero-config in-grid auth, no key) and persists the conversation to grid-shared Mongo. Edition-gate first, scaffold, put the app under services/web/, declare needs:{ai:true,database:true}, read DATABASE_MONGODB_URL lazily, deploy async, poll to a live URL.
9
9
  ---
10
10
 
@@ -66,7 +66,7 @@ ai: true, database: true }`).
66
66
  ai: true
67
67
  database: true
68
68
  # For retrieval-augmented (RAG) over your own docs, add `needs: { vector:
69
- # pgvector }` once platform issue #1545 ships — not yet available.
69
+ # pgvector }` — available now (#1545 shipped); this template does not use it.
70
70
  ```
71
71
  **Declare `needs: { ai: true, database: true }`** — the canonical shape. The
72
72
  deployer wires the AI gateway (in-grid identity) and provisions Mongo,
@@ -129,8 +129,9 @@ the SAME entity (`target_entity_id`) so it updates the same URL.
129
129
 
130
130
  If the user wants the bot to answer over **their own documents** (retrieval-
131
131
  augmented generation), that needs a vector store (`needs: { vector: pgvector }`).
132
- That path is **not yet available** — platform issue #1545. Ship the plain
133
- chatbot now; add retrieval once the vector need is unblocked.
132
+ That path is **now available** — #1545 shipped. The template itself is a plain
133
+ chatbot; declaring `vector: pgvector` injects `VECTOR_PGVECTOR_URL`, and you
134
+ build the retrieval code on top of it.
134
135
 
135
136
  Keep it honest: async build, local-edition only, AI auth and DB credentials
136
137
  provided by the grid.
@@ -5,8 +5,8 @@ needs: ai+database
5
5
  deploy: runtime
6
6
  editions: local
7
7
  kind: blueprint
8
- capabilities_note: "persistent RAG — needs ai + database. Runtime app, async build, local edition only. Declare needs:{ai:true, database:true}; the deployer injects AI_GATEWAY_URL (AI Gateway via @cloudgrid-io/ai — works today) and DATABASE_MONGODB_URL (+legacy MONGODB_URL). The ideal embedding store needs:{vector:pgvector} (VECTOR_PGVECTOR_URL) is PENDING platform issue #1545 — until it lands, store embeddings in a Mongo `chunks` collection and cosine-rank in-app."
9
- summary: "A BLUEPRINT for a retrieval-augmented 'ask my docs' knowledge base — ingest documents, chunk + embed them, retrieve the nearest chunks per question, and answer grounded with citations, on persistent Next.js + Mongo + the CloudGrid AI Gateway. This is not fill-in-the-blanks app code; it ships structure + cloudgrid.yaml. Fetch the template, read AGENTS.md for the file tree / collections / RAG loop / CloudGrid wiring (needs ai+database, the pgvector #1545 note), then BUILD the app under services/web/ following it, and deploy async to a live URL (local edition only)."
8
+ capabilities_note: "persistent RAG — needs ai + database. Runtime app, async build, local edition only. Declare needs:{ai:true, database:true}; the deployer injects AI_GATEWAY_URL (AI Gateway via @cloudgrid-io/ai — works today) and DATABASE_MONGODB_URL (+legacy MONGODB_URL). The ideal embedding store needs:{vector:pgvector} (VECTOR_PGVECTOR_URL) is now AVAILABLE — #1545 shipped (verified live 2026-07-16) — but this blueprint still stores embeddings in a Mongo `chunks` collection and cosine-ranks in-app; declare vector:pgvector only if you also build the retrieval on pgvector."
9
+ summary: "A BLUEPRINT for a retrieval-augmented 'ask my docs' knowledge base — ingest documents, chunk + embed them, retrieve the nearest chunks per question, and answer grounded with citations, on persistent Next.js + Mongo + the CloudGrid AI Gateway. This is not fill-in-the-blanks app code; it ships structure + cloudgrid.yaml. Fetch the template, read AGENTS.md for the file tree / collections / RAG loop / CloudGrid wiring (needs ai+database, the pgvector note — #1545 shipped), then BUILD the app under services/web/ following it, and deploy async to a live URL (local edition only)."
10
10
  ---
11
11
 
12
12
  # Workflow: ai-knowledge-base
@@ -26,10 +26,11 @@ app code. The recipe is: fetch it, **read `AGENTS.md`**, then **build** the app
26
26
  following it. Be honest that a runtime deploy is async and needs the local edition.
27
27
 
28
28
  > **Vector-store status:** the ideal embedding store is `needs: { vector: pgvector }`
29
- > (injects `VECTOR_PGVECTOR_URL`), but it is **PENDING platform issue #1545**. The
30
- > **`ai` part works today.** Until #1545 ships, store embeddings in a Mongo
31
- > `chunks` collection and cosine-rank in the app — the blueprint declares
32
- > `needs: { ai: true, database: true }` and keeps `vector: pgvector` commented.
29
+ > (injects `VECTOR_PGVECTOR_URL`), and it is **now available** — #1545 shipped. The
30
+ > **blueprint still stores embeddings in a Mongo `chunks` collection** and
31
+ > cosine-ranks in the app; it declares `needs: { ai: true, database: true }` and
32
+ > keeps `vector: pgvector` commented. Uncomment it only if you also build the
33
+ > retrieval on pgvector.
33
34
 
34
35
  ## 1. Edition check FIRST (hard gate)
35
36
 
@@ -54,11 +55,11 @@ CLI.
54
55
  **`AGENTS.md` structure guide** — read it before writing anything. It defines:
55
56
  - the `services/web/` file tree (pages, `api/documents` + `api/ask` routes, `lib/`),
56
57
  - the Mongo collections (`documents`, `chunks`) + fields,
57
- - the CloudGrid injection table (needs ai+database + the pgvector #1545 note),
58
+ - the CloudGrid injection table (needs ai+database + the pgvector note),
58
59
  - the ingest → retrieve → answer RAG loop, with lazy `lib/db.js` / `lib/ai.js` /
59
- `lib/retrieve.js` (cosine top-k over Mongo embeddings until #1545),
60
+ `lib/retrieve.js` (cosine top-k over Mongo embeddings as shipped),
60
61
  - optional auth/payments wiring via `vault:`,
61
- - the deploy flow and the pgvector swap-in once #1545 lands.
62
+ - the deploy flow and the pgvector swap-in (now available — #1545 shipped).
62
63
 
63
64
  There is no app code to copy — you generate it from the guide.
64
65
 
@@ -86,7 +87,9 @@ the filesystem path. **Declare `needs: { ai: true, database: true }`** (canonica
86
87
  key) and provisions Mongo, injecting `DATABASE_MONGODB_URL` (+ legacy
87
88
  `MONGODB_URL`). `requires:` is the deprecated v1 alias; never author it and never
88
89
  set `needs:` and `requires:` together (the validator rejects the combination).
89
- Leave `vector: pgvector` **commented** — it is PENDING #1545.
90
+ Leave `vector: pgvector` **commented** unless you build the retrieval on pgvector
91
+ — the need is available (#1545 shipped), but this blueprint's code uses Mongo
92
+ embeddings.
90
93
 
91
94
  ## 5. Build the app following AGENTS.md
92
95
 
@@ -100,7 +103,7 @@ Generate the files under `services/web/` per the guide:
100
103
  `embedding` array, mark the `documents` doc `ready`.
101
104
  - **Retrieve** (`lib/retrieve.js`): embed the question, cosine-rank the Mongo
102
105
  `chunks`, return top-k `{ text, documentId, score }` — a pure function that swaps
103
- cleanly to a pgvector query once #1545 lands.
106
+ cleanly to a pgvector query (now available — #1545 shipped).
104
107
  - **Answer** (`POST /api/ask`): retrieve top-k, prompt the model to answer ONLY
105
108
  from the provided context and to cite the chunks, `chat()` via the AI Gateway,
106
109
  return the answer plus source chunks so the UI shows **citations**.
@@ -132,6 +135,6 @@ response is `status: "building"`, NOT a live URL yet.
132
135
  Give the user the live app URL — the deliverable. To iterate, re-plug the SAME
133
136
  entity so it updates the same URL. Keep it honest: this is a blueprint you built
134
137
  from, the build is async, it is local-edition only, credentials are injected by the
135
- grid (AI Gateway + Mongo via `needs`), and the pgvector store is PENDING #1545 (so
136
- embeddings currently live in Mongo). Swap to pgvector when #1545 ships per
137
- `AGENTS.md` §7.
138
+ grid (AI Gateway + Mongo via `needs`), and the blueprint stores embeddings in
139
+ Mongo (the pgvector store is available — #1545 shipped — but the shipped code
140
+ does not use it). To swap to pgvector, follow `AGENTS.md` §7.
@@ -4,8 +4,8 @@ when: "search over my documents / PDFs / notes / knowledge base, semantic search
4
4
  needs: database
5
5
  deploy: runtime
6
6
  editions: local
7
- capabilities_note: "Document search — needs a database (Mongo) for the documents + chunks collections. Two services: a React (Vite) static frontend at / and a FastAPI backend at /backend. Hybrid search = Mongo $text (lexical) + in-app NumPy cosine over chunk embedding arrays (semantic) + metadata filters, blended to the document level. Runtime app, async build, local edition only. Declare needs:{database:true}. NO needs:vector — pgvector runtime role-grant is blocked (#1545), so embeddings live in the Mongo chunks collection and are cosine-ranked in-app. Two refresh paths: the manager 'Refresh now' endpoint AND a scheduled Python type:cron job (services/refresh, daily 03:00 UTC) that vendors the backend app/ modules and reuses indexing.run_sync — both degrade gracefully. Health is green with no secrets; startup ensure_indexes() so $text never 500s on an empty catalog. Pluggable source (dropbox|local|url) + embeddings (OpenAI-compatible) each behind one config point, all secrets from env."
8
- summary: "Build a document-search app on the grid — index a folder of PDFs/DOCX/TXT/MD, then search by keyword + meaning + metadata, with a grounded answer mode and manager admin. React static frontend at / + FastAPI backend at /backend, backed by grid-shared Mongo. Edition-gate first, scaffold, put the code under services/web/ + services/backend/, declare needs:{database:true} (NO vector #1545), wire the web+backend+refresh services, set the embeddings + source + manager secrets, deploy async, poll to a live URL, then Refresh now (or wait for the daily cron) to index."
7
+ capabilities_note: "Document search — needs a database (Mongo) for the documents + chunks collections. Two services: a React (Vite) static frontend at / and a FastAPI backend at /backend. Hybrid search = Mongo $text (lexical) + in-app NumPy cosine over chunk embedding arrays (semantic) + metadata filters, blended to the document level. Runtime app, async build, local edition only. Declare needs:{database:true}. NO needs:vector — this template stores embeddings in the Mongo chunks collection and cosine-ranks in-app (vector:pgvector is now available, #1545 shipped, verified live 2026-07-16; the template code does not use it). Two refresh paths: the manager 'Refresh now' endpoint AND a scheduled Python type:cron job (services/refresh, daily 03:00 UTC) that vendors the backend app/ modules and reuses indexing.run_sync — both degrade gracefully. Health is green with no secrets; startup ensure_indexes() so $text never 500s on an empty catalog. Pluggable source (dropbox|local|url) + embeddings (OpenAI-compatible) each behind one config point, all secrets from env."
8
+ summary: "Build a document-search app on the grid — index a folder of PDFs/DOCX/TXT/MD, then search by keyword + meaning + metadata, with a grounded answer mode and manager admin. React static frontend at / + FastAPI backend at /backend, backed by grid-shared Mongo. Edition-gate first, scaffold, put the code under services/web/ + services/backend/, declare needs:{database:true} (no vector — the template uses Mongo embeddings; #1545 shipped), wire the web+backend+refresh services, set the embeddings + source + manager secrets, deploy async, poll to a live URL, then Refresh now (or wait for the daily cron) to index."
9
9
  recipe: "edition-gate -> auth+grid -> grid_create_project -> fetch template + read AGENTS.md -> put code under services/web + services/backend, set cloudgrid.yaml active fields -> grid secrets set (embeddings + source + MANAGER_PASSWORD_HASH) -> optional grid dev -> grid plug (async, poll) -> Refresh now to index -> return live URL"
10
10
  ---
11
11
 
@@ -88,9 +88,10 @@ injects `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`), read lazily in
88
88
  `app/db.py`. `requires:` is the deprecated v1 alias; never author it and never set
89
89
  `needs:` and `requires:` together (the validator rejects the combination).
90
90
 
91
- **NO `needs: vector`** — the pgvector runtime role-grant is blocked on platform
92
- issue **#1545**, so this template stores embeddings in the Mongo `chunks`
93
- collection (a float array) and cosine-ranks in-app. **The refresh cron IS active**
91
+ **NO `needs: vector`** — this template stores embeddings in the Mongo `chunks`
92
+ collection (a float array) and cosine-ranks in-app. (`vector: pgvector` is now
93
+ available — **#1545** shipped — but the template code does not use it; declare it
94
+ only if you also switch the search code to pgvector.) **The refresh cron IS active**
94
95
  — a Python `type: cron`, `run: job` service (`services/refresh/`) reindexes daily
95
96
  at 03:00 UTC, and the manager "Refresh now" endpoint indexes on demand. Both call
96
97
  the same `indexing.run_sync`. Because each service builds in an isolated container,
@@ -137,6 +138,7 @@ Give the user the live app URL — the deliverable. To iterate, re-plug the SAME
137
138
  entity so it updates the same URL.
138
139
 
139
140
  Keep it honest: this ships real code you adapt, the build is async and
140
- local-edition only, Mongo is injected by `needs`, embeddings live in Mongo (no
141
- pgvector until #1545), and refresh runs both on demand (endpoint) and on a schedule
141
+ local-edition only, Mongo is injected by `needs`, embeddings live in Mongo (the
142
+ template does not use pgvector, though #1545 shipped and the need is available),
143
+ and refresh runs both on demand (endpoint) and on a schedule
142
144
  (the `refresh` cron).
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: simple-semantic-search
3
+ when: semantic search over a document / article / text, "search my doc by meaning", question-answering search box over content, embeddings search, vector search, similarity search, "find the passage that answers X" — a SINGLE document or small text set, one service. For a multi-service document portfolio with upload UI + refresh cron, use semantic-search instead.
4
+ needs: vector, ai
5
+ deploy: runtime
6
+ editions: local
7
+ capabilities_note: "vector search — needs: { vector: pgvector } (injects VECTOR_PGVECTOR_URL; #1545 shipped, verified live 2026-07-16) + ai: true (embeddings via @cloudgrid-io/runtime, no API key). Runtime app, async build, local edition only."
8
+ summary: "One node service that embeds a document into the entity's own pgvector schema at startup and serves POST /search + a minimal UI — probe-sized vector column, idempotent background seed, /health 503-until-ready, cosine <=> ranking. grid init app --here, grid plug, live in ~3 minutes."
9
+ ---
10
+
11
+ # Workflow: simple-semantic-search
12
+
13
+ The user wants to search content by MEANING — "find where it talks about X",
14
+ a question box over an article, notes, or docs. Keyword match is not enough;
15
+ this is embeddings + a vector store. On CloudGrid that is one declared need,
16
+ not infrastructure to run.
17
+
18
+ ## 1. Edition check FIRST (hard gate)
19
+
20
+ A runtime app: needs the **local edition** (Claude Desktop / Claude Code) or
21
+ the CLI. On the hosted edition, say persistence/vector isn't available there
22
+ and offer a static alternative honestly.
23
+
24
+ ## 2. Fetch the template
25
+
26
+ `grid_get_template({kind: "template", name: "simple-semantic-search"})` — it
27
+ contains the full working source (proven by a live deploy), the cloudgrid.yaml,
28
+ and the `.cloudgrid/link.json` explainer.
29
+
30
+ ## 3. Adapt
31
+
32
+ - Drop the user's content into `services/web/src/document.txt` (plain text,
33
+ blank-line-separated passages). Everything else usually stays as-is.
34
+ - Keep the three proven mechanics exactly: probe-sized `vector(dim)` column,
35
+ idempotent background seed with `/health` 503-until-ready, lazy
36
+ `VECTOR_PGVECTOR_URL` read (legacy `PGVECTOR_URL` fallback).
37
+ - The manifest is two needs and one service:
38
+
39
+ ```yaml
40
+ needs:
41
+ vector: pgvector
42
+ ai: true
43
+ services:
44
+ web: { type: node, path: /, health: /health }
45
+ ```
46
+
47
+ ## 4. Deploy
48
+
49
+ ```bash
50
+ grid init app <name> --here # registers the entity, writes .cloudgrid/link.json
51
+ grid plug # async build — live URL in ~2-3 minutes
52
+ grid visibility set <slug> link
53
+ ```
54
+
55
+ `.cloudgrid/link.json` (entity_id / org / slug binding) is WRITTEN BY THE CLI —
56
+ never hand-author it. If the folder was copied from someone else's project,
57
+ `rm -rf .cloudgrid` first so `grid plug` doesn't update THEIR app.
58
+
59
+ ## 5. Verify like it's real
60
+
61
+ Open the live URL, ask a question whose answer is in the document, and check
62
+ the top passage actually answers it (rules out keyword-only luck). `grid logs`
63
+ shows the `[seed]` lines; `/health` returns `{status:"ok", chunks: N, dim: D}`
64
+ when ready.