Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from __future__ import annotations

import urllib.error
from collections.abc import Callable
from typing import Any

Expand Down Expand Up @@ -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,
Expand Down
43 changes: 28 additions & 15 deletions scripts/seed_meaningful_demos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:
Expand Down
Loading