@cloudgrid-io/mcp 0.14.0 → 0.15.0
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 +14 -1
- package/src/corpus/templates/semantic-search/AGENTS.md +192 -0
- package/src/corpus/templates/semantic-search/README.md +74 -0
- package/src/corpus/templates/semantic-search/cloudgrid.yaml +217 -0
- package/src/corpus/templates/semantic-search/index.md +97 -0
- package/src/corpus/templates/semantic-search/services/backend/requirements.txt +7 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/__init__.py +0 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/answer.py +90 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/auth.py +65 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/config.py +110 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/db.py +89 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/embeddings.py +44 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/indexing.py +222 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/main.py +259 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/search.py +222 -0
- package/src/corpus/templates/semantic-search/services/backend/src/app/source.py +259 -0
- package/src/corpus/templates/semantic-search/services/backend/src/main.py +12 -0
- package/src/corpus/templates/semantic-search/services/web/index.html +12 -0
- package/src/corpus/templates/semantic-search/services/web/package-lock.json +1680 -0
- package/src/corpus/templates/semantic-search/services/web/package.json +19 -0
- package/src/corpus/templates/semantic-search/services/web/src/App.jsx +55 -0
- package/src/corpus/templates/semantic-search/services/web/src/api.js +125 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/Dashboard.jsx +61 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/ManagerLogin.jsx +39 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/SearchView.jsx +166 -0
- package/src/corpus/templates/semantic-search/services/web/src/components/Settings.jsx +148 -0
- package/src/corpus/templates/semantic-search/services/web/src/main.jsx +10 -0
- package/src/corpus/templates/semantic-search/services/web/src/styles.css +78 -0
- package/src/corpus/templates/semantic-search/services/web/vite.config.js +10 -0
- package/src/corpus/workflows/semantic-search.md +133 -0
- package/src/tools.js +73 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@ cloudgrid.yaml schema (every field, the `needs:` injection table, the
|
|
|
13
13
|
requires-vs-needs caveat, validation rules), fetch the companion reference:
|
|
14
14
|
`gridctl_fetch("doc", "cloudgrid-yaml")`.
|
|
15
15
|
|
|
16
|
-
## The
|
|
16
|
+
## The 59 templates
|
|
17
17
|
|
|
18
18
|
| Intent (match on `when:`) | Template | `needs:` | Deploy | Edition |
|
|
19
19
|
|---|---|---|---|---|
|
|
@@ -57,6 +57,7 @@ requires-vs-needs caveat, validation rules), fetch the companion reference:
|
|
|
57
57
|
| revenue dashboard, sales dashboard, MRR/revenue view | `revenue-dashboard` | `database: true` | runtime (async, poll) | local |
|
|
58
58
|
| property listings, real estate site, rentals/homes listings | `property-listings` | `database: true` | runtime (async, poll) | local |
|
|
59
59
|
| project management, projects and tasks tracker, team project board | `project-management` | `database: true` | runtime (async, poll) | local |
|
|
60
|
+
| search over my documents / PDFs / notes / knowledge base, semantic search, document search, find across my files, searchable archive | `semantic-search` | `database: true` (NO `vector` #1545; NO cron #1585) | runtime (async, poll) | local |
|
|
60
61
|
|
|
61
62
|
**Rule of thumb:** if the app must SAVE/remember data, share state across
|
|
62
63
|
users/sessions, log in, or store submissions → it is persistent → runtime, local
|
|
@@ -73,6 +74,18 @@ differ only by domain schema + UI. Match the request to the closest one by its
|
|
|
73
74
|
domain, use `app-with-data` (the minimal reference). Deploy path is identical for
|
|
74
75
|
all of them.
|
|
75
76
|
|
|
77
|
+
**Document search (`semantic-search`).** For "search over my own documents / PDFs
|
|
78
|
+
/ notes / knowledge base" with keyword **and** meaning, `semantic-search` ships
|
|
79
|
+
real runnable code in a different shape from the Next.js family: a React (Vite)
|
|
80
|
+
**static** frontend at `/` plus a **Python FastAPI** backend at `/backend`, on the
|
|
81
|
+
same grid-shared Mongo (`needs: { database: true }`). Hybrid search = Mongo
|
|
82
|
+
`$text` + in-app NumPy cosine over embedding arrays + metadata filters. It stores
|
|
83
|
+
embeddings in the Mongo `chunks` collection (NO `needs: vector` — pgvector is
|
|
84
|
+
blocked on **#1545**) and refreshes via a manager endpoint (NO active cron — a
|
|
85
|
+
Python `type: cron` is blocked on **#1585**). Read its `AGENTS.md` for the source
|
|
86
|
+
adapter (dropbox/local/url) + embeddings wiring and the health-without-secrets /
|
|
87
|
+
startup-index patterns.
|
|
88
|
+
|
|
76
89
|
## Held / pending platform (not yet buildable)
|
|
77
90
|
|
|
78
91
|
Two more archetypes are designed but blocked on platform work — the LLM should
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# AGENTS.md — semantic-search template (CloudGrid wiring guide)
|
|
2
|
+
|
|
3
|
+
This template is a **runnable app**, not a blueprint — it ships real FastAPI +
|
|
4
|
+
React code. Read this guide, adapt the naming/metadata to the user's domain, wire
|
|
5
|
+
the secrets, and deploy. The four "baked-in learnings" (health-without-secrets,
|
|
6
|
+
startup index, `$text` guard, no vector/no cron) are already in the code — do not
|
|
7
|
+
regress them.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. File tree
|
|
12
|
+
|
|
13
|
+
Every service's code lives under `services/<name>/` — `path:` in `cloudgrid.yaml`
|
|
14
|
+
is the URL mount, NOT the filesystem path. Files at the repo root fail with
|
|
15
|
+
`Error: Service directory not found: …/services/<name>`.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
cloudgrid.yaml # name + services(web,backend) + needs:{database:true}
|
|
19
|
+
services/web/ # React + Vite static frontend, served at /
|
|
20
|
+
package.json vite.config.js index.html
|
|
21
|
+
src/main.jsx App.jsx api.js styles.css
|
|
22
|
+
src/components/SearchView.jsx ManagerLogin.jsx Settings.jsx Dashboard.jsx
|
|
23
|
+
services/backend/ # FastAPI, mounted at /backend
|
|
24
|
+
requirements.txt
|
|
25
|
+
src/main.py # uvicorn app.main:app (reads PORT)
|
|
26
|
+
src/app/
|
|
27
|
+
main.py config.py db.py search.py indexing.py
|
|
28
|
+
source.py embeddings.py answer.py auth.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Mongo collections + fields
|
|
34
|
+
|
|
35
|
+
The grid provisions one MongoDB database (`needs: { database: true }`). Two
|
|
36
|
+
primary collections (`_id` is the Mongo ObjectId on every doc):
|
|
37
|
+
|
|
38
|
+
**`documents`** — one row per source document
|
|
39
|
+
- `title` (string), `collection` (string, free-form grouping tag, nullable),
|
|
40
|
+
`date` (ISO date string, nullable) — the metadata filters.
|
|
41
|
+
- `source_filename` (string, unique), `source_rev` (string — content hash / rev
|
|
42
|
+
for incremental sync), `file_path` (string, source-internal, never exposed).
|
|
43
|
+
- `has_media` (bool), `word_count` (int), `indexed_at` (Date).
|
|
44
|
+
|
|
45
|
+
**`chunks`** — one embeddable passage per document (the retrieval unit)
|
|
46
|
+
- `document_id` (ObjectId → documents), `chunk_index` (int),
|
|
47
|
+
- `content` (string — indexed by the `$text` index, returned as the passage),
|
|
48
|
+
- `embedding` (float[] — the semantic vector; `null` if embeddings unconfigured).
|
|
49
|
+
|
|
50
|
+
Support collections: `settings` (feature flags, synonyms, `catalog.last_refresh`),
|
|
51
|
+
`usage_events`, `index_reports`, `sessions`.
|
|
52
|
+
|
|
53
|
+
**Adapt the metadata to the domain:** rename/extend `collection` and `date` in
|
|
54
|
+
`indexing.py::_parse_metadata`, and mirror the field names in `search.py`,
|
|
55
|
+
`main.py`, and the web `SearchView`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. How CloudGrid injects the datastore
|
|
60
|
+
|
|
61
|
+
You never provision infra or set a connection string by hand. `needs:{database:
|
|
62
|
+
true}` provisions Mongo and injects it as an env var at `grid dev` (local) and at
|
|
63
|
+
runtime (after `grid plug`):
|
|
64
|
+
|
|
65
|
+
| What | Declared in cloudgrid.yaml | Injected env var(s) | Read in code |
|
|
66
|
+
|------|----------------------------|---------------------|--------------|
|
|
67
|
+
| MongoDB | `needs: { database: true }` | `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`) | `app/db.py::get_client` (lazy) |
|
|
68
|
+
|
|
69
|
+
Read it LAZILY — `os.environ.get("DATABASE_MONGODB_URL") or
|
|
70
|
+
os.environ.get("MONGODB_URL")` inside `get_client()`, never at module top level.
|
|
71
|
+
`requires:` is the deprecated v1 alias; never author it and never set `needs:` and
|
|
72
|
+
`requires:` together (the validator rejects the combination).
|
|
73
|
+
|
|
74
|
+
**No `needs: vector`.** The ideal embedding store is pgvector
|
|
75
|
+
(`needs: { vector: pgvector }` → `VECTOR_PGVECTOR_URL`), but the runtime
|
|
76
|
+
role-grant is **blocked on platform issue #1545**. Until it lands, embeddings live
|
|
77
|
+
in the Mongo `chunks.embedding` float array and are cosine-ranked in-app
|
|
78
|
+
(`search.py::_cosine`). When #1545 ships, move the vectors to pgvector and replace
|
|
79
|
+
the in-app cosine block with an `ORDER BY embedding <=> $q LIMIT k` query — the
|
|
80
|
+
rest of the pipeline is unchanged.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Pluggable document source (SOURCE_TYPE)
|
|
85
|
+
|
|
86
|
+
The source is where raw documents live, not the query DB. `SOURCE_TYPE` selects
|
|
87
|
+
the adapter in `app/source.py`; all creds are gated in `app/config.py` and read
|
|
88
|
+
lazily:
|
|
89
|
+
|
|
90
|
+
- **`dropbox`** (default) — read-only, offline refresh-token flow. Secrets:
|
|
91
|
+
`DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET`, `DROPBOX_REFRESH_TOKEN`,
|
|
92
|
+
`DROPBOX_FOLDER_PATH` (optional).
|
|
93
|
+
- **`local`** — a directory on the container filesystem. Set `LOCAL_SOURCE_PATH`
|
|
94
|
+
(or add `needs: { disk: true }` and it defaults to the injected `DISK_PATH`).
|
|
95
|
+
- **`url`** — a manifest of document URLs. Set `URL_SOURCE_MANIFEST` to a
|
|
96
|
+
newline- or comma-separated list.
|
|
97
|
+
|
|
98
|
+
**Add your own adapter:** subclass `Source` (implement `list_files()` returning
|
|
99
|
+
`SourceFile[]` and `download(path) -> bytes`), gate its config in
|
|
100
|
+
`config.py::source_configured`, and return it from `get_source()`. Text
|
|
101
|
+
extraction for txt/md/pdf/docx is shared in `source.extract_text`.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 5. Embeddings (one config point)
|
|
106
|
+
|
|
107
|
+
`app/embeddings.py` is the single provider seam — OpenAI-compatible by default.
|
|
108
|
+
Secrets/config (all gated, read lazily):
|
|
109
|
+
|
|
110
|
+
- `EMBEDDINGS_API_KEY` — the key (semantic search + generated answers degrade
|
|
111
|
+
gracefully to lexical/extractive when unset).
|
|
112
|
+
- `EMBEDDINGS_MODEL` (default `text-embedding-3-small`) — changing it changes
|
|
113
|
+
vector dimensions and requires a full reindex.
|
|
114
|
+
- `EMBEDDINGS_BASE_URL` (default `https://api.openai.com/v1`) — point at any
|
|
115
|
+
OpenAI-compatible endpoint.
|
|
116
|
+
- `ANSWER_MODEL` (default `gpt-4o-mini`) — the grounded-answer chat model.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 6. Secrets via `grid secrets set`
|
|
121
|
+
|
|
122
|
+
Store secrets with the CLI; never commit them, never read them at import time:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
grid secrets set EMBEDDINGS_API_KEY=sk-...
|
|
126
|
+
grid secrets set SOURCE_TYPE=dropbox
|
|
127
|
+
grid secrets set DROPBOX_APP_KEY=... DROPBOX_APP_SECRET=... DROPBOX_REFRESH_TOKEN=...
|
|
128
|
+
grid secrets set MANAGER_PASSWORD_HASH=$(printf '%s' 'your-password' | shasum -a 256 | cut -d' ' -f1)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`MANAGER_PASSWORD_HASH` is the sha256 hex of the manager password, compared
|
|
132
|
+
server-side in `auth.py`. The in-app "Change password" screen returns a new hash
|
|
133
|
+
to store the same way. Non-secret public config can also go via `grid env`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 7. The baked-in learnings (do NOT regress)
|
|
138
|
+
|
|
139
|
+
1. **Startup index.** `main.py` has `@app.on_event("startup")` →
|
|
140
|
+
`db.ensure_indexes()`, which creates the chunks `$text` index (plus the
|
|
141
|
+
documents metadata indexes) at boot. Without it, `$text` raises
|
|
142
|
+
`IndexNotFound` (a 500) on an empty catalog. `ensure_indexes()` is safe to call
|
|
143
|
+
repeatedly and never blocks startup if Mongo is briefly unavailable.
|
|
144
|
+
2. **`$text` guard.** The lexical query in `search.py` is wrapped in
|
|
145
|
+
`except OperationFailure` so a missing index degrades to "lexical contributes
|
|
146
|
+
nothing", never a 500 — belt-and-suspenders on top of the startup index.
|
|
147
|
+
3. **Health without secrets.** No secret is read at import time; every secret is
|
|
148
|
+
gated inside a function (`config.py`). `GET /backend/health` needs only Mongo.
|
|
149
|
+
Search/indexing degrade gracefully (empty results + a clear note) when the
|
|
150
|
+
source or embeddings are unconfigured. `GET /backend/status` reports capability
|
|
151
|
+
**booleans** (`source_configured`, `embeddings_configured`, …), never secret
|
|
152
|
+
values.
|
|
153
|
+
4. **No `needs: vector` / no active cron.** `needs:` is `database` only (#1545).
|
|
154
|
+
The cron refresh service is commented out (#1585) — see §8.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 8. Refresh: manager endpoint now, cron later
|
|
159
|
+
|
|
160
|
+
The supported refresh path today is the manager-only endpoint
|
|
161
|
+
`POST /backend/admin/refresh` (wired to the "Refresh now" button in Settings). It
|
|
162
|
+
runs the same incremental sync pipeline (`indexing.run_sync`) a cron would.
|
|
163
|
+
|
|
164
|
+
A scheduled `type: cron` refresh service is a **follow-up**: a Python cron entity
|
|
165
|
+
is currently blocked on **platform issue #1585**. The `cloudgrid.yaml` keeps a
|
|
166
|
+
`refresh:` cron service commented out with a `services/refresh` source pointing at
|
|
167
|
+
the same `indexing.run_sync`. When #1585 lands, uncomment it (`schedule`,
|
|
168
|
+
`timezone`, `run: job`) and add a thin `services/refresh/src/main.py` that calls
|
|
169
|
+
`app.indexing.run_sync(triggered_by="cron")`.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 9. Deploy + edition note
|
|
174
|
+
|
|
175
|
+
This is a built + deployed multi-service app → the **local edition** (Claude
|
|
176
|
+
Desktop / Claude Code) or the CLI; the hosted edition cannot build a runtime
|
|
177
|
+
container.
|
|
178
|
+
|
|
179
|
+
1. `grid init semantic-search` — creates the entity + `.cloudgrid/link.json`.
|
|
180
|
+
2. Put the code under `services/web/` and `services/backend/`; set `cloudgrid.yaml`
|
|
181
|
+
to the active fields (web static+build, backend python, `needs:{database:true}`).
|
|
182
|
+
3. `grid secrets set …` for embeddings + source + `MANAGER_PASSWORD_HASH` (§6).
|
|
183
|
+
4. (Optional) `grid dev` to run locally against injected dev Mongo.
|
|
184
|
+
5. **Build the frontend first:** `(cd services/web && npm install && npm run build)`.
|
|
185
|
+
CloudGrid validates that a `type: static` service carrying a `build:` block
|
|
186
|
+
already has `services/web/dist/index.html` on disk at plug time — plug fails
|
|
187
|
+
validation otherwise.
|
|
188
|
+
6. `grid plug` — a runtime deploy is **ASYNC**: the first response is
|
|
189
|
+
`status: building`, not a live URL. Poll `grid status` (surface a liveness
|
|
190
|
+
signal, never a bare wait) until Ready.
|
|
191
|
+
7. Sign in as manager, hit "Refresh now" to index the source, then search.
|
|
192
|
+
Re-plug the same entity to update the same URL.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# semantic-search template — CloudGrid-native document search
|
|
2
|
+
|
|
3
|
+
A deployable, domain-agnostic **document search** app: index a folder of
|
|
4
|
+
documents (PDF / DOCX / TXT / MD), then search them by keyword, by meaning, and
|
|
5
|
+
by metadata — with a grounded answer mode and a manager admin. Data lives in the
|
|
6
|
+
grid-shared MongoDB, so the catalog survives refresh and is shared across users.
|
|
7
|
+
|
|
8
|
+
**Hybrid search** = Mongo `$text` (lexical) + in-app NumPy cosine over chunk
|
|
9
|
+
embeddings (semantic) + metadata filters (date / collection), blended and
|
|
10
|
+
aggregated to the document level with a best-passage highlight.
|
|
11
|
+
|
|
12
|
+
## Services
|
|
13
|
+
|
|
14
|
+
| Service | Type | Path | Entry |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `web` | `static` (React, Vite) | `/` | build → `dist` |
|
|
17
|
+
| `backend` | `python` (FastAPI) | `/backend` | `services/backend/src/main.py` |
|
|
18
|
+
|
|
19
|
+
`needs: { database: true }` → CloudGrid injects `DATABASE_MONGODB_URL` (fallback
|
|
20
|
+
`MONGODB_URL`). **No `needs: vector`** — pgvector runtime role-grant is blocked
|
|
21
|
+
(platform #1545), so embeddings live in the Mongo `chunks` collection and are
|
|
22
|
+
cosine-ranked in-app. A scheduled (cron) refresh is a follow-up, blocked on
|
|
23
|
+
platform #1585 — ship the manager "Refresh now" endpoint for now (see AGENTS.md).
|
|
24
|
+
|
|
25
|
+
## Pluggable document source
|
|
26
|
+
|
|
27
|
+
`SOURCE_TYPE` selects the adapter, all credentials from env secrets:
|
|
28
|
+
|
|
29
|
+
- `dropbox` (default) — read-only Dropbox via the offline refresh-token flow.
|
|
30
|
+
- `local` — a directory on the container filesystem (`LOCAL_SOURCE_PATH`, or a
|
|
31
|
+
`needs: disk` mount at `DISK_PATH`).
|
|
32
|
+
- `url` — a manifest of document URLs (`URL_SOURCE_MANIFEST`).
|
|
33
|
+
|
|
34
|
+
Add your own by subclassing `Source` in
|
|
35
|
+
`services/backend/src/app/source.py` and returning it from `get_source()`.
|
|
36
|
+
|
|
37
|
+
## Embeddings
|
|
38
|
+
|
|
39
|
+
Behind one config point (`services/backend/src/app/embeddings.py`), OpenAI by
|
|
40
|
+
default. `EMBEDDINGS_API_KEY` from env; `EMBEDDINGS_MODEL` / `EMBEDDINGS_BASE_URL`
|
|
41
|
+
to point at any OpenAI-compatible provider.
|
|
42
|
+
|
|
43
|
+
## Secrets (set with `grid secrets set KEY=VALUE`)
|
|
44
|
+
|
|
45
|
+
`EMBEDDINGS_API_KEY`, `SOURCE_TYPE`, and the source's own secrets
|
|
46
|
+
(`DROPBOX_APP_KEY` / `DROPBOX_APP_SECRET` / `DROPBOX_REFRESH_TOKEN` /
|
|
47
|
+
`DROPBOX_FOLDER_PATH`, or `LOCAL_SOURCE_PATH`, or `URL_SOURCE_MANIFEST`), and
|
|
48
|
+
`MANAGER_PASSWORD_HASH` (sha256 hex of the manager password).
|
|
49
|
+
|
|
50
|
+
The app starts and passes `GET /backend/health` with **none** of these set —
|
|
51
|
+
search / indexing degrade to a clear "not indexed / missing secret" state.
|
|
52
|
+
|
|
53
|
+
## Health without secrets (baked-in)
|
|
54
|
+
|
|
55
|
+
- `@app.on_event("startup")` calls `db.ensure_indexes()` so the chunks `$text`
|
|
56
|
+
index exists at boot — `$text` returns `[]` on an empty catalog, never a 500.
|
|
57
|
+
- The `$text` query is additionally guarded in `except OperationFailure`.
|
|
58
|
+
- No secret is read at import time; every secret is gated inside a function.
|
|
59
|
+
- `/backend/status` reports capability booleans, never secret values.
|
|
60
|
+
|
|
61
|
+
## Deploy (manager does this)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
grid init semantic-search # creates the entity + .cloudgrid/link.json
|
|
65
|
+
grid secrets set EMBEDDINGS_API_KEY=sk-... # + source secrets + MANAGER_PASSWORD_HASH
|
|
66
|
+
|
|
67
|
+
# Build the static frontend BEFORE plug — CloudGrid validates that a
|
|
68
|
+
# type:static service with a build: block already has services/web/dist/index.html.
|
|
69
|
+
(cd services/web && npm install && npm run build)
|
|
70
|
+
|
|
71
|
+
grid plug # async build + deploy; poll to a live URL
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
See `AGENTS.md` for the full wiring guide.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# cloudgrid.yaml — Complete Example
|
|
3
|
+
# ============================================================================
|
|
4
|
+
#
|
|
5
|
+
# This file shows EVERY field the platform supports, with comments explaining
|
|
6
|
+
# each one. It is a reference — no real project would use all of these at once.
|
|
7
|
+
#
|
|
8
|
+
# STANDARD: every CloudGrid template ships its cloudgrid.yaml in THIS full form.
|
|
9
|
+
# Uncomment ONLY the fields the archetype needs (always: name + services; plus
|
|
10
|
+
# needs: for runtime apps). Leave everything else commented. Comments are
|
|
11
|
+
# ignored by the parser, so the file deploys to exactly the active fields.
|
|
12
|
+
#
|
|
13
|
+
# Only two fields are required: name and services. Everything else is optional.
|
|
14
|
+
# Full docs: docs/cloudgrid-yaml-reference.md
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ─── IDENTITY ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
# Entity slug (REQUIRED). 2-42 chars, lowercase alphanumeric + hyphens.
|
|
21
|
+
# Must start and end with a letter or digit.
|
|
22
|
+
# Becomes part of your public URL: <name>-<4hex>.<org>.cloudgrid.io
|
|
23
|
+
name: semantic-search
|
|
24
|
+
|
|
25
|
+
# One-line summary. Max 280 chars. If omitted, Grid Brain auto-generates one.
|
|
26
|
+
# description: Hybrid semantic + keyword search over your documents
|
|
27
|
+
|
|
28
|
+
# Single emoji or short text shown as the entity icon. Max 64 chars.
|
|
29
|
+
# icon: "🔎"
|
|
30
|
+
|
|
31
|
+
# Entity type: 'app' (default) or 'agent'. An agent is an app with an agent: block.
|
|
32
|
+
# kind: app
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ─── NETWORKING ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
# Whether this entity serves public HTTP traffic.
|
|
38
|
+
# true = public URL + Ingress (default). false = internal only.
|
|
39
|
+
# expose: true
|
|
40
|
+
|
|
41
|
+
# Additional domain names this entity serves on (the platform URL is always active).
|
|
42
|
+
# custom_domains:
|
|
43
|
+
# - orders.example.com
|
|
44
|
+
# - shop.example.com
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ─── HEALTH PROBE (entity-level override) ──────────────────────────────────
|
|
48
|
+
|
|
49
|
+
# Override the default health check for the entire entity.
|
|
50
|
+
# probe:
|
|
51
|
+
# path: /healthz # HTTP path to probe (default: /).
|
|
52
|
+
# expected: "<500" # '<500' (default), '200', '2xx'.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ─── DEPLOY NOTIFICATIONS ──────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
# Slack notification to the org's default channel on deploy failure.
|
|
58
|
+
# true = notify (default), false = silent.
|
|
59
|
+
# notify: true
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ─── SCALING ────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
# Replica scaling intent.
|
|
65
|
+
# 'auto' — platform decides from the grid tier (default).
|
|
66
|
+
# 'alert' — single-replica, emit a SPOF warning at plug time.
|
|
67
|
+
# 1-10 — explicit replica count, clamped to the grid max.
|
|
68
|
+
# scale: auto
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ─── INFRASTRUCTURE NEEDS ──────────────────────────────────────────────────
|
|
72
|
+
#
|
|
73
|
+
# Declare infrastructure; the platform provisions it and injects connection
|
|
74
|
+
# details as env vars. Nine needs: database, cache, kv, queue, pubsub, vector,
|
|
75
|
+
# object_storage, disk, ai. Simplest form is `true`.
|
|
76
|
+
#
|
|
77
|
+
# needs:
|
|
78
|
+
# # database — MongoDB. Injects DATABASE_MONGODB_URL (+ legacy MONGODB_URL).
|
|
79
|
+
# # true | pool | dedicated | { tier: external, secret: MY_DB }
|
|
80
|
+
# database: true
|
|
81
|
+
# # cache — Redis, LRU eviction (derived data). Injects CACHE_REDIS_URL (+ REDIS_URL).
|
|
82
|
+
# # true | redis | { engine: external, secret: MY_REDIS_URL }
|
|
83
|
+
# cache: true
|
|
84
|
+
# # kv — Redis, no eviction (durable: flags, idempotency). Injects KV_REDIS_URL.
|
|
85
|
+
# kv: true
|
|
86
|
+
# # queue — Redis job-queue substrate (BullMQ). Injects QUEUE_REDIS_URL.
|
|
87
|
+
# queue: true
|
|
88
|
+
# # pubsub — Redis broadcast pub/sub. Injects PUBSUB_REDIS_URL.
|
|
89
|
+
# pubsub: true
|
|
90
|
+
# # vector — pgvector on Postgres. Injects VECTOR_PGVECTOR_URL (+ PGVECTOR_URL).
|
|
91
|
+
# # true | pgvector | { engine: pgvector, dim: 1536 }
|
|
92
|
+
# # NOT USED HERE — pgvector runtime role-grant is blocked (platform #1545).
|
|
93
|
+
# # This template stores embeddings in the Mongo `chunks` collection and
|
|
94
|
+
# # cosine-ranks in-app instead. Swap to `vector: pgvector` once #1545 lands.
|
|
95
|
+
# vector: pgvector
|
|
96
|
+
# # object_storage — GCS bucket. Injects OBJECT_STORAGE_GCS_BUCKET, _GCS_REGION.
|
|
97
|
+
# # true | { size: 10Gi }
|
|
98
|
+
# object_storage: true
|
|
99
|
+
# # disk — persistent FS mount (PVC). Injects DISK_PATH. Pins to 1 replica.
|
|
100
|
+
# # true | { size: 1Gi, mount: /var/data } (forbidden: /proc /sys /dev)
|
|
101
|
+
# # Use with SOURCE_TYPE=local to read documents from the mount.
|
|
102
|
+
# disk: true
|
|
103
|
+
# # ai — AI Gateway via @cloudgrid-io/ai. Injects AI_GATEWAY_URL. true | false.
|
|
104
|
+
# ai: true
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ─── INTER-ENTITY COMMUNICATION ────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
# Entities this app calls (outbound). Service-account token injected per pair.
|
|
110
|
+
# calls:
|
|
111
|
+
# - org:atomic/billing-api
|
|
112
|
+
|
|
113
|
+
# Inbound caller policy — who can call YOUR entity.
|
|
114
|
+
# callers:
|
|
115
|
+
# policy: auto # 'auto' | 'manual' (default).
|
|
116
|
+
# scope: org # 'org' (default) | 'space' | 'explicit'.
|
|
117
|
+
# fallback: deny # 'deny' (default) | 'warn'.
|
|
118
|
+
# # allowed_apps: # required only when scope: explicit
|
|
119
|
+
# # - org:atomic/frontend
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ─── VAULT ──────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
# Map org vault items to env vars. Keys UPPER_SNAKE_CASE; values = vault item keys.
|
|
125
|
+
# The source + embeddings secrets can also be stored directly with
|
|
126
|
+
# `grid secrets set KEY=VALUE` — see AGENTS.md.
|
|
127
|
+
# vault:
|
|
128
|
+
# EMBEDDINGS_API_KEY: embeddings-api-key
|
|
129
|
+
# DROPBOX_REFRESH_TOKEN: dropbox-refresh-token
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ─── CONNECTORS AND HOOKS ──────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
# connectors:
|
|
135
|
+
# - id: team-slack
|
|
136
|
+
# target: org # 'system' | 'org'.
|
|
137
|
+
# channel: "#deploys"
|
|
138
|
+
# hooks:
|
|
139
|
+
# - on: deploy.failed # release.published | deploy.failed | feedback.received | entity.created
|
|
140
|
+
# via: team-slack
|
|
141
|
+
# template: "Deploy failed for {entity_name}: {error}"
|
|
142
|
+
# channel: "#alerts"
|
|
143
|
+
# triggers:
|
|
144
|
+
# - from: team-slack
|
|
145
|
+
# event: message
|
|
146
|
+
# channel: "#support"
|
|
147
|
+
# route: /webhooks/slack
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ─── LOCAL DEVELOPMENT ──────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
# Config for `grid dev`. Redact fields in dev data snapshots.
|
|
153
|
+
# dev:
|
|
154
|
+
# snapshot:
|
|
155
|
+
# mask:
|
|
156
|
+
# - users.email
|
|
157
|
+
# - users.phone
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ─── AGENT METADATA ────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
# An agent is an app with kind: agent (above) and an agent: block.
|
|
163
|
+
# agent:
|
|
164
|
+
# purpose: Sync customer data from external CRM every night
|
|
165
|
+
# schedule: "0 2 * * *"
|
|
166
|
+
# trigger: webhook
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ─── PRIVATE RESOURCE AUTO-DEPENDENCIES ────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
# When true (default), private resources are auto-injected as depends_on.
|
|
172
|
+
# auto_depends_on_private_requires: true
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ─── SERVICES (REQUIRED) ───────────────────────────────────────────────────
|
|
176
|
+
#
|
|
177
|
+
# At least one service is required. Key = service name (slug rules).
|
|
178
|
+
# Types: node, nextjs, python, static, cron.
|
|
179
|
+
#
|
|
180
|
+
services:
|
|
181
|
+
# React (Vite) built to static assets, served at the root.
|
|
182
|
+
web:
|
|
183
|
+
type: static
|
|
184
|
+
path: /
|
|
185
|
+
build:
|
|
186
|
+
command: npm run build
|
|
187
|
+
output: dist
|
|
188
|
+
node_version: "22"
|
|
189
|
+
# Python FastAPI backend — hybrid search, answer mode, manager admin.
|
|
190
|
+
# Entry: services/backend/src/main.py (uvicorn app.main:app).
|
|
191
|
+
backend:
|
|
192
|
+
type: python
|
|
193
|
+
path: /backend
|
|
194
|
+
# A scheduled `type: cron` refresh service is a follow-up — a Python cron
|
|
195
|
+
# entity is currently blocked on platform issue #1585. Until it lands, use the
|
|
196
|
+
# manager "Refresh now" endpoint (POST /backend/admin/refresh). See AGENTS.md.
|
|
197
|
+
# refresh:
|
|
198
|
+
# type: cron
|
|
199
|
+
# schedule: "0 3 * * *"
|
|
200
|
+
# timezone: UTC
|
|
201
|
+
# run: job
|
|
202
|
+
# source:
|
|
203
|
+
# path: services/refresh
|
|
204
|
+
# CloudGrid managed Mongo → injects DATABASE_MONGODB_URL (+ legacy MONGODB_URL).
|
|
205
|
+
# NO `needs: vector` — pgvector runtime role-grant is still blocked (#1545);
|
|
206
|
+
# embeddings live in the Mongo `chunks` collection and are cosine-ranked in-app.
|
|
207
|
+
needs:
|
|
208
|
+
database: true
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ============================================================================
|
|
212
|
+
# NOTES
|
|
213
|
+
# - NOT here: org:/entity_id: (→ .cloudgrid/link.json), secrets (→ grid secrets set),
|
|
214
|
+
# Dockerfile (platform generates from services.*.type).
|
|
215
|
+
# - Deploy: grid plug | Local with grid resources: grid dev
|
|
216
|
+
# - Full reference: docs/cloudgrid-yaml-reference.md | CLI: docs/cli-reference.md
|
|
217
|
+
# ============================================================================
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Template: semantic-search (CloudGrid-native document search)
|
|
2
|
+
|
|
3
|
+
A deployable, domain-agnostic **document search** app — index a folder of
|
|
4
|
+
documents (PDF/DOCX/TXT/MD), then search by keyword, by meaning, and by metadata,
|
|
5
|
+
with a grounded answer mode and a manager admin. Two services: a React (Vite)
|
|
6
|
+
static frontend at `/` and a FastAPI backend at `/backend`, backed by
|
|
7
|
+
grid-shared Mongo. This template ships **real, runnable code** — read `AGENTS.md`,
|
|
8
|
+
adapt the naming/metadata to your domain, then deploy.
|
|
9
|
+
|
|
10
|
+
**Hybrid search** = Mongo `$text` (lexical) + in-app NumPy cosine over chunk
|
|
11
|
+
embeddings (semantic) + metadata filters, blended and aggregated to the document
|
|
12
|
+
level with a best-passage highlight.
|
|
13
|
+
|
|
14
|
+
**Key rules (proven by a live build — do not regress these):**
|
|
15
|
+
|
|
16
|
+
1. **Every service's code lives under `services/<name>/`.** `path:` in
|
|
17
|
+
`cloudgrid.yaml` is the URL mount, NOT the filesystem path. The `web` service
|
|
18
|
+
→ `services/web/`; the `backend` service → `services/backend/`.
|
|
19
|
+
2. **Read the Mongo URL LAZILY** — `os.environ.get("DATABASE_MONGODB_URL") or
|
|
20
|
+
os.environ.get("MONGODB_URL")` inside `get_client()`, never at import time.
|
|
21
|
+
3. **Health is green with NO secrets set.** No secret is read at import time;
|
|
22
|
+
every secret is gated inside a function. `GET /backend/health` needs only Mongo.
|
|
23
|
+
4. **`@app.on_event("startup")` calls `db.ensure_indexes()`** so the chunks
|
|
24
|
+
`$text` index exists at boot — `$text` returns `[]` on an empty catalog rather
|
|
25
|
+
than raising `IndexNotFound` (a 500). The `$text` query is also guarded in
|
|
26
|
+
`except OperationFailure` as belt-and-suspenders.
|
|
27
|
+
5. **No `needs: vector`** — pgvector runtime role-grant is blocked (#1545), so
|
|
28
|
+
embeddings live in the Mongo `chunks` collection (a float array) and are
|
|
29
|
+
cosine-ranked in-app. **No active cron** — a Python `type: cron` refresh is
|
|
30
|
+
blocked (#1585); ship the manager "Refresh now" endpoint and add cron later.
|
|
31
|
+
|
|
32
|
+
## File tree
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
cloudgrid.yaml # name + services(web static+build, backend python) + needs:{database:true}
|
|
36
|
+
services/web/ # React + Vite static frontend (LTR, neutral)
|
|
37
|
+
package.json vite.config.js index.html
|
|
38
|
+
src/main.jsx App.jsx api.js styles.css
|
|
39
|
+
src/components/SearchView.jsx ManagerLogin.jsx Settings.jsx Dashboard.jsx
|
|
40
|
+
services/backend/ # FastAPI backend, mounted at /backend
|
|
41
|
+
requirements.txt
|
|
42
|
+
src/main.py # uvicorn entry (reads PORT; no secret at import)
|
|
43
|
+
src/app/
|
|
44
|
+
main.py # routes + startup ensure_indexes()
|
|
45
|
+
config.py # lazy secret gating (SOURCE_TYPE, EMBEDDINGS_*, ...)
|
|
46
|
+
db.py # lazy Mongo; ensure_indexes(); collections documents+chunks
|
|
47
|
+
search.py # hybrid: $text + cosine + metadata, blended to doc level
|
|
48
|
+
indexing.py # discover -> extract -> chunk -> embed -> store (incremental)
|
|
49
|
+
source.py # pluggable adapter: dropbox | local | url
|
|
50
|
+
embeddings.py # one config point (OpenAI-compatible)
|
|
51
|
+
answer.py # grounded answer + citations (extractive fallback)
|
|
52
|
+
auth.py # manager session auth (MANAGER_PASSWORD_HASH)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## cloudgrid.yaml (active fields)
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
# On disk this file is the full-annotated reference (templates/_cloudgrid.yaml.reference)
|
|
59
|
+
# with EVERY field present as a comment; only the fields below are uncommented.
|
|
60
|
+
name: semantic-search
|
|
61
|
+
services:
|
|
62
|
+
web:
|
|
63
|
+
type: static
|
|
64
|
+
path: /
|
|
65
|
+
build:
|
|
66
|
+
command: npm run build
|
|
67
|
+
output: dist
|
|
68
|
+
node_version: "22"
|
|
69
|
+
backend:
|
|
70
|
+
type: python
|
|
71
|
+
path: /backend
|
|
72
|
+
needs:
|
|
73
|
+
database: true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> **Capability:** the need is `database: true` — the deployer provisions Mongo and
|
|
77
|
+
> injects `DATABASE_MONGODB_URL` (+ legacy `MONGODB_URL`). NO `needs: vector`
|
|
78
|
+
> (#1545) and NO active cron (#1585). `requires:` is the deprecated v1 alias —
|
|
79
|
+
> don't mix it with `needs:`.
|
|
80
|
+
|
|
81
|
+
## Configure + deploy
|
|
82
|
+
|
|
83
|
+
1. `grid init semantic-search` — creates the entity + `.cloudgrid/link.json`.
|
|
84
|
+
2. `grid secrets set EMBEDDINGS_API_KEY=sk-…` plus the source secrets
|
|
85
|
+
(`SOURCE_TYPE` + Dropbox/local/url config) and `MANAGER_PASSWORD_HASH`
|
|
86
|
+
(sha256 hex of the manager password). See `AGENTS.md`.
|
|
87
|
+
3. (Optional) `grid dev` to run locally against the injected dev Mongo.
|
|
88
|
+
4. `grid plug` to deploy — a runtime deploy is **async**: poll status until live,
|
|
89
|
+
then sign in as manager and hit "Refresh now" to index the source.
|
|
90
|
+
|
|
91
|
+
## Adapt it
|
|
92
|
+
|
|
93
|
+
- Rename the `collection` / `date` metadata to your domain in
|
|
94
|
+
`services/backend/src/app/indexing.py` (`_parse_metadata`).
|
|
95
|
+
- Add a source adapter: subclass `Source` in `source.py`, return it from
|
|
96
|
+
`get_source()`, gate its secrets in `config.py`.
|
|
97
|
+
- Point embeddings at any OpenAI-compatible provider via `EMBEDDINGS_BASE_URL`.
|
|
File without changes
|