From 4a626acc6fc776b88bbc2e968d70e759d0a1c98c Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Fri, 19 Jun 2026 12:20:23 -0400 Subject: [PATCH 1/2] docs(readme): make Solr-only the primary quickstart Lead the Quickstart with a Solr-only path (RELYLOOP_ENGINES=solr make up + make seed-solr) so a fresh clone reaches a running app booting one engine instead of three. Demote the all-three-engines flow to a documented variant. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- README.md | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 47ef9612..f242fe27 100644 --- a/README.md +++ b/README.md @@ -74,21 +74,40 @@ the three-engine reach. ## Quickstart +The fastest path to a running app is **Solr-only** — one engine, one seed step: + ```bash git clone https://github.com/SoundMindsAI/relyloop.git cd relyloop -make up # auto-generates secrets, builds api + ui images, brings up the stack (~5-10 min first build; ~60s warm) -make seed-clusters # register local-es + local-opensearch + local-solr -make seed-es # seed local-es 'products' index from samples/products.json (1,000 docs) +RELYLOOP_ENGINES=solr make up # auto-generates secrets, builds api + ui, starts Solr + app, auto-seeds demo (~5-10 min first build; ~60s warm) +make seed-solr # create the Solr 'products' collection from samples/products.json (1,000 docs) open http://localhost:3000/chat ``` -`make up` runs the Alembic migrations and initializes the Optuna schema -automatically via a `migrate` init container — no separate `make migrate` step -needed for a fresh stack (run `make migrate` only after authoring a new revision -without bouncing the stack). +That's it — clone to running app. `RELYLOOP_ENGINES=solr` starts only the Apache +Solr engine (skipping Elasticsearch + OpenSearch), so the first build pulls and +boots one engine instead of three. `make up` auto-generates all required secrets, +runs the Alembic migrations, initializes the Optuna schema via a `migrate` init +container (no separate `make migrate` step needed for a fresh stack), and +auto-seeds demo data once the stack is healthy. No OpenAI key is required to boot +— the chat UI loads and the stack is fully operational; an LLM endpoint is only +needed to *send* a chat message (see the tutorial's Step 0 for local-LLM setup). + +**All three engines (ES + OpenSearch + Solr).** To run the full three-engine +stack instead, omit the engine selector and seed Elasticsearch as well: + +```bash +make up # default: starts all three engines (heavier first build) +make seed-clusters # register local-es + local-opensearch + local-solr +make seed-es # seed local-es 'products' index from samples/products.json +make seed-solr # seed local-solr 'products' collection +``` + +`RELYLOOP_ENGINES` accepts any comma-separated subset (`es`, `os`, `solr`) — e.g. +`RELYLOOP_ENGINES=es,solr make up`. Run `make migrate` only after authoring a new +Alembic revision without bouncing the stack. Tutorial — the full operator walkthrough from `git clone` through "PR opened in GitHub" — is in From 47ae5f1c065e017df3aa28d3127c47d68e6d02bf Mon Sep 17 00:00:00 2001 From: SoundMindsAI Date: Fri, 19 Jun 2026 12:27:42 -0400 Subject: [PATCH 2/2] fix(seed): tolerate absent engine in demo truncate (solr-only auto-seed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit truncate_demo_state() issued DELETE against the elasticsearch host unconditionally. Under RELYLOOP_ENGINES=solr the ES/OpenSearch containers never start, so the host doesn't resolve and urllib raises URLError — which the 404-only except clause didn't catch, crashing the entire make-up auto-seed before any demo data was written. Extract _truncate_engine_indices() to tolerate URLError (engine not running) and skip that engine, mirroring the per-engine reachability tolerance the seeding scenarios already apply via _engine_reachable(). Noticed during a clean-room solr-only README validation pass. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: SoundMindsAI --- ..._seed_meaningful_demos_engine_tolerance.py | 57 +++++++++++++++++++ scripts/seed_meaningful_demos.py | 43 +++++++++----- 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/backend/tests/unit/scripts/test_seed_meaningful_demos_engine_tolerance.py b/backend/tests/unit/scripts/test_seed_meaningful_demos_engine_tolerance.py index dd356ba9..498f835c 100644 --- a/backend/tests/unit/scripts/test_seed_meaningful_demos_engine_tolerance.py +++ b/backend/tests/unit/scripts/test_seed_meaningful_demos_engine_tolerance.py @@ -17,6 +17,7 @@ from __future__ import annotations +import urllib.error from collections.abc import Callable from typing import Any @@ -136,6 +137,62 @@ def test_all_reachable_seeds_everything_exit_zero( assert rc == 0 +def test_truncate_skips_engine_when_host_unreachable( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """A down engine (URLError on DELETE) is skipped, not propagated. + + Regression for the solr-only auto-seed crash: `RELYLOOP_ENGINES=solr make up` + never starts ES/OpenSearch, so the pre-seed truncate's DELETE against the + `elasticsearch` host raised `URLError` and killed the whole auto-seed before + any data was seeded. The truncate must tolerate an absent engine and move on. + """ + calls: list[str] = [] + + def _fake_http(method: str, url: str, **_kw: Any) -> None: + calls.append(url) + raise urllib.error.URLError("[Errno -2] Name or service not known") + + monkeypatch.setattr(sm, "http", _fake_http) + + # Must not raise even with multiple indices to delete. + sm._truncate_engine_indices("es", "http://elasticsearch:9200", ("a", "b"), ("idx1", "idx2")) + + # Bails after the first unreachable hit — no point retrying every index on a + # host that isn't up. + assert calls == ["http://elasticsearch:9200/idx1"] + assert "es: not reachable" in capsys.readouterr().out + + +def test_truncate_tolerates_404_and_deletes_all_indices( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A 404 (index already gone) is fine; every listed index is still attempted.""" + calls: list[str] = [] + + def _fake_http(method: str, url: str, **_kw: Any) -> None: + calls.append(url) + raise urllib.error.HTTPError(url, 404, "Not Found", {}, None) # type: ignore[arg-type] + + monkeypatch.setattr(sm, "http", _fake_http) + + sm._truncate_engine_indices("os", "http://opensearch:9200", ("a", "b"), ("x", "y")) + + assert calls == ["http://opensearch:9200/x", "http://opensearch:9200/y"] + + +def test_truncate_reraises_non_404_http_error(monkeypatch: pytest.MonkeyPatch) -> None: + """A real server error (non-404) must propagate — it is not engine-absence.""" + + def _fake_http(method: str, url: str, **_kw: Any) -> None: + raise urllib.error.HTTPError(url, 500, "Server Error", {}, None) # type: ignore[arg-type] + + monkeypatch.setattr(sm, "http", _fake_http) + + with pytest.raises(urllib.error.HTTPError): + sm._truncate_engine_indices("es", "http://elasticsearch:9200", ("a", "b"), ("idx",)) + + def test_rich_openai_skip_uses_separate_summary_not_engine_unreachable( monkeypatch: pytest.MonkeyPatch, patched_io: _Calls, diff --git a/scripts/seed_meaningful_demos.py b/scripts/seed_meaningful_demos.py index 82d9b6ec..e8612db1 100644 --- a/scripts/seed_meaningful_demos.py +++ b/scripts/seed_meaningful_demos.py @@ -3007,6 +3007,32 @@ def _psql(sql: str) -> None: ) +def _truncate_engine_indices( + engine: str, host: str, auth: tuple[str, str], indices: tuple[str, ...] +) -> None: + """DELETE each demo index from one engine, tolerating an absent engine. + + A 404 means the index was already gone — fine. A ``URLError`` (DNS failure + or refused connection) means the engine container isn't running at all, + which is the normal case under a single-engine startup like + ``RELYLOOP_ENGINES=solr make up`` (ES + OpenSearch are never started). There + is nothing to truncate for an engine that isn't up, so skip the rest of its + indices rather than crashing the whole auto-seed. Mirrors the per-engine + reachability tolerance the seeding scenarios already apply via + ``_engine_reachable()``. + """ + for idx in indices: + print(f" {engine}: DELETE /{idx}") + try: + http("DELETE", f"{host}/{idx}", auth=auth) + except urllib.error.HTTPError as e: + if e.code != 404: + raise + except urllib.error.URLError as e: + print(f" {engine}: not reachable ({e.reason}) — skipping truncate") + return + + def truncate_demo_state() -> None: """Wipe every demo-owned row from Postgres + every demo index from ES/OS. @@ -3018,21 +3044,8 @@ def truncate_demo_state() -> None: print(f" postgres: TRUNCATE {tables} RESTART IDENTITY CASCADE") _psql(f"TRUNCATE {tables} RESTART IDENTITY CASCADE;") - for idx in DEMO_ES_INDICES: - print(f" es: DELETE /{idx}") - try: - http("DELETE", f"{ES}/{idx}", auth=ES_AUTH) - except urllib.error.HTTPError as e: - if e.code != 404: - raise - - for idx in DEMO_OS_INDICES: - print(f" os: DELETE /{idx}") - try: - http("DELETE", f"{OS}/{idx}", auth=OS_AUTH) - except urllib.error.HTTPError as e: - if e.code != 404: - raise + _truncate_engine_indices("es", ES, ES_AUTH, DEMO_ES_INDICES) + _truncate_engine_indices("os", OS, OS_AUTH, DEMO_OS_INDICES) def apply_study_renames(results: list[dict]) -> None: