@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.
- package/package.json +1 -1
- package/src/corpus/capability-map.md +21 -13
- package/src/corpus/cloudgrid-yaml.md +5 -3
- package/src/corpus/examples/ai-app/index.md +4 -3
- package/src/corpus/templates/ai-app/README.md +4 -4
- package/src/corpus/templates/ai-app/index.md +4 -4
- package/src/corpus/templates/ai-knowledge-base/AGENTS.md +17 -17
- package/src/corpus/templates/ai-knowledge-base/README.md +5 -4
- package/src/corpus/templates/ai-knowledge-base/cloudgrid.yaml +5 -4
- package/src/corpus/templates/ai-knowledge-base/index.md +15 -11
- package/src/corpus/templates/semantic-search/AGENTS.md +9 -9
- package/src/corpus/templates/semantic-search/README.md +4 -3
- package/src/corpus/templates/semantic-search/cloudgrid.yaml +6 -5
- package/src/corpus/templates/semantic-search/index.md +7 -4
- package/src/corpus/templates/simple-semantic-search/README.md +20 -0
- package/src/corpus/templates/simple-semantic-search/cloudgrid.yaml +235 -0
- package/src/corpus/templates/simple-semantic-search/index.md +353 -0
- package/src/corpus/templates/simple-semantic-search/services/web/package.json +15 -0
- package/src/corpus/templates/simple-semantic-search/services/web/src/document.txt +40 -0
- package/src/corpus/templates/simple-semantic-search/services/web/src/index.js +225 -0
- package/src/corpus/workflows/ai-app.md +5 -4
- package/src/corpus/workflows/ai-knowledge-base.md +17 -14
- package/src/corpus/workflows/semantic-search.md +9 -7
- package/src/corpus/workflows/simple-semantic-search.md +64 -0
|
@@ -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, '<') + '</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 }` —
|
|
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 }`
|
|
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 **
|
|
133
|
-
chatbot
|
|
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
|
|
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
|
|
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`),
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
> `
|
|
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
|
|
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
|
|
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
|
|
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**
|
|
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
|
|
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
|
|
136
|
-
|
|
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 —
|
|
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} (
|
|
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`** —
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
141
|
-
pgvector
|
|
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.
|