diff --git a/.gitignore b/.gitignore
index 6bd1a98c..4d6999ba 100755
--- a/.gitignore
+++ b/.gitignore
@@ -151,6 +151,10 @@ demo/memcell_outputs/
# macOS
.DS_Store
+
+# AI agent tooling (local agent skills/config, not part of the project)
+.agents/
+AGENTS.md
*.duckdb
diff --git a/README.md b/README.md
index c63c4008..a0f4ce5d 100644
--- a/README.md
+++ b/README.md
@@ -122,16 +122,20 @@ Run this before configuring API keys or starting the server:
everos demo
```
-The command asks for one memory and one recall question, then opens a
-full-screen terminal UI. This is an educational visualizer: it is hardcoded,
-local to the CLI, and does not connect to the EverOS server. Its job is to make
-the memory lifecycle visible: conversation -> memory sphere -> recall -> source
-proof -> confetti. See [docs/everos-demo.md](docs/everos-demo.md) for the demo
-scope and TUI source layout.
+The full-screen terminal UI has an input box: type a memory, then a recall
+question, directly in the UI. Each round runs the real memory pipeline against
+[EverOS Cloud](https://everos.evermind.ai) (keys and storage live on the
+platform, so you need no local keys), and the panels follow your own input:
+conversation -> memory sphere ->
+recall -> source proof -> confetti. See
+[docs/everos-demo.md](docs/everos-demo.md) for the demo scope and TUI source
+layout.
The sphere moves through ingest, extraction, indexing, recall, source reveal,
-and a confetti burst after the first memory lands. Press `r` to replay and `q`
-to quit.
+and a confetti burst after the first memory lands. Type `/` to see the
+commands (`/replay`, `/live`, `/quit`); `ctrl+c` exits anytime. After a few
+rounds the demo points you at configuring your own keys (`everos init`, then
+`everos demo --live`).
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 2d7feb15..0741e894 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -124,15 +124,17 @@ uv pip install everos
everos demo
```
-这个命令会询问一条记忆和一个召回问题,然后打开一个全屏 terminal UI。
-这是一个 educational visualizer:它是 hardcoded 的,只在 CLI 本地运行,
-不会连接 EverOS server。它的作用是把 memory lifecycle 变成可感知的过程:
+全屏 terminal UI 里带有一个输入框:你直接在 UI 里输入一条记忆,再输入一个
+召回问题。每一轮都会对 [EverOS Cloud](https://everos.evermind.ai) 跑真实的
+memory pipeline(key 和存储都在平台侧,本地无需任何 API keys),各面板跟着
+你自己的输入更新:
conversation -> memory sphere -> recall -> source proof -> confetti。Demo
范围和 TUI 代码结构见 [docs/everos-demo.md](docs/everos-demo.md)。
Sphere 会经历 ingest、extraction、indexing、recall、source reveal,
-并在第一条记忆落地后进入 confetti successful moment。按 `r` 可以 replay,
-按 `q` 可以退出。
+并在第一条记忆落地后进入 confetti successful moment。输入 `/` 可以查看可用命令
+(`/replay`、`/live`、`/quit`);`ctrl+c` 随时退出。几轮之后,demo 会引导你去
+配置自己的 key(`everos init`,然后 `everos demo --live`)。
diff --git a/docs/everos-demo.md b/docs/everos-demo.md
index 53b3c209..21393370 100644
--- a/docs/everos-demo.md
+++ b/docs/everos-demo.md
@@ -1,8 +1,8 @@
# EverOS Demo
-`everos demo` is a local educational TUI. It helps new users feel the memory
-lifecycle before they configure API keys, start the server, or write real
-memory through the API.
+`everos demo` is an interactive TUI that lets new users feel the memory
+lifecycle — type a memory, ask for it back, watch EverOS recall it — before they
+configure their own API keys.
## Run It
@@ -10,48 +10,50 @@ memory through the API.
everos demo
```
-The command asks for one memory and one recall question, then opens a
-full-screen terminal UI. The visual flow is deterministic and local to the CLI:
-conversation -> memory sphere -> recall -> source proof -> confetti.
+This opens a full-screen terminal UI with an input box. You type a memory and a
+recall question directly in the UI, and each round runs the **real** memory
+pipeline against [EverOS Cloud](https://everos.evermind.ai) (`https://api.evermind.ai`):
+`POST /api/v1/memories` -> `POST /api/v1/memories/flush` ->
+`POST /api/v1/memories/search`. The panels follow your own input: conversation
+-> memory sphere -> recall -> source proof -> confetti.
-For non-interactive shells or a copyable preview, use:
+EverOS Cloud holds all model keys and manages storage, so the demo needs only a
+single platform API key — no server to deploy, no model keys locally. Each run
+uses a fresh, isolated `(session_id, user_id)` pair, so demo visitors never see
+each other's memories.
-```bash
-everos demo --plain
-```
-
-For the looping showroom view used by README media, use:
+The demo key is read from the `EVEROS_CLOUD_DEMO_KEY` environment variable (a
+restricted, shippable key can be baked in later). If no key is set, the key is
+rejected, or the quota is exhausted, the UI says so and points you at
+configuring your own key — it never fakes a result.
-```bash
-everos demo --cinematic
-```
+The endpoint can be overridden with `EVEROS_CLOUD_DEMO_URL` (or `--server-url`).
-## Run It Against A Server
+## Run It With Your Own Cloud Key
-After `everos init` and `everos server start`, run:
+Get a key from , then:
```bash
+export EVEROS_CLOUD_API_KEY=
everos demo --live
```
-Live mode keeps the same TUI, but the memory lifecycle is backed by real
-server calls:
+`--live` runs the same flow against the same platform, just authenticated with
+your own key instead of the restricted demo key.
-1. `GET /health`
-2. `POST /api/v1/memory/add`
-3. `POST /api/v1/memory/flush`
-4. `POST /api/v1/memory/search`
+## Static Previews
-If your server is not running on `http://127.0.0.1:8000`, pass
-`--server-url `.
+For non-interactive shells or a copyable preview (no input box, no network):
-## What It Does Not Do
+```bash
+everos demo --plain
+```
+
+For the looping showroom view used by README media:
-By default, `everos demo` does not connect to the EverOS server, call LLM
-providers, or write production memory files. It is intentionally hardcoded so
-users can try the experience before configuring the full runtime. Use
-`everos demo --live` when you want the same visual flow backed by a running
-server.
+```bash
+everos demo --cinematic
+```
## Source Layout
@@ -60,8 +62,9 @@ because the public command is still `everos demo`.
The TUI implementation lives under `src/everos/entrypoints/tui/demo/`:
-- `app.py` renders the Textual app.
-- `data.py` builds the deterministic demo story.
+- `app.py` renders the Textual app and drives the interactive rounds.
+- `cloud.py` is the EverOS Cloud HTTP client (`add -> flush -> search`).
+- `data.py` holds the static showcase story for `--plain` / `--cinematic`.
- `widgets/sphere.py` builds the memory sphere frames.
- `readme_media.py` renders README media.
diff --git a/src/everos/component/tokenizer/jieba_provider.py b/src/everos/component/tokenizer/jieba_provider.py
index ab083aa9..d01a80bf 100644
--- a/src/everos/component/tokenizer/jieba_provider.py
+++ b/src/everos/component/tokenizer/jieba_provider.py
@@ -18,10 +18,19 @@
from __future__ import annotations
+import warnings
from collections.abc import Sequence
from typing import Final
-import jieba
+with warnings.catch_warnings():
+ # jieba 0.42.1 (its last release; unmaintained) ships regex string
+ # literals with invalid escape sequences. Python >= 3.12 flags these as
+ # SyntaxWarning at first-import compile time (before the .pyc is cached),
+ # leaking noise on a user's first run. The warnings are harmless and out
+ # of our control, so we suppress them at the single jieba import site.
+ # See https://github.com/EverMind-AI/EverOS/issues/304.
+ warnings.simplefilter("ignore", SyntaxWarning)
+ import jieba
# Small bilingual stopword set. Intentionally tight (not a full
# Chinese stopword list) so the behaviour is predictable; callers
diff --git a/src/everos/entrypoints/cli/commands/demo.py b/src/everos/entrypoints/cli/commands/demo.py
index 12c3463a..159cbbb7 100644
--- a/src/everos/entrypoints/cli/commands/demo.py
+++ b/src/everos/entrypoints/cli/commands/demo.py
@@ -1,27 +1,24 @@
-"""``everos demo`` — first-run memory sphere demo."""
+"""``everos demo`` — first-run memory sphere demo.
+
+The default command launches an interactive Textual TUI: the user types memories
+and recall questions directly in the UI, and each round runs the *real* memory
+pipeline against a hosted EverOS server (keys live server-side; see
+:mod:`everos.entrypoints.tui.demo.cloud`). ``--plain`` / ``--cinematic`` are
+static, no-network renderings for non-interactive shells and README media.
+"""
from __future__ import annotations
-import json
+import getpass
+import subprocess
import sys
-import time
-import urllib.error
-import urllib.request
-from collections.abc import Callable
-from typing import Any
import typer
from rich.console import Console
from rich.panel import Panel
-from everos.component.utils.datetime import get_utc_now
-from everos.entrypoints.tui.demo.data import (
- DEFAULT_MEMORY_SEED,
- DEFAULT_QUERY,
- DemoStory,
- build_demo_story,
- default_demo_story,
-)
+from everos.entrypoints.tui.demo import cloud
+from everos.entrypoints.tui.demo.data import DemoStory, default_demo_story
from everos.entrypoints.tui.demo.widgets.sphere import (
EVEROS_GREEN,
EVEROS_YELLOW,
@@ -29,15 +26,6 @@
render_dot_sphere_text,
)
-LIVE_DEMO_SERVER_URL = "http://127.0.0.1:8000"
-LIVE_DEMO_SESSION_ID = "everos-demo-live"
-LIVE_DEMO_USER_ID = "everos_demo_user"
-LIVE_DEMO_APP_ID = "default"
-LIVE_DEMO_PROJECT_ID = "default"
-LIVE_DEMO_TIMEOUT_SECONDS = 10.0
-LIVE_DEMO_SEARCH_ATTEMPTS = 6
-LIVE_DEMO_SEARCH_INTERVAL_SECONDS = 0.5
-
def register(parent: typer.Typer) -> None:
"""Attach the ``demo`` command to the root CLI app."""
@@ -52,53 +40,83 @@ def demo(
cinematic: bool = typer.Option(
False,
"--cinematic",
- help="Skip prompts and launch the looping README-style demo.",
+ help="Launch the looping README-style showcase (no input box).",
),
live: bool = typer.Option(
False,
"--live",
- help="Connect to a running EverOS server and run add/flush/search.",
+ help="Use your own EverOS Cloud API key (env EVEROS_CLOUD_API_KEY).",
+ ),
+ cloud_mode: bool = typer.Option(
+ False,
+ "--cloud",
+ help="Run against EverOS Cloud with the demo key (this is the default).",
),
server_url: str = typer.Option(
- LIVE_DEMO_SERVER_URL,
+ cloud.LIVE_DEMO_SERVER_URL,
"--server-url",
- help="EverOS server URL used by --live.",
+ help="Override the EverOS Cloud API base URL.",
),
) -> None:
"""Launch the EverOS first-memory Textual TUI."""
- if live:
- _run_live_demo(
- cinematic=cinematic,
- plain=plain or not sys.stdout.isatty(),
- base_url=server_url,
- )
- return
-
if plain or not sys.stdout.isatty():
_print_plain_demo()
return
- _run_interactive_demo(cinematic=cinematic)
+ user_label = _resolve_local_user()
+ if cinematic:
+ _load_run_demo_tui()(user_label=user_label)
+ return
+
+ _launch_interactive_demo(
+ live=live, server_url=server_url, user_label=user_label
+ )
-def _run_interactive_demo(*, cinematic: bool) -> None:
+def _launch_interactive_demo(
+ *, live: bool, server_url: str, user_label: str = "you"
+) -> None:
+ """Launch the cloud-platform interactive TUI.
+
+ Both modes talk to EverOS Cloud; ``--live`` just uses the user's own
+ platform key instead of the restricted demo key.
+ """
+
run_demo_tui = _load_run_demo_tui()
- story = None if cinematic else _collect_playable_story()
- run_demo_tui(story=story)
+ base_url = cloud.resolve_cloud_base_url(server_url)
+ session_id, user_id = cloud.new_demo_identity()
+ api_key = cloud.resolve_user_key() if live else cloud.resolve_demo_key()
+ run_demo_tui(
+ interactive=True,
+ base_url=base_url,
+ session_id=session_id,
+ user_id=user_id,
+ api_key=api_key,
+ user_label=user_label,
+ )
-def _run_live_demo(*, cinematic: bool, plain: bool, base_url: str) -> None:
- run_demo_tui = None if plain else _load_run_demo_tui()
- story = default_demo_story() if cinematic or plain else _collect_playable_story()
- live_story = _run_live_demo_flow(story, base_url=base_url)
- if plain:
- _print_plain_demo(live_story)
- return
+def _resolve_local_user() -> str:
+ """Local-first display name: the clone's git identity, else the OS user."""
- if run_demo_tui is None: # pragma: no cover - guarded by plain branch.
- raise typer.Exit(code=1)
- run_demo_tui(story=live_story)
+ try:
+ result = subprocess.run(
+ ["git", "config", "user.name"],
+ capture_output=True,
+ text=True,
+ timeout=2,
+ check=False,
+ )
+ name = result.stdout.strip()
+ except (OSError, subprocess.SubprocessError):
+ name = ""
+ if name:
+ return name
+ try:
+ return getpass.getuser()
+ except Exception:
+ return "you"
def _load_run_demo_tui():
@@ -118,178 +136,6 @@ def _load_run_demo_tui():
return run_demo_tui
-def _collect_playable_story() -> DemoStory:
- Console().print(
- f"[bold {EVEROS_YELLOW}]EverOS demo[/] "
- "Give it one memory, then ask for it back."
- )
- memory = typer.prompt(
- "Give EverOS one thing to remember",
- default=DEFAULT_MEMORY_SEED,
- )
- query = typer.prompt(
- "Ask EverOS to recall it",
- default=DEFAULT_QUERY,
- )
- return build_demo_story(memory, query)
-
-
-def _run_live_demo_flow(
- story: DemoStory,
- *,
- base_url: str,
- request_json: Callable[..., dict[str, Any]] | None = None,
- timeout_seconds: float = LIVE_DEMO_TIMEOUT_SECONDS,
- search_attempts: int = LIVE_DEMO_SEARCH_ATTEMPTS,
- search_interval_seconds: float = LIVE_DEMO_SEARCH_INTERVAL_SECONDS,
-) -> DemoStory:
- """Run the educational demo story through a live EverOS server."""
-
- request = request_json or _request_json
- health = request(
- "GET",
- "/health",
- base_url=base_url,
- timeout_seconds=timeout_seconds,
- )
- if health.get("status") != "ok":
- raise typer.BadParameter(
- f"EverOS server at {base_url} did not return healthy status"
- )
-
- timestamp_ms = int(get_utc_now().timestamp() * 1000)
- request(
- "POST",
- "/api/v1/memory/add",
- base_url=base_url,
- json_body={
- "session_id": LIVE_DEMO_SESSION_ID,
- "app_id": LIVE_DEMO_APP_ID,
- "project_id": LIVE_DEMO_PROJECT_ID,
- "messages": [
- {
- "sender_id": LIVE_DEMO_USER_ID,
- "role": "user",
- "timestamp": timestamp_ms,
- "content": story.memory,
- }
- ],
- },
- timeout_seconds=timeout_seconds,
- )
- request(
- "POST",
- "/api/v1/memory/flush",
- base_url=base_url,
- json_body={
- "session_id": LIVE_DEMO_SESSION_ID,
- "app_id": LIVE_DEMO_APP_ID,
- "project_id": LIVE_DEMO_PROJECT_ID,
- },
- timeout_seconds=timeout_seconds,
- )
-
- search_payload = {
- "user_id": LIVE_DEMO_USER_ID,
- "app_id": LIVE_DEMO_APP_ID,
- "project_id": LIVE_DEMO_PROJECT_ID,
- "query": story.query,
- "top_k": 5,
- }
- for attempt in range(search_attempts):
- search = request(
- "POST",
- "/api/v1/memory/search",
- base_url=base_url,
- json_body=search_payload,
- timeout_seconds=timeout_seconds,
- )
- episode = _first_live_episode(search)
- if episode is not None:
- return _story_from_live_episode(story, episode)
- if attempt < search_attempts - 1:
- time.sleep(search_interval_seconds)
-
- raise typer.BadParameter(
- "EverOS server accepted the memory, but search did not return it yet. "
- "Try `everos demo --live` again after indexing catches up."
- )
-
-
-def _request_json(
- method: str,
- path: str,
- *,
- base_url: str,
- json_body: dict[str, object] | None = None,
- timeout_seconds: float,
-) -> dict[str, Any]:
- url = f"{base_url.rstrip('/')}{path}"
- data = None if json_body is None else json.dumps(json_body).encode("utf-8")
- request = urllib.request.Request(
- url,
- data=data,
- method=method,
- headers={"Content-Type": "application/json"},
- )
- try:
- with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
- raw = response.read().decode("utf-8")
- except urllib.error.URLError as exc:
- raise typer.BadParameter(
- f"Could not reach EverOS server at {base_url}. "
- "Start it with `everos server start` and try again."
- ) from exc
- if not raw:
- return {}
- parsed = json.loads(raw)
- if not isinstance(parsed, dict):
- raise typer.BadParameter(f"EverOS server returned non-object JSON: {url}")
- return parsed
-
-
-def _first_live_episode(payload: dict[str, Any]) -> dict[str, Any] | None:
- data = payload.get("data")
- if not isinstance(data, dict):
- return None
- episodes = data.get("episodes")
- if not isinstance(episodes, list) or not episodes:
- return None
- first = episodes[0]
- return first if isinstance(first, dict) else None
-
-
-def _story_from_live_episode(story: DemoStory, episode: dict[str, Any]) -> DemoStory:
- facts = episode.get("atomic_facts")
- first_fact = facts[0] if isinstance(facts, list) and facts else None
- fact_id = _string_field(first_fact, "id") if isinstance(first_fact, dict) else ""
- answer = (
- _string_field(first_fact, "content") if isinstance(first_fact, dict) else ""
- )
- if not answer:
- answer = (
- _string_field(episode, "summary")
- or _string_field(episode, "episode")
- or story.answer
- )
- episode_id = _string_field(episode, "id") or "live"
- return DemoStory(
- owner=LIVE_DEMO_USER_ID,
- memory=story.memory,
- query=story.query,
- answer=answer,
- source_filename=f"episode:{episode_id}",
- fact_filename=f"fact:{fact_id or 'live'}",
- )
-
-
-def _string_field(payload: dict[str, Any] | None, key: str) -> str:
- if payload is None:
- return ""
- value = payload.get(key)
- return value if isinstance(value, str) else ""
-
-
def _print_plain_demo(story: DemoStory | None = None) -> None:
story = story or default_demo_story()
console = Console()
diff --git a/src/everos/entrypoints/tui/demo/app.py b/src/everos/entrypoints/tui/demo/app.py
index b1eabafc..e1b37321 100644
--- a/src/everos/entrypoints/tui/demo/app.py
+++ b/src/everos/entrypoints/tui/demo/app.py
@@ -2,13 +2,26 @@
from __future__ import annotations
+from functools import partial
+
+import anyio
from rich.text import Text
+from textual import on
from textual.app import App, ComposeResult
+from textual.binding import Binding
from textual.containers import Horizontal, Vertical
+from textual.message import Message
from textual.timer import Timer
-from textual.widgets import Footer, Static
-
-from everos.entrypoints.tui.demo.data import DemoStory, default_demo_story
+from textual.widgets import Footer, Input, Static
+
+from everos.component.utils.datetime import today_with_timezone
+from everos.entrypoints.tui.demo import cloud
+from everos.entrypoints.tui.demo.data import (
+ DEFAULT_MEMORY_SEED,
+ DEFAULT_QUERY,
+ DemoStory,
+ default_demo_story,
+)
from everos.entrypoints.tui.demo.widgets.sphere import (
EVEROS_AMBER,
EVEROS_AMBER_DIM,
@@ -31,6 +44,36 @@
SPHERE_FRAME_HEIGHT = 17
TERMINAL_CELL_HEIGHT_RATIO = 2.0
SIGNAL_RAIL_SOURCE_WIDTH = 18
+# Offline default demo: how many memory -> recall rounds a user plays before the
+# TUI nudges them toward the real pipeline (`--cloud` / `--live`).
+DEFAULT_DEMO_ROUNDS = 3
+
+# Sphere animation cadence. Each named state (and its highlighted trace word)
+# dwells for SPHERE_STAGE_SECONDS so a viewer can read the stage it represents.
+SPHERE_FPS = 12
+SPHERE_STAGE_SECONDS = 3.0
+SPHERE_STAGE_TICKS = round(SPHERE_FPS * SPHERE_STAGE_SECONDS)
+
+# The four pipeline stages shown in the trace header. They line up with the four
+# core sphere states, so the active word can highlight in sync with the sphere.
+TRACE_STAGES = ("ingest", "extract", "index", "recall")
+
+# Words a user can type in the input box to quit back to the terminal.
+QUIT_COMMANDS = frozenset({"quit", "exit", ":q", "/quit", "/exit"})
+_STATE_TO_STAGE = {
+ "ingesting": 0,
+ "extracting": 1,
+ "indexing": 2,
+ "recalling": 3,
+ "remembered": 3,
+ "source": 3,
+}
+
+
+def _state_to_stage(state_key: str) -> int:
+ """Map a sphere state to its trace-stage index (-1 = no stage highlighted)."""
+
+ return _STATE_TO_STAGE.get(state_key, -1)
class DotSphereWidget(Static):
@@ -54,14 +97,22 @@ class DotSphereWidget(Static):
"celebrating",
)
+ class StageChanged(Message):
+ """Posted when the sphere enters a different trace stage."""
+
+ def __init__(self, stage: int) -> None:
+ self.stage = stage
+ super().__init__()
+
def __init__(self) -> None:
super().__init__()
self._phase = 0.0
self._tick = 0
+ self._last_stage = -2
self._animation_timer: Timer | None = None
def on_mount(self) -> None:
- self._animation_timer = self.set_interval(1 / 12, self._advance)
+ self._animation_timer = self.set_interval(1 / SPHERE_FPS, self._advance)
self._advance()
def pause_animation(self) -> None:
@@ -71,7 +122,7 @@ def pause_animation(self) -> None:
def _advance(self) -> None:
self._phase = (self._phase + 0.025) % 1.0
self._tick += 1
- state = self.STATES[(self._tick // 36) % len(self.STATES)]
+ state = self.STATES[(self._tick // SPHERE_STAGE_TICKS) % len(self.STATES)]
frame = build_dot_sphere(
width=SPHERE_FRAME_WIDTH,
height=SPHERE_FRAME_HEIGHT,
@@ -80,13 +131,59 @@ def _advance(self) -> None:
)
self.update(render_dot_sphere_text(frame))
+ stage = _state_to_stage(state)
+ if stage != self._last_stage:
+ self._last_stage = stage
+ self.post_message(self.StageChanged(stage))
+
+
+class QueryAnswerBar(Static):
+ """Query <-> Answer bar with a marker that propagates back and forth."""
+
+ TRACK_WIDTH = 11
+
+ def __init__(self, **kwargs: object) -> None:
+ super().__init__(**kwargs)
+ self._pos = 0
+ self._dir = 1
+ self._timer: Timer | None = None
+
+ def on_mount(self) -> None:
+ self._timer = self.set_interval(0.1, self._advance)
+
+ def _advance(self) -> None:
+ self._pos += self._dir
+ if self._pos >= self.TRACK_WIDTH - 1:
+ self._pos = self.TRACK_WIDTH - 1
+ self._dir = -1
+ elif self._pos <= 0:
+ self._pos = 0
+ self._dir = 1
+ self.refresh()
+
+ def render(self) -> Text:
+ glyph = "▶" if self._dir > 0 else "◀"
+ left = "·" * self._pos
+ right = "·" * (self.TRACK_WIDTH - 1 - self._pos)
+ return Text.assemble(
+ ("Query ", f"bold {EVEROS_CYAN}"),
+ (f" {left}", EVEROS_AMBER),
+ (glyph, f"bold {EVEROS_YELLOW}"),
+ (f"{right} ", EVEROS_AMBER),
+ ("Answer", f"bold {EVEROS_GREEN}"),
+ )
+
class EverOSDemoApp(App[None]):
"""Fullscreen first-run demo cockpit."""
TITLE = "EverOS Memory Core"
SUB_TITLE = "dot sphere demo"
+ # ctrl+c / ctrl+q are priority bindings so they quit even while the input
+ # box has focus (where a bare "q" would just be typed into the field).
BINDINGS = [
+ Binding("ctrl+c", "quit", "Quit", priority=True, show=False),
+ Binding("ctrl+q", "quit", "Quit", priority=True),
("q", "quit", "Quit"),
("r", "replay", "Replay"),
]
@@ -133,12 +230,29 @@ class EverOSDemoApp(App[None]):
border-top: hkey {EVEROS_AMBER_DIM};
background: {EVEROS_SURFACE_RAISED};
padding: 0 1;
+ content-align: center middle;
}}
- #signal-rail {{
+ #right-rail {{
width: 48;
height: 100%;
margin-left: 1;
+ }}
+
+ #capabilities {{
+ height: 9;
+ border: panel {EVEROS_YELLOW};
+ border-title-color: {EVEROS_BLACK};
+ border-title-background: {EVEROS_YELLOW};
+ border-title-style: bold;
+ background: {EVEROS_SURFACE_RAISED};
+ padding: 0 2;
+ margin-bottom: 1;
+ }}
+
+ #signal-rail {{
+ width: 100%;
+ height: 1fr;
border: round {EVEROS_AMBER};
background: {EVEROS_SURFACE};
padding: 1 2;
@@ -164,16 +278,32 @@ class EverOSDemoApp(App[None]):
padding: 0 2;
}}
- #payoff {{
- height: 2;
+ #conversation {{
+ height: 4;
border-top: hkey {EVEROS_YELLOW};
background: {EVEROS_SURFACE};
color: {EVEROS_INK};
padding: 0 1;
margin-top: 1;
+ }}
+
+ #console {{
+ height: 2;
+ margin-top: 1;
+ }}
+
+ #console-prompt {{
+ height: 1;
+ padding: 0 1;
content-align: left middle;
}}
+ #console-input {{
+ height: 1;
+ border: none;
+ background: {EVEROS_SURFACE_RAISED};
+ }}
+
Footer {{
background: {EVEROS_BLACK};
color: {EVEROS_MUTED};
@@ -195,9 +325,35 @@ class EverOSDemoApp(App[None]):
}}
"""
- def __init__(self, *, story: DemoStory | None = None) -> None:
+ def __init__(
+ self,
+ *,
+ story: DemoStory | None = None,
+ interactive: bool = False,
+ base_url: str = cloud.CLOUD_API_BASE_URL,
+ session_id: str = cloud.LIVE_DEMO_SESSION_ID,
+ user_id: str = cloud.LIVE_DEMO_USER_ID,
+ api_key: str = "",
+ user_label: str = "you",
+ max_rounds: int = DEFAULT_DEMO_ROUNDS,
+ ) -> None:
super().__init__()
self._story = story or default_demo_story()
+ self._interactive = interactive
+ self._base_url = base_url
+ self._session_id = session_id
+ self._user_id = user_id
+ self._api_key = api_key
+ self._user_label = user_label
+ self._max_rounds = max_rounds
+ self._round = 0
+ self._active_stage = -1
+ self._conversation_phase = "memory"
+ self._pending_memory = ""
+ self._lights = _initial_lights()
+ self._log: list[tuple[str, str]] = []
+ self._history_chars = 0
+ self._saved_pct: int | None = None
def compose(self) -> ComposeResult:
with Vertical(id="shell"):
@@ -206,22 +362,265 @@ def compose(self) -> ComposeResult:
memory_field = Vertical(id="memory-field")
memory_field.border_title = "memory field"
with memory_field:
- yield Static(_field_header_text(self._story), id="field-header")
+ yield Static(
+ _field_header_text(
+ user_label=self._user_label,
+ active_stage=self._active_stage,
+ ),
+ id="field-header",
+ )
yield DotSphereWidget()
- yield Static(_sphere_caption(self._story), id="field-answer")
- signal_rail = Static(_signal_rail_text(self._story), id="signal-rail")
- signal_rail.border_title = "signal rail"
- yield signal_rail
+ yield QueryAnswerBar(id="field-answer")
+ with Vertical(id="right-rail"):
+ capabilities = Static(_capabilities_text(), id="capabilities")
+ capabilities.border_title = "EverOS strengths"
+ yield capabilities
+ signal_rail = Static(
+ _signal_rail_text(self._lights), id="signal-rail"
+ )
+ signal_rail.border_title = "signal rail"
+ yield signal_rail
with Horizontal(id="provenance-strip"):
- source_lock = Static(_source_tree_text(self._story), id="source-lock")
+ source_lock = Static(_source_tree_text(), id="source-lock")
source_lock.border_title = "source lock"
yield source_lock
- recall_lock = Static(_recall_proof_text(self._story), id="recall-lock")
+ recall_lock = Static(
+ _recall_proof_text(self._story, user_label=self._user_label),
+ id="recall-lock",
+ )
recall_lock.border_title = "recall lock"
yield recall_lock
- yield Static(_payoff_text(self._story), id="payoff")
+ conversation = Static(_conversation_text(self._log), id="conversation")
+ conversation.border_title = "conversation"
+ yield conversation
+ if self._interactive:
+ with Vertical(id="console"):
+ yield Static(
+ _prompt_memory_text(self._round, self._max_rounds),
+ id="console-prompt",
+ )
+ yield Input(
+ placeholder=(
+ "type a memory & enter · /help for commands · "
+ "/quit to exit"
+ ),
+ id="console-input",
+ )
yield Footer(show_command_palette=False)
+ def on_mount(self) -> None:
+ if self._interactive:
+ self.query_one("#console-input", Input).focus()
+
+ @on(DotSphereWidget.StageChanged)
+ def _on_stage_changed(self, event: DotSphereWidget.StageChanged) -> None:
+ self._active_stage = event.stage
+ self.query_one("#field-header", Static).update(
+ _field_header_text(
+ user_label=self._user_label,
+ active_stage=self._active_stage,
+ )
+ )
+
+ def on_input_submitted(self, event: Input.Submitted) -> None:
+ if not self._interactive or self._conversation_phase in {"recalling", "done"}:
+ return
+ value = event.value.strip()
+ if value.lower() in QUIT_COMMANDS:
+ self.exit()
+ return
+ if value.startswith("/"):
+ self._run_slash_command(value.lower())
+ return
+ prompt = self.query_one("#console-prompt", Static)
+ field = self.query_one("#console-input", Input)
+ if self._conversation_phase == "memory":
+ self._pending_memory = value or DEFAULT_MEMORY_SEED
+ self._conversation_phase = "query"
+ prompt.update(_prompt_query_text())
+ field.value = ""
+ return
+
+ # Query submitted: run the real cloud round off the event loop so the UI
+ # (sphere animation, input) stays responsive while we wait on the server.
+ query = value or DEFAULT_QUERY
+ self._conversation_phase = "recalling"
+ field.value = ""
+ field.disabled = True
+ prompt.update(_recalling_text())
+ self.run_worker(
+ self._recall(self._pending_memory, query),
+ group="recall",
+ exclusive=True,
+ )
+
+ def on_input_changed(self, event: Input.Changed) -> None:
+ # Live slash-command panel: as soon as the user types "/", surface the
+ # available commands; restore the phase prompt once they type real text.
+ if not self._interactive or self._conversation_phase in {"recalling", "done"}:
+ return
+ prompt = self.query_one("#console-prompt", Static)
+ if event.value.startswith("/"):
+ prompt.update(_help_text())
+ elif event.value:
+ prompt.update(self._phase_prompt())
+
+ def _run_slash_command(self, command: str) -> None:
+ prompt = self.query_one("#console-prompt", Static)
+ self.query_one("#console-input", Input).value = ""
+ if command in {"/help", "/?"}:
+ prompt.update(_help_text())
+ elif command == "/live":
+ prompt.update(_live_guidance_text())
+ elif command == "/replay":
+ self.action_replay()
+ prompt.update(self._phase_prompt())
+ elif command == "/clear":
+ self._log.clear()
+ self.query_one("#conversation", Static).update(
+ _conversation_text(self._log)
+ )
+ prompt.update(self._phase_prompt())
+ else:
+ prompt.update(_unknown_command_text(command))
+
+ def _phase_prompt(self) -> Text:
+ if self._conversation_phase == "query":
+ return _prompt_query_text()
+ return _prompt_memory_text(self._round, self._max_rounds)
+
+ async def _recall(self, memory: str, query: str) -> None:
+ # Reset the per-round lights; each step below lights up as it completes,
+ # so the signal rail mirrors the real add -> flush -> search pipeline.
+ self._reset_round_lights()
+ base_url, session_id, user_id, api_key = (
+ self._base_url,
+ self._session_id,
+ self._user_id,
+ self._api_key,
+ )
+ try:
+ task_id = await anyio.to_thread.run_sync(
+ partial(
+ cloud.add_memory,
+ memory,
+ base_url=base_url,
+ session_id=session_id,
+ user_id=user_id,
+ api_key=api_key,
+ )
+ )
+ # A successful add means the key authenticated and the memory landed.
+ self._set_light("core", "ready")
+ self._set_light("conversation", "captured")
+ await anyio.to_thread.run_sync(
+ partial(cloud.wait_task, task_id, base_url=base_url, api_key=api_key)
+ )
+ await anyio.to_thread.run_sync(
+ partial(
+ cloud.flush_memory,
+ base_url=base_url,
+ session_id=session_id,
+ user_id=user_id,
+ api_key=api_key,
+ )
+ )
+ self._set_light("facts", "live")
+ self._set_light("index", "synced")
+ story = await anyio.to_thread.run_sync(
+ partial(
+ cloud.search_recall,
+ memory,
+ query,
+ base_url=base_url,
+ user_id=user_id,
+ api_key=api_key,
+ )
+ )
+ except cloud.CloudQuotaError:
+ self._enter_done(_quota_guidance_text())
+ return
+ except cloud.CloudDemoError as exc:
+ self._set_light("core", "error")
+ self._show_recall_error(str(exc))
+ return
+
+ if story is None:
+ self._set_light("recall", "miss")
+ answer = "(no matching memory found)"
+ self._record_turn(query, answer)
+ story = DemoStory(
+ owner=user_id,
+ memory=memory,
+ query=query,
+ answer=answer,
+ source_filename="",
+ fact_filename="",
+ )
+ else:
+ self._set_light("recall", "hit")
+ self._record_turn(story.query, story.answer)
+ self._update_savings(memory, query, story.answer)
+ self._finish_round(story)
+
+ def _update_savings(self, memory: str, query: str, answer: str) -> None:
+ # Estimate (not measured): carrying the whole conversation as LLM context
+ # vs. EverOS handing back only the compact recalled answer. Char counts
+ # are a token proxy; the ratio is what matters, so the /4 cancels out.
+ self._history_chars += len(memory) + len(query) + len(answer)
+ if self._history_chars:
+ ratio = 1 - len(answer) / self._history_chars
+ self._saved_pct = max(0, min(99, round(100 * ratio)))
+
+ def _finish_round(self, story: DemoStory) -> None:
+ self._story = story
+ self.query_one("#recall-lock", Static).update(
+ _recall_proof_text(
+ story, user_label=self._user_label, saved_pct=self._saved_pct
+ )
+ )
+ self.action_replay()
+ self._round += 1
+ if self._round >= self._max_rounds:
+ self._enter_done(_quota_guidance_text())
+ return
+ self._conversation_phase = "memory"
+ self.query_one("#console-prompt", Static).update(
+ _prompt_memory_text(self._round, self._max_rounds)
+ )
+ self._reenable_input()
+
+ def _reset_round_lights(self) -> None:
+ self._lights.update(
+ conversation="idle", facts="idle", index="idle", recall="idle"
+ )
+ self.query_one("#signal-rail", Static).update(_signal_rail_text(self._lights))
+
+ def _set_light(self, key: str, state: str) -> None:
+ self._lights[key] = state
+ self.query_one("#signal-rail", Static).update(_signal_rail_text(self._lights))
+
+ def _record_turn(self, query: str, answer: str) -> None:
+ self._log.append((query, answer))
+ self.query_one("#conversation", Static).update(_conversation_text(self._log))
+
+ def _enter_done(self, message: Text) -> None:
+ self._conversation_phase = "done"
+ self.query_one("#console-prompt", Static).update(message)
+ self.query_one("#console-input", Input).disabled = True
+
+ def _show_recall_error(self, message: str) -> None:
+ # Recall failed (server unreachable, unhealthy, or slow). Surface the
+ # reason honestly and let the user retry a fresh round.
+ self._conversation_phase = "memory"
+ self.query_one("#console-prompt", Static).update(_recall_error_text(message))
+ self._reenable_input()
+
+ def _reenable_input(self) -> None:
+ field = self.query_one("#console-input", Input)
+ field.disabled = False
+ field.focus()
+
def action_replay(self) -> None:
widget = self.query_one(DotSphereWidget)
widget._tick = 0
@@ -229,117 +628,247 @@ def action_replay(self) -> None:
widget._advance()
-def run_demo_tui(*, story: DemoStory | None = None) -> None:
- EverOSDemoApp(story=story).run()
+def run_demo_tui(
+ *,
+ story: DemoStory | None = None,
+ interactive: bool = False,
+ base_url: str = cloud.CLOUD_API_BASE_URL,
+ session_id: str = cloud.LIVE_DEMO_SESSION_ID,
+ user_id: str = cloud.LIVE_DEMO_USER_ID,
+ api_key: str = "",
+ user_label: str = "you",
+) -> None:
+ EverOSDemoApp(
+ story=story,
+ interactive=interactive,
+ base_url=base_url,
+ session_id=session_id,
+ user_id=user_id,
+ api_key=api_key,
+ user_label=user_label,
+ ).run()
+
+
+def _prompt_memory_text(round_index: int, total_rounds: int) -> Text:
+ if round_index == 0:
+ return Text("What should EverOS remember?", style=f"bold {EVEROS_YELLOW}")
+ return Text.assemble(
+ (f"round {round_index + 1}/{total_rounds} ", EVEROS_MUTED),
+ ("what should EverOS remember next?", f"bold {EVEROS_YELLOW}"),
+ )
-def _hero_text() -> Text:
+def _prompt_query_text() -> Text:
+ return Text("Now ask EverOS to recall it.", style=f"bold {EVEROS_CYAN}")
+
+
+def _recalling_text() -> Text:
+ return Text("recalling from EverOS...", style=f"bold {EVEROS_ORANGE}")
+
+
+def _recall_error_text(message: str) -> Text:
return Text.assemble(
- (" everos demo ", f"bold black on {EVEROS_YELLOW}"),
- (" memory core ", f"bold {EVEROS_YELLOW}"),
- ("online", EVEROS_MUTED),
+ ("could not reach the demo server ", f"bold {EVEROS_ORANGE}"),
+ (f"({message}) ", EVEROS_MUTED),
+ ("set EVEROS_CLOUD_DEMO_URL or use --live; type to retry", EVEROS_INK),
)
-def _field_header_text(story: DemoStory | None = None) -> Text:
- story = story or default_demo_story()
+def _help_text() -> Text:
return Text.assemble(
- (f"user={story.owner}", f"bold {EVEROS_INK}"),
- (" scope=local-first", f"bold {EVEROS_YELLOW_SOFT}"),
- (" trace ", EVEROS_MUTED),
- ("conversation -> facts -> index", f"bold {EVEROS_YELLOW}"),
- (" live", f"bold {EVEROS_ORANGE}"),
+ ("commands ", f"bold {EVEROS_YELLOW}"),
+ ("/help", f"bold {EVEROS_GREEN}"),
+ (" list ", EVEROS_MUTED),
+ ("/live", f"bold {EVEROS_GREEN}"),
+ (" use your key ", EVEROS_MUTED),
+ ("/replay", f"bold {EVEROS_GREEN}"),
+ (" re-run ", EVEROS_MUTED),
+ ("/clear", f"bold {EVEROS_GREEN}"),
+ (" wipe log ", EVEROS_MUTED),
+ ("/quit", f"bold {EVEROS_GREEN}"),
+ (" exit", EVEROS_MUTED),
)
-def _sphere_caption(story: DemoStory | None = None) -> Text:
- story = story or default_demo_story()
+def _live_guidance_text() -> Text:
return Text.assemble(
- ("query ", f"bold {EVEROS_CYAN}"),
- (f"{story.query} ", EVEROS_INK),
- ("-> ", EVEROS_MUTED),
- ("answer ", f"bold {EVEROS_GREEN}"),
- (story.answer, f"bold {EVEROS_GREEN}"),
+ ("use your own key ", f"bold {EVEROS_YELLOW}"),
+ ("everos init", f"bold {EVEROS_GREEN}"),
+ (" then ", EVEROS_MUTED),
+ ("everos demo --live", f"bold {EVEROS_GREEN}"),
)
-def _signal_rail_text(story: DemoStory | None = None) -> Text:
- story = story or default_demo_story()
+def _unknown_command_text(command: str) -> Text:
+ return Text.assemble(
+ (f"unknown command {command} ", f"bold {EVEROS_ORANGE}"),
+ ("try /help", EVEROS_INK),
+ )
+
+
+def _quota_guidance_text() -> Text:
+ return Text.assemble(
+ ("free demo rounds used up ", f"bold {EVEROS_YELLOW}"),
+ ("configure your own key -> ", EVEROS_INK),
+ ("everos init", f"bold {EVEROS_GREEN}"),
+ (" then ", EVEROS_MUTED),
+ ("everos demo --live", f"bold {EVEROS_GREEN}"),
+ )
+
+
+def _hero_text() -> Text:
return Text.assemble(
- ("● ", f"bold {EVEROS_GREEN}"),
- ("memory core ", EVEROS_INK),
- ("ready\n", f"bold {EVEROS_GREEN}"),
- ("● ", f"bold {EVEROS_YELLOW_SOFT}"),
- ("conversation ", EVEROS_INK),
- ("captured\n", f"bold {EVEROS_YELLOW_SOFT}"),
- ("● ", f"bold {EVEROS_ORANGE}"),
- ("episode -> facts ", EVEROS_INK),
- ("live\n", f"bold {EVEROS_ORANGE}"),
- ("● ", f"bold {EVEROS_CYAN}"),
- ("SQLite + LanceDB ", EVEROS_INK),
- ("synced\n", f"bold {EVEROS_CYAN}"),
- ("● ", f"bold {EVEROS_GREEN}"),
- ("memory recall ", EVEROS_INK),
- ("hit\n", f"bold {EVEROS_GREEN}"),
- ("\nsource route\n", EVEROS_MUTED),
- (_rail_cell(story.source_filename), EVEROS_INK),
- (" attached\n", f"bold {EVEROS_YELLOW_SOFT}"),
- (_rail_cell(story.fact_filename), EVEROS_INK),
- (" 7 nodes\n", f"bold {EVEROS_ORANGE}"),
- ("lancedb orbit ", EVEROS_INK),
- ("synced\n", f"bold {EVEROS_CYAN}"),
- ("\nrecall proof\n", EVEROS_MUTED),
- ("score ", EVEROS_INK),
- ("0.628\n", f"bold {EVEROS_GREEN}"),
- ("source ", EVEROS_INK),
- (f"{story.source_filename}\n", f"bold {EVEROS_CYAN}"),
- ("field integrity\n", EVEROS_MUTED),
- ("█████████░ 92%\n", f"bold {EVEROS_YELLOW}"),
- ("latency ", EVEROS_MUTED),
- ("42 ms\n", f"bold {EVEROS_GREEN}"),
- ("mode ", EVEROS_MUTED),
- ("local-first", f"bold {EVEROS_INK}"),
+ (" everos demo ", f"bold black on {EVEROS_YELLOW}"),
+ (" memory core ", f"bold {EVEROS_YELLOW}"),
+ ("online", EVEROS_MUTED),
)
+def _field_header_text(*, user_label: str = "you", active_stage: int = -1) -> Text:
+ parts: list[tuple[str, str]] = [
+ (f"user={user_label}", f"bold {EVEROS_INK}"),
+ (" scope=local-first", f"bold {EVEROS_YELLOW_SOFT}"),
+ (" trace ", EVEROS_MUTED),
+ ]
+ for index, stage in enumerate(TRACE_STAGES):
+ if index:
+ parts.append((" · ", EVEROS_MUTED))
+ if index == active_stage:
+ parts.append((stage, f"bold {EVEROS_YELLOW}"))
+ else:
+ parts.append((stage, EVEROS_AMBER))
+ return Text.assemble(*parts)
+
+
+def _initial_lights() -> dict[str, str]:
+ """Default signal-rail state before any round runs."""
+
+ return {
+ "core": "not_ready",
+ "conversation": "idle",
+ "facts": "idle",
+ "index": "idle",
+ "recall": "idle",
+ }
+
+
+# White = not ready / idle / miss; yellow = ready / active / hit; black = error.
+_LIGHT_YELLOW = frozenset({"ready", "captured", "live", "synced", "hit"})
+
+
+def _light_color(state: str) -> str:
+ if state in _LIGHT_YELLOW:
+ return EVEROS_YELLOW
+ if state == "error":
+ return EVEROS_BLACK
+ return EVEROS_INK
+
+
+def _light_label(state: str) -> str:
+ return "not ready" if state == "not_ready" else state
+
+
+_SIGNAL_ROWS = (
+ ("core", "memory core "),
+ ("conversation", "conversation "),
+ ("facts", "episode -> facts "),
+ ("index", "SQLite + LanceDB "),
+ ("recall", "memory recall "),
+)
+
+
+def _signal_rail_text(lights: dict[str, str] | None = None) -> Text:
+ lights = lights or _initial_lights()
+ parts: list[tuple[str, str]] = []
+ for key, label in _SIGNAL_ROWS:
+ state = lights.get(key, "idle")
+ color = _light_color(state)
+ parts.append(("● ", f"bold {color}"))
+ parts.append((label, EVEROS_INK))
+ parts.append((f"{_light_label(state)}\n", f"bold {color}"))
+ parts.append(("\nsource route\n", EVEROS_MUTED))
+ parts.append((_rail_cell(_demo_episode_name()), EVEROS_INK))
+ parts.append((" attached\n", f"bold {EVEROS_YELLOW_SOFT}"))
+ parts.append((_rail_cell(_demo_fact_name()), EVEROS_INK))
+ parts.append((" stored", f"bold {EVEROS_ORANGE}"))
+ return Text.assemble(*parts)
+
+
def _rail_cell(value: str, *, width: int = SIGNAL_RAIL_SOURCE_WIDTH) -> str:
if len(value) > width:
return f"{value[: width - 3]}..."
return f"{value:<{width}}"
-def _source_tree_text(story: DemoStory | None = None) -> Text:
- story = story or default_demo_story()
+def _demo_episode_name() -> str:
+ """Date-stamped episode filename reflecting when the demo is used."""
+
+ return f"episode-{today_with_timezone().isoformat()}.md"
+
+
+def _demo_fact_name() -> str:
+ return f"atomic_fact-{today_with_timezone().isoformat()}.md"
+
+
+def _capabilities_text() -> Text:
+ # Real highlights from evermind.ai: the token-efficiency claim, one headline
+ # SOTA benchmark, and core capabilities. No fabricated figures. (local-first
+ # is dropped here because the field header already shows scope=local-first.)
+ rows = (
+ ("token efficiency ", "1/10 of full context", EVEROS_YELLOW),
+ ("LoCoMo ", "93.05% (SOTA)", EVEROS_GREEN),
+ ("context window ", "unlimited", EVEROS_CYAN),
+ ("hybrid retrieval ", "BM25 + vector", EVEROS_ORANGE),
+ ("agentic rerank ", "on", EVEROS_YELLOW_SOFT),
+ ("multimodal ", "pdf / image / docs", EVEROS_INK),
+ ("self-evolving ", "cases -> skills", EVEROS_GREEN),
+ )
+ parts: list[tuple[str, str]] = []
+ for label, value, color in rows:
+ parts.append((label, EVEROS_MUTED))
+ parts.append((f"{value}\n", f"bold {color}"))
+ return Text.assemble(*parts)
+
+
+def _source_tree_text() -> Text:
return Text.assemble(
("episode ", EVEROS_MUTED),
- (f"{story.source_filename}\n", f"bold {EVEROS_YELLOW_SOFT}"),
+ (f"{_demo_episode_name()}\n", f"bold {EVEROS_YELLOW_SOFT}"),
("facts ", EVEROS_MUTED),
- (f"{story.fact_filename}\n", f"bold {EVEROS_ORANGE}"),
+ (f"{_demo_fact_name()}\n", f"bold {EVEROS_ORANGE}"),
("index ", EVEROS_MUTED),
("sqlite/system.db + lancedb/*.lance\n", EVEROS_CYAN),
("root ", EVEROS_MUTED),
- ("~/.everos/default_app/default_project", EVEROS_INK),
+ ("~/.everos/default_app/demo", EVEROS_INK),
)
-def _recall_proof_text(story: DemoStory | None = None) -> Text:
+def _recall_proof_text(
+ story: DemoStory | None = None,
+ *,
+ user_label: str = "you",
+ saved_pct: int | None = None,
+) -> Text:
story = story or default_demo_story()
+ score = f"{story.score:.3f}" if story.score else "—"
+ saved = f"~{saved_pct}% tokens (est)" if saved_pct is not None else "—"
return Text.assemble(
("score ", EVEROS_MUTED),
- ("0.628\n", f"bold {EVEROS_GREEN}"),
+ (f"{score}\n", f"bold {EVEROS_GREEN}"),
+ ("saved ", EVEROS_MUTED),
+ (f"{saved}\n", f"bold {EVEROS_YELLOW}"),
("scope ", EVEROS_MUTED),
- (f"user={story.owner} project=default\n", EVEROS_INK),
- ("answer ", EVEROS_MUTED),
- (story.answer, f"bold {EVEROS_YELLOW}"),
+ (f"user={user_label} project=demo", EVEROS_INK),
)
-def _payoff_text(story: DemoStory | None = None) -> Text:
- story = story or default_demo_story()
- return Text.assemble(
- ("memory formed: ", f"bold {EVEROS_YELLOW}"),
- (
- f"EverOS recalled {story.answer} and kept the source attached.",
- f"bold {EVEROS_INK}",
- ),
- )
+def _conversation_text(log: list[tuple[str, str]]) -> Text:
+ if not log:
+ return Text("your input and EverOS output will appear here", style=EVEROS_MUTED)
+ parts: list[tuple[str, str]] = []
+ for query, answer in log:
+ parts.append(("you ", f"bold {EVEROS_CYAN}"))
+ parts.append((f"{query}\n", EVEROS_INK))
+ parts.append(("everos ", f"bold {EVEROS_GREEN}"))
+ parts.append((f"{answer}\n", EVEROS_INK))
+ return Text.assemble(*parts)
diff --git a/src/everos/entrypoints/tui/demo/cloud.py b/src/everos/entrypoints/tui/demo/cloud.py
new file mode 100644
index 00000000..0e3abe6a
--- /dev/null
+++ b/src/everos/entrypoints/tui/demo/cloud.py
@@ -0,0 +1,328 @@
+"""Cloud-platform HTTP client for ``everos demo``.
+
+The interactive demo runs the *real* memory pipeline against the EverOS Cloud
+platform (``https://api.evermind.ai``). The platform holds all model keys and
+manages storage, so a user experiences genuine extraction and recall by passing
+a single platform API key — no server to deploy, no DNS, no model keys locally.
+
+Auth is ``Authorization: Bearer ``. The default demo uses a restricted
+demo key (env ``EVEROS_CLOUD_DEMO_KEY``); ``--live`` uses the user's own
+platform key (env ``EVEROS_CLOUD_API_KEY``).
+
+One round is: ``add`` (async, returns a task) -> wait for the task -> ``flush``
+(force extraction) -> poll ``search``. Each run uses a fresh
+``(session_id, user_id)`` pair so demo visitors never read each other's memory.
+
+The functions here are typer-free on purpose: they are called from the Textual
+TUI worker. Failures raise :class:`CloudDemoError` (or the more specific
+:class:`CloudQuotaError` / :class:`CloudAuthError`); callers decide how to
+surface them.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import time
+import urllib.error
+import urllib.request
+import uuid
+from collections.abc import Callable
+from typing import Any
+
+from everos.component.utils.datetime import get_utc_now
+from everos.entrypoints.tui.demo.data import DemoStory
+
+# Sentinel default for the --server-url option; a different value means the user
+# explicitly pointed the demo somewhere else.
+LIVE_DEMO_SERVER_URL = "http://127.0.0.1:8000"
+LIVE_DEMO_SESSION_ID = "everos-demo-live"
+LIVE_DEMO_USER_ID = "everos_demo_user"
+
+CLOUD_API_BASE_URL = "https://api.evermind.ai"
+CLOUD_DEMO_SERVER_URL_ENV = "EVEROS_CLOUD_DEMO_URL"
+CLOUD_DEMO_KEY_ENV = "EVEROS_CLOUD_DEMO_KEY"
+CLOUD_USER_KEY_ENV = "EVEROS_CLOUD_API_KEY"
+# Restricted, shippable demo key goes here once the platform issues one. Empty
+# means the demo reads the key from the env var instead (and otherwise reports
+# "not configured").
+DEFAULT_CLOUD_DEMO_KEY = ""
+
+TIMEOUT_SECONDS = 15.0
+TASK_ATTEMPTS = 12
+TASK_INTERVAL_SECONDS = 1.0
+SEARCH_ATTEMPTS = 8
+SEARCH_INTERVAL_SECONDS = 1.5
+
+
+class CloudDemoError(Exception):
+ """A cloud demo round could not be completed."""
+
+
+class CloudQuotaError(CloudDemoError):
+ """The platform hit a rate/quota limit (HTTP 429)."""
+
+
+class CloudAuthError(CloudDemoError):
+ """The platform rejected the API key (HTTP 401/403)."""
+
+
+def resolve_cloud_base_url(server_url: str) -> str:
+ """Pick the API endpoint: explicit --server-url wins, then env, then default."""
+
+ if server_url != LIVE_DEMO_SERVER_URL:
+ return server_url
+ return os.environ.get(CLOUD_DEMO_SERVER_URL_ENV, CLOUD_API_BASE_URL)
+
+
+def resolve_demo_key() -> str:
+ """The restricted demo key: env override, else the shipped default."""
+
+ return os.environ.get(CLOUD_DEMO_KEY_ENV, DEFAULT_CLOUD_DEMO_KEY)
+
+
+def resolve_user_key() -> str:
+ """The user's own platform key for --live (env only)."""
+
+ return os.environ.get(CLOUD_USER_KEY_ENV, "")
+
+
+def new_demo_identity() -> tuple[str, str]:
+ """Generate a unique ``(session_id, user_id)`` pair for one demo run."""
+
+ token = uuid.uuid4().hex[:12]
+ return f"everos-demo-{token}", f"everos_demo_{token}"
+
+
+def add_memory(
+ memory: str,
+ *,
+ base_url: str,
+ session_id: str,
+ user_id: str,
+ api_key: str,
+ request_json: Callable[..., dict[str, Any]] | None = None,
+ timeout_seconds: float = TIMEOUT_SECONDS,
+) -> str:
+ """Queue one user memory; returns the async task id. Blocking."""
+
+ request = request_json or _request_json
+ timestamp_ms = int(get_utc_now().timestamp() * 1000)
+ resp = request(
+ "POST",
+ "/api/v1/memories",
+ base_url=base_url,
+ api_key=api_key,
+ json_body={
+ "user_id": user_id,
+ "session_id": session_id,
+ "messages": [
+ {"role": "user", "timestamp": timestamp_ms, "content": memory}
+ ],
+ },
+ timeout_seconds=timeout_seconds,
+ )
+ data = resp.get("data")
+ return _string_field(data if isinstance(data, dict) else None, "task_id")
+
+
+def wait_task(
+ task_id: str,
+ *,
+ base_url: str,
+ api_key: str,
+ request_json: Callable[..., dict[str, Any]] | None = None,
+ attempts: int = TASK_ATTEMPTS,
+ interval_seconds: float = TASK_INTERVAL_SECONDS,
+ timeout_seconds: float = TIMEOUT_SECONDS,
+) -> None:
+ """Best-effort poll of the async add task until it finishes. Blocking.
+
+ Transient errors (e.g. a 404 before the task registers) are tolerated; if
+ the task never reports success we return anyway and let flush/search drive
+ eventual consistency. A reported failure raises.
+ """
+
+ if not task_id:
+ return
+ request = request_json or _request_json
+ for attempt in range(attempts):
+ status = ""
+ try:
+ resp = request(
+ "GET",
+ f"/api/v1/tasks/{task_id}",
+ base_url=base_url,
+ api_key=api_key,
+ timeout_seconds=timeout_seconds,
+ )
+ data = resp.get("data") if isinstance(resp.get("data"), dict) else resp
+ status = _string_field(data, "status")
+ except CloudQuotaError:
+ raise
+ except CloudDemoError:
+ status = "" # task not registered yet / transient
+ if status in {"success", "completed", "done"}:
+ return
+ if status in {"failed", "error"}:
+ raise CloudDemoError("memory processing failed")
+ if attempt < attempts - 1:
+ time.sleep(interval_seconds)
+
+
+def flush_memory(
+ *,
+ base_url: str,
+ session_id: str,
+ user_id: str,
+ api_key: str,
+ request_json: Callable[..., dict[str, Any]] | None = None,
+ timeout_seconds: float = TIMEOUT_SECONDS,
+) -> None:
+ """Force extraction of the session into episodes/facts. Blocking."""
+
+ request = request_json or _request_json
+ request(
+ "POST",
+ "/api/v1/memories/flush",
+ base_url=base_url,
+ api_key=api_key,
+ json_body={"user_id": user_id, "session_id": session_id, "force": True},
+ timeout_seconds=timeout_seconds,
+ )
+
+
+def search_recall(
+ memory: str,
+ query: str,
+ *,
+ base_url: str,
+ user_id: str,
+ api_key: str,
+ request_json: Callable[..., dict[str, Any]] | None = None,
+ search_attempts: int = SEARCH_ATTEMPTS,
+ search_interval_seconds: float = SEARCH_INTERVAL_SECONDS,
+ timeout_seconds: float = TIMEOUT_SECONDS,
+) -> DemoStory | None:
+ """Search the query, polling while indexing catches up.
+
+ Returns a :class:`DemoStory` (with the real recall score) on a hit, or
+ ``None`` on a miss (the platform answered but returned nothing). Blocking.
+ """
+
+ request = request_json or _request_json
+ payload = {
+ "query": query,
+ "filters": {"user_id": user_id},
+ "method": "hybrid",
+ "top_k": 5,
+ }
+ for attempt in range(search_attempts):
+ search = request(
+ "POST",
+ "/api/v1/memories/search",
+ base_url=base_url,
+ api_key=api_key,
+ json_body=payload,
+ timeout_seconds=timeout_seconds,
+ )
+ episode = _first_live_episode(search)
+ if episode is not None:
+ return _story_from_live_episode(memory, query, episode, user_id=user_id)
+ if attempt < search_attempts - 1:
+ time.sleep(search_interval_seconds)
+ return None
+
+
+def _request_json(
+ method: str,
+ path: str,
+ *,
+ base_url: str,
+ api_key: str | None = None,
+ json_body: dict[str, object] | None = None,
+ timeout_seconds: float,
+) -> dict[str, Any]:
+ url = f"{base_url.rstrip('/')}{path}"
+ data = None if json_body is None else json.dumps(json_body).encode("utf-8")
+ headers = {"Content-Type": "application/json"}
+ if api_key:
+ headers["Authorization"] = f"Bearer {api_key}"
+ request = urllib.request.Request(url, data=data, method=method, headers=headers)
+ try:
+ with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
+ raw = response.read().decode("utf-8")
+ except urllib.error.HTTPError as exc:
+ if exc.code in (401, 403):
+ raise CloudAuthError(
+ "EverOS Cloud rejected the API key (set EVEROS_CLOUD_DEMO_KEY)."
+ ) from exc
+ if exc.code == 429:
+ raise CloudQuotaError(base_url) from exc
+ raise CloudDemoError(
+ f"EverOS Cloud at {base_url} returned HTTP {exc.code}."
+ ) from exc
+ except urllib.error.URLError as exc:
+ raise CloudDemoError(f"Could not reach EverOS Cloud at {base_url}.") from exc
+ if not raw:
+ return {}
+ parsed = json.loads(raw)
+ if not isinstance(parsed, dict):
+ raise CloudDemoError(f"EverOS Cloud returned non-object JSON: {url}")
+ return parsed
+
+
+def _first_live_episode(payload: dict[str, Any]) -> dict[str, Any] | None:
+ data = payload.get("data")
+ if not isinstance(data, dict):
+ return None
+ episodes = data.get("episodes")
+ if not isinstance(episodes, list) or not episodes:
+ return None
+ first = episodes[0]
+ return first if isinstance(first, dict) else None
+
+
+def _story_from_live_episode(
+ memory: str,
+ query: str,
+ episode: dict[str, Any],
+ *,
+ user_id: str,
+) -> DemoStory:
+ facts = episode.get("atomic_facts")
+ first_fact = facts[0] if isinstance(facts, list) and facts else None
+ fact = first_fact if isinstance(first_fact, dict) else None
+ fact_id = _string_field(fact, "id")
+ # Cloud puts the recalled content in ``atomic_fact`` and the score on the
+ # fact (episode-level score is null).
+ answer = _string_field(fact, "atomic_fact") or (
+ _string_field(episode, "summary")
+ or _string_field(episode, "episode")
+ or memory
+ )
+ score = _float_field(fact, "score") or _float_field(episode, "score")
+ episode_id = _string_field(episode, "id") or "live"
+ return DemoStory(
+ owner=user_id,
+ memory=memory,
+ query=query,
+ answer=answer,
+ source_filename=f"episode:{episode_id}",
+ fact_filename=f"fact:{fact_id or 'live'}",
+ score=score,
+ )
+
+
+def _string_field(payload: dict[str, Any] | None, key: str) -> str:
+ if payload is None:
+ return ""
+ value = payload.get(key)
+ return value if isinstance(value, str) else ""
+
+
+def _float_field(payload: dict[str, Any] | None, key: str) -> float:
+ if payload is None:
+ return 0.0
+ value = payload.get(key)
+ return float(value) if isinstance(value, int | float) else 0.0
diff --git a/src/everos/entrypoints/tui/demo/data.py b/src/everos/entrypoints/tui/demo/data.py
index c5e23305..89132384 100644
--- a/src/everos/entrypoints/tui/demo/data.py
+++ b/src/everos/entrypoints/tui/demo/data.py
@@ -18,10 +18,16 @@ class DemoStory:
answer: str
source_filename: str
fact_filename: str
+ score: float = 0.0
def default_demo_story() -> DemoStory:
- """Return the cinematic story used by README media and no-prompt previews."""
+ """Return the cinematic story used by README media and no-prompt previews.
+
+ This is only the static showcase content for ``--plain`` / ``--cinematic``.
+ The interactive demo builds its story from real server recall (see
+ :func:`everos.entrypoints.tui.demo.cloud.search_recall`).
+ """
return DemoStory(
owner="alice",
@@ -31,45 +37,3 @@ def default_demo_story() -> DemoStory:
source_filename="episode-2026-06-20.md",
fact_filename="atomic_fact-2026-06-20.md",
)
-
-
-def build_demo_story(
- memory_seed: str | None = None,
- query: str | None = None,
-) -> DemoStory:
- """Build a playable demo story from one user memory and one recall query."""
-
- memory = _clean(memory_seed, DEFAULT_MEMORY_SEED)
- recall_query = _clean(query, DEFAULT_QUERY)
- return DemoStory(
- owner="you",
- memory=memory,
- query=recall_query,
- answer=_derive_demo_answer(memory),
- source_filename="episode-demo.md",
- fact_filename="atomic_fact-demo.md",
- )
-
-
-def _clean(value: str | None, fallback: str) -> str:
- if value is None:
- return fallback
- stripped = value.strip()
- return stripped or fallback
-
-
-def _derive_demo_answer(memory: str) -> str:
- """Keep the demo deterministic without pretending to run the server."""
-
- lower_memory = memory.lower()
- if "yosemite" in lower_memory:
- if "spring" in lower_memory:
- return "Yosemite every spring"
- return "Yosemite"
- return _compact(memory)
-
-
-def _compact(text: str, *, limit: int = 66) -> str:
- if len(text) <= limit:
- return text
- return f"{text[: limit - 3].rstrip()}..."
diff --git a/src/everos/entrypoints/tui/demo/widgets/sphere.py b/src/everos/entrypoints/tui/demo/widgets/sphere.py
index 8c15c34d..971acd9b 100644
--- a/src/everos/entrypoints/tui/demo/widgets/sphere.py
+++ b/src/everos/entrypoints/tui/demo/widgets/sphere.py
@@ -99,7 +99,7 @@ def caption(self) -> str:
),
"remembered": SphereState(
key="remembered",
- caption="remembered Yosemite preference",
+ caption="found the matching memory",
accent=EVEROS_YELLOW,
),
"source": SphereState(
diff --git a/tests/unit/test_entrypoints/test_cli/test_demo_command.py b/tests/unit/test_entrypoints/test_cli/test_demo_command.py
index adc01d13..03fbcd8c 100644
--- a/tests/unit/test_entrypoints/test_cli/test_demo_command.py
+++ b/tests/unit/test_entrypoints/test_cli/test_demo_command.py
@@ -4,26 +4,16 @@
import re
-import pytest
import typer
from rich.panel import Panel
from typer.testing import CliRunner
from everos.entrypoints.cli.commands import demo as demo_command
-from everos.entrypoints.tui.demo.data import build_demo_story
+from everos.entrypoints.tui.demo import cloud
+from everos.entrypoints.tui.demo.data import DemoStory
-def test_demo_help_exposes_cinematic_mode() -> None:
- app = typer.Typer()
- demo_command.register(app)
-
- result = CliRunner().invoke(app, ["demo", "--help"], terminal_width=120)
-
- assert result.exit_code == 0
- assert "--cinematic" in _strip_ansi(result.stdout)
-
-
-def test_demo_help_exposes_live_mode() -> None:
+def test_demo_help_exposes_all_modes() -> None:
app = typer.Typer()
demo_command.register(app)
@@ -31,47 +21,8 @@ def test_demo_help_exposes_live_mode() -> None:
help_text = _strip_ansi(result.stdout)
assert result.exit_code == 0
- assert "--live" in help_text
- assert "--server-url" in help_text
-
-
-def test_collect_playable_story_prompts_for_memory_then_query(monkeypatch) -> None:
- prompts: list[tuple[str, str]] = []
- replies = iter(
- [
- "I keep my Monday design review notes in Notion.",
- "Where are my Monday review notes?",
- ]
- )
-
- def fake_prompt(label: str, *, default: str) -> str:
- prompts.append((label, default))
- return next(replies)
-
- monkeypatch.setattr(demo_command.typer, "prompt", fake_prompt)
-
- story = demo_command._collect_playable_story()
-
- assert [label for label, _ in prompts] == [
- "Give EverOS one thing to remember",
- "Ask EverOS to recall it",
- ]
- assert story.memory == "I keep my Monday design review notes in Notion."
- assert story.query == "Where are my Monday review notes?"
-
-
-def test_interactive_demo_checks_textual_before_prompt(monkeypatch) -> None:
- def fail_load_tui() -> object:
- raise typer.Exit(code=1)
-
- def fail_prompt(*_: object, **__: object) -> str:
- pytest.fail("demo prompted before checking TUI availability")
-
- monkeypatch.setattr(demo_command, "_load_run_demo_tui", fail_load_tui)
- monkeypatch.setattr(demo_command.typer, "prompt", fail_prompt)
-
- with pytest.raises(typer.Exit):
- demo_command._run_interactive_demo(cinematic=False)
+ for flag in ("--cinematic", "--live", "--cloud", "--server-url"):
+ assert flag in help_text
def test_plain_demo_uses_poster_gold_brand_primary(monkeypatch) -> None:
@@ -102,9 +53,13 @@ def print(self, *renderables: object, **_: object) -> None:
monkeypatch.setattr(demo_command, "Console", FakeConsole)
demo_command._print_plain_demo(
- build_demo_story(
- "I keep my Monday design review notes in Notion.",
- "Where are my Monday review notes?",
+ DemoStory(
+ owner="you",
+ memory="I keep my Monday design review notes in Notion.",
+ query="Where are my Monday review notes?",
+ answer="In Notion.",
+ source_filename="episode-demo.md",
+ fact_filename="atomic_fact-demo.md",
)
)
@@ -114,74 +69,47 @@ def print(self, *renderables: object, **_: object) -> None:
assert "episode-demo.md" in printed_text
-def test_live_demo_flow_calls_server_and_builds_story() -> None:
- story = build_demo_story(
- "I love climbing in Yosemite every spring.",
- "Where do I like to climb?",
+def test_launch_interactive_defaults_to_cloud_with_unique_identity(monkeypatch) -> None:
+ captured: dict[str, object] = {}
+
+ monkeypatch.setattr(
+ demo_command,
+ "_load_run_demo_tui",
+ lambda: lambda **kwargs: captured.update(kwargs),
+ )
+ monkeypatch.delenv(cloud.CLOUD_DEMO_SERVER_URL_ENV, raising=False)
+ monkeypatch.setenv(cloud.CLOUD_DEMO_KEY_ENV, "demo-key")
+
+ demo_command._launch_interactive_demo(
+ live=False, server_url=cloud.LIVE_DEMO_SERVER_URL
)
- calls: list[tuple[str, str, dict[str, object] | None]] = []
-
- def fake_request(
- method: str,
- path: str,
- *,
- base_url: str,
- json_body: dict[str, object] | None = None,
- timeout_seconds: float,
- ) -> dict[str, object]:
- calls.append((method, path, json_body))
- assert base_url == "http://server.test"
- assert timeout_seconds == 3.0
- if path == "/health":
- return {"status": "ok"}
- if path == "/api/v1/memory/add":
- return {"message_count": 1, "status": "accumulated"}
- if path == "/api/v1/memory/flush":
- return {"message_count": 1, "status": "extracted"}
- if path == "/api/v1/memory/search":
- return {
- "data": {
- "episodes": [
- {
- "id": "alice_ep_20260623_0001",
- "episode": "Alice loves climbing in Yosemite every spring.",
- "summary": "Alice climbs in Yosemite every spring.",
- "subject": "Yosemite climbing",
- "score": 0.82,
- "atomic_facts": [
- {
- "id": "alice_af_20260623_0001",
- "content": (
- "Alice loves climbing in Yosemite every spring."
- ),
- "score": 0.91,
- }
- ],
- }
- ]
- }
- }
- raise AssertionError(f"unexpected request: {method} {path}")
-
- live_story = demo_command._run_live_demo_flow(
- story,
- base_url="http://server.test",
- request_json=fake_request,
- timeout_seconds=3.0,
+
+ assert captured["interactive"] is True
+ assert captured["base_url"] == cloud.CLOUD_API_BASE_URL
+ assert str(captured["session_id"]).startswith("everos-demo-")
+ assert str(captured["user_id"]).startswith("everos_demo_")
+ assert captured["api_key"] == "demo-key" # default mode uses the demo key
+
+
+def test_launch_interactive_live_uses_own_cloud_key(monkeypatch) -> None:
+ captured: dict[str, object] = {}
+
+ monkeypatch.setattr(
+ demo_command,
+ "_load_run_demo_tui",
+ lambda: lambda **kwargs: captured.update(kwargs),
+ )
+ monkeypatch.delenv(cloud.CLOUD_DEMO_SERVER_URL_ENV, raising=False)
+ monkeypatch.setenv(cloud.CLOUD_USER_KEY_ENV, "user-key")
+
+ demo_command._launch_interactive_demo(
+ live=True, server_url=cloud.LIVE_DEMO_SERVER_URL
)
- assert [path for _, path, _ in calls] == [
- "/health",
- "/api/v1/memory/add",
- "/api/v1/memory/flush",
- "/api/v1/memory/search",
- ]
- add_body = calls[1][2]
- assert add_body is not None
- assert add_body["session_id"] == "everos-demo-live"
- assert live_story.answer == "Alice loves climbing in Yosemite every spring."
- assert live_story.source_filename == "episode:alice_ep_20260623_0001"
- assert live_story.fact_filename == "fact:alice_af_20260623_0001"
+ # --live still hits the cloud platform, just with the user's own key.
+ assert captured["base_url"] == cloud.CLOUD_API_BASE_URL
+ assert captured["api_key"] == "user-key"
+ assert str(captured["session_id"]).startswith("everos-demo-")
def _strip_ansi(value: str) -> str:
diff --git a/tests/unit/test_entrypoints/test_tui/test_demo_app.py b/tests/unit/test_entrypoints/test_tui/test_demo_app.py
index 6d94fbbc..0010ef40 100644
--- a/tests/unit/test_entrypoints/test_tui/test_demo_app.py
+++ b/tests/unit/test_entrypoints/test_tui/test_demo_app.py
@@ -8,19 +8,35 @@
SPHERE_FRAME_HEIGHT,
SPHERE_FRAME_WIDTH,
TERMINAL_CELL_HEIGHT_RATIO,
+ TRACE_STAGES,
DotSphereWidget,
EverOSDemoApp,
+ QueryAnswerBar,
+ _capabilities_text,
+ _conversation_text,
_field_header_text,
_hero_text,
- _payoff_text,
_recall_proof_text,
_signal_rail_text,
_source_tree_text,
- _sphere_caption,
+ _state_to_stage,
)
-from everos.entrypoints.tui.demo.data import build_demo_story
+from everos.entrypoints.tui.demo.data import DemoStory
from everos.entrypoints.tui.demo.widgets.sphere import SPHERE_STATES
+_YELLOW = "#F9B91C"
+
+
+def _story(memory: str, query: str, answer: str) -> DemoStory:
+ return DemoStory(
+ owner="you",
+ memory=memory,
+ query=query,
+ answer=answer,
+ source_filename="episode-demo.md",
+ fact_filename="atomic_fact-demo.md",
+ )
+
def test_demo_tui_uses_poster_derived_brand_palette() -> None:
css = EverOSDemoApp.CSS
@@ -41,8 +57,9 @@ def test_demo_tui_uses_elevated_instrument_layout() -> None:
assert "#command-strip" in css
assert "#memory-field" in css
assert "#signal-rail" in css
+ assert "#capabilities" in css
assert "#provenance-strip" in css
- assert "#payoff" in css
+ assert "#conversation" in css
assert "FooterKey" in css
assert "background: #F9B91C" in css
assert any("on #F9B91C" in span.style for span in _hero_text().spans)
@@ -55,7 +72,7 @@ def test_demo_tui_uses_balanced_panel_proportions() -> None:
command_strip = _css_block(css, "#command-strip")
signal_rail = _css_block(css, "#signal-rail")
- payoff = _css_block(css, "#payoff")
+ conversation = _css_block(css, "#conversation")
assert "height: 2;" in command_strip
assert "border-left: thick" not in command_strip
@@ -65,15 +82,11 @@ def test_demo_tui_uses_balanced_panel_proportions() -> None:
assert "height: 1fr;" in DotSphereWidget.DEFAULT_CSS
- assert "height: 100%;" in signal_rail
+ assert "height: 1fr;" in signal_rail
assert "source route" in _signal_rail_text().plain
- assert "recall proof" in _signal_rail_text().plain
- assert "height: 2;" in payoff
- assert "background: #24231E;" in payoff
- assert "padding: 0 1;" in payoff
- assert _payoff_text().plain.startswith("memory formed:")
- assert "bold #F9B91C" in {span.style for span in _payoff_text().spans}
+ # The conversation log sits below the yellow line.
+ assert "border-top: hkey #F9B91C;" in conversation
def test_demo_tui_sphere_renders_round_in_terminal_cells() -> None:
@@ -91,21 +104,222 @@ def test_demo_tui_celebrates_after_source_reveal() -> None:
assert set(DotSphereWidget.STATES).issubset(SPHERE_STATES)
-def test_demo_tui_renders_playable_story_copy() -> None:
- story = build_demo_story(
- "I keep my Monday design review notes in Notion.",
- "Where are my Monday review notes?",
+def test_signal_rail_lights_reflect_state() -> None:
+ idle = _signal_rail_text().plain
+ assert "memory core" in idle
+ assert "not ready" in idle # core idle => not ready
+ assert "source route" in idle
+
+ active = _signal_rail_text(
+ {
+ "core": "ready",
+ "conversation": "captured",
+ "facts": "live",
+ "index": "synced",
+ "recall": "hit",
+ }
+ ).plain
+ for label in ("ready", "captured", "live", "synced", "hit"):
+ assert label in active
+
+
+def test_signal_rail_light_colors_follow_white_yellow_black() -> None:
+ rail = _signal_rail_text(
+ {
+ "core": "error",
+ "conversation": "idle",
+ "facts": "live",
+ "index": "idle",
+ "recall": "idle",
+ }
+ )
+ dot_styles = [
+ span.style for span in rail.spans if "●" in rail.plain[span.start : span.end]
+ ]
+ assert f"bold {_YELLOW}" in dot_styles # an active light is yellow
+ assert "bold #1D1C18" in dot_styles # the errored light is black
+
+
+def test_capabilities_box_uses_real_website_numbers() -> None:
+ text = _capabilities_text().plain
+ # Real highlights from evermind.ai: token efficiency + one SOTA benchmark.
+ assert "1/10 of full context" in text # real token-efficiency claim
+ assert "93.05%" in text # one headline benchmark (LoCoMo)
+ assert "83.00%" not in text # only one score now
+ assert "rerank" in text
+ # local-first is dropped here (already shown in the field header scope).
+ assert "local-first" not in text
+
+
+def test_source_lock_uses_date_stamped_filenames() -> None:
+ text = _source_tree_text().plain
+ assert "episode-" in text and ".md" in text
+ assert "atomic_fact-" in text
+
+
+def test_recall_lock_shows_real_score_and_demo_scope() -> None:
+ story = DemoStory(
+ owner="everos_demo_abc",
+ memory="m",
+ query="q",
+ answer="a",
+ source_filename="",
+ fact_filename="",
+ score=0.873,
+ )
+ text = _recall_proof_text(story, user_label="YangtzeSeventh", saved_pct=62).plain
+ assert "0.873" in text
+ assert "user=YangtzeSeventh" in text # local user, not the session id or alice
+ assert "project=demo" in text
+ assert "~62% tokens (est)" in text
+ assert "similarity" not in text
+ # No saved figure until a round has run.
+ assert "saved —" in _recall_proof_text(story, user_label="x").plain
+
+
+def test_conversation_log_accumulates_turns() -> None:
+ empty = _conversation_text([]).plain
+ assert "will appear here" in empty
+
+ filled = _conversation_text([("where do I climb?", "Yosemite")]).plain
+ assert "you" in filled
+ assert "where do I climb?" in filled
+ assert "everos" in filled
+ assert "Yosemite" in filled
+
+
+def test_field_header_shows_local_user_and_trace_stages() -> None:
+ header = _field_header_text(user_label="YangtzeSeventh", active_stage=1)
+
+ assert "user=YangtzeSeventh" in header.plain
+ assert "scope=local-first" in header.plain
+ for stage in TRACE_STAGES:
+ assert stage in header.plain
+
+
+def test_field_header_highlights_only_the_active_stage() -> None:
+ header = _field_header_text(user_label="you", active_stage=2)
+
+ highlighted = {
+ header.plain[span.start : span.end]
+ for span in header.spans
+ if span.style == f"bold {_YELLOW}"
+ }
+ assert highlighted & set(TRACE_STAGES) == {"index"}
+
+
+def test_state_to_stage_maps_sphere_states_to_trace_words() -> None:
+ assert _state_to_stage("ingesting") == 0
+ assert _state_to_stage("extracting") == 1
+ assert _state_to_stage("indexing") == 2
+ assert _state_to_stage("recalling") == 3
+ assert _state_to_stage("booting") == -1
+
+
+def test_query_answer_bar_keeps_both_labels() -> None:
+ rendered = QueryAnswerBar().render().plain
+
+ assert "Query" in rendered
+ assert "Answer" in rendered
+
+
+def test_ctrl_c_is_a_priority_quit_binding() -> None:
+ quit_keys = {
+ binding.key
+ for binding in EverOSDemoApp.BINDINGS
+ if getattr(binding, "action", None) == "quit"
+ and getattr(binding, "priority", False)
+ }
+ assert "ctrl+c" in quit_keys
+ assert "ctrl+q" in quit_keys
+
+
+def test_help_and_unknown_command_text() -> None:
+ from everos.entrypoints.tui.demo.app import _help_text, _unknown_command_text
+
+ help_plain = _help_text().plain
+ for command in ("/help", "/live", "/replay", "/clear", "/quit"):
+ assert command in help_plain
+ assert "unknown command /bogus" in _unknown_command_text("/bogus").plain
+
+
+def test_live_guidance_points_to_own_key_flow() -> None:
+ from everos.entrypoints.tui.demo.app import _live_guidance_text
+
+ text = _live_guidance_text().plain
+ assert "everos init" in text
+ assert "everos demo --live" in text
+
+
+async def test_slash_live_does_not_consume_a_turn() -> None:
+ from textual.widgets import Input
+
+ app = EverOSDemoApp(
+ interactive=True, base_url="http://server.test", session_id="s", user_id="u"
)
+ async with app.run_test() as pilot:
+ console_input = app.query_one("#console-input", Input)
+ console_input.value = "/live"
+ await pilot.press("enter")
+ await pilot.pause()
- assert "user=you" in _field_header_text(story).plain
- assert "Where are my Monday review notes?" in _sphere_caption(story).plain
- assert story.answer in _sphere_caption(story).plain
- assert "server wake" not in _signal_rail_text(story).plain
- assert "memory core" in _signal_rail_text(story).plain
- assert story.source_filename in _source_tree_text(story).plain
- assert story.fact_filename in _source_tree_text(story).plain
- assert story.answer in _recall_proof_text(story).plain
- assert story.answer in _payoff_text(story).plain
+ assert app._conversation_phase == "memory"
+ assert app._pending_memory == ""
+
+
+async def test_slash_help_does_not_consume_a_turn() -> None:
+ from textual.widgets import Input
+
+ app = EverOSDemoApp(
+ interactive=True, base_url="http://server.test", session_id="s", user_id="u"
+ )
+ async with app.run_test() as pilot:
+ console_input = app.query_one("#console-input", Input)
+ console_input.value = "/help"
+ await pilot.press("enter")
+ await pilot.pause()
+
+ # /help is a command, not a memory: the conversation does not advance.
+ assert app._conversation_phase == "memory"
+ assert app._pending_memory == ""
+
+
+async def test_slash_clear_wipes_the_conversation_log() -> None:
+ from textual.widgets import Input
+
+ app = EverOSDemoApp(
+ interactive=True, base_url="http://server.test", session_id="s", user_id="u"
+ )
+ async with app.run_test() as pilot:
+ app._record_turn("where do I climb?", "Yosemite")
+ assert app._log
+
+ console_input = app.query_one("#console-input", Input)
+ console_input.value = "/clear"
+ await pilot.press("enter")
+ await pilot.pause()
+
+ assert app._log == []
+
+
+async def test_typing_quit_exits_the_app(monkeypatch) -> None:
+ from textual.widgets import Input
+
+ app = EverOSDemoApp(
+ interactive=True,
+ base_url="http://server.test",
+ session_id="s",
+ user_id="u",
+ )
+ async with app.run_test() as pilot:
+ exited: list[bool] = []
+ monkeypatch.setattr(app, "exit", lambda *a, **k: exited.append(True))
+ console_input = app.query_one("#console-input", Input)
+ console_input.value = "quit"
+ await pilot.press("enter")
+ await pilot.pause()
+
+ assert exited == [True]
def test_demo_tui_signal_rail_keeps_source_status_columns_separate() -> None:
@@ -116,6 +330,106 @@ def test_demo_tui_signal_rail_keeps_source_status_columns_separate() -> None:
assert "..." in rail
+async def test_demo_tui_interactive_runs_cloud_round_per_input(monkeypatch) -> None:
+ from textual.widgets import Input
+
+ from everos.entrypoints.tui.demo import cloud
+
+ monkeypatch.setattr(cloud, "add_memory", lambda *_, **__: "task-1")
+ monkeypatch.setattr(cloud, "wait_task", lambda *_, **__: None)
+ monkeypatch.setattr(cloud, "flush_memory", lambda *_, **__: None)
+
+ def fake_search(
+ memory: str, query: str, *, base_url: str, user_id: str, **_: object
+ ) -> DemoStory:
+ assert base_url == "http://server.test"
+ return _story(memory, query, f"recalled<{memory}>")
+
+ monkeypatch.setattr(cloud, "search_recall", fake_search)
+
+ app = EverOSDemoApp(
+ interactive=True,
+ max_rounds=2,
+ base_url="http://server.test",
+ session_id="everos-demo-x",
+ user_id="everos_demo_x",
+ )
+ async with app.run_test() as pilot:
+ console_input = app.query_one("#console-input", Input)
+
+ # Round 1: a memory, then a recall query -> a real (faked) cloud round.
+ console_input.value = "我喜欢吃杨梅"
+ await pilot.press("enter")
+ console_input.value = "我喜欢吃什么"
+ await pilot.press("enter")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+
+ # BUG 305: the panels follow the user's own input, never Yosemite.
+ assert app._story.memory == "我喜欢吃杨梅"
+ assert app._story.query == "我喜欢吃什么"
+ assert app._story.answer == "recalled<我喜欢吃杨梅>"
+ assert "Yosemite" not in app._story.answer
+ # Lights walked the full pipeline to a hit.
+ assert app._lights["core"] == "ready"
+ assert app._lights["recall"] == "hit"
+ # A per-round token-saving estimate was computed.
+ assert app._saved_pct is not None
+
+ # Round 2 reaches the cap and locks the input behind the upgrade nudge.
+ console_input.value = "I bike to work"
+ await pilot.press("enter")
+ console_input.value = "How do I commute?"
+ await pilot.press("enter")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+
+ assert app._conversation_phase == "done"
+ assert console_input.disabled is True
+
+
+async def test_demo_tui_interactive_shows_quota_guidance(monkeypatch) -> None:
+ from textual.widgets import Input
+
+ from everos.entrypoints.tui.demo import cloud
+ from everos.entrypoints.tui.demo.app import _quota_guidance_text
+
+ def quota(*_: object, **__: object) -> None:
+ raise cloud.CloudQuotaError("http://server.test")
+
+ monkeypatch.setattr(cloud, "add_memory", quota)
+
+ app = EverOSDemoApp(
+ interactive=True,
+ base_url="http://server.test",
+ session_id="s",
+ user_id="u",
+ )
+ async with app.run_test() as pilot:
+ console_input = app.query_one("#console-input", Input)
+ console_input.value = "a memory"
+ await pilot.press("enter")
+ console_input.value = "a question"
+ await pilot.press("enter")
+ await app.workers.wait_for_complete()
+ await pilot.pause()
+
+ assert app._conversation_phase == "done"
+ assert console_input.disabled is True
+
+ assert "everos init" in _quota_guidance_text().plain
+
+
+async def test_demo_tui_non_interactive_has_no_input_box() -> None:
+ from textual.css.query import NoMatches
+ from textual.widgets import Input
+
+ app = EverOSDemoApp()
+ async with app.run_test():
+ with pytest.raises(NoMatches):
+ app.query_one("#console-input", Input)
+
+
def _css_block(css: str, selector: str) -> str:
start = css.index(f"{selector} {{")
end = css.index("}", start)
diff --git a/tests/unit/test_entrypoints/test_tui/test_demo_cloud.py b/tests/unit/test_entrypoints/test_tui/test_demo_cloud.py
new file mode 100644
index 00000000..15781e44
--- /dev/null
+++ b/tests/unit/test_entrypoints/test_tui/test_demo_cloud.py
@@ -0,0 +1,233 @@
+"""EverOS Cloud demo client contracts."""
+
+from __future__ import annotations
+
+import urllib.error
+
+import pytest
+
+from everos.entrypoints.tui.demo import cloud
+
+
+def test_resolve_cloud_base_url_prefers_explicit_then_env(monkeypatch) -> None:
+ monkeypatch.delenv(cloud.CLOUD_DEMO_SERVER_URL_ENV, raising=False)
+ assert (
+ cloud.resolve_cloud_base_url(cloud.LIVE_DEMO_SERVER_URL)
+ == cloud.CLOUD_API_BASE_URL
+ )
+
+ monkeypatch.setenv(cloud.CLOUD_DEMO_SERVER_URL_ENV, "https://env.test")
+ assert (
+ cloud.resolve_cloud_base_url(cloud.LIVE_DEMO_SERVER_URL) == "https://env.test"
+ )
+
+ assert (
+ cloud.resolve_cloud_base_url("https://explicit.test") == "https://explicit.test"
+ )
+
+
+def test_resolve_keys_read_their_env_vars(monkeypatch) -> None:
+ monkeypatch.setenv(cloud.CLOUD_DEMO_KEY_ENV, "demo-key")
+ monkeypatch.setenv(cloud.CLOUD_USER_KEY_ENV, "user-key")
+ assert cloud.resolve_demo_key() == "demo-key"
+ assert cloud.resolve_user_key() == "user-key"
+
+
+def test_new_demo_identity_is_unique_and_paired() -> None:
+ session_a, user_a = cloud.new_demo_identity()
+ session_b, user_b = cloud.new_demo_identity()
+
+ assert session_a != session_b
+ assert user_a != user_b
+ assert session_a.startswith("everos-demo-")
+ assert user_a.startswith("everos_demo_")
+
+
+def test_add_memory_posts_messages_and_returns_task_id() -> None:
+ calls: list[tuple[str, str, dict[str, object] | None, str | None]] = []
+
+ def fake_request(
+ method: str,
+ path: str,
+ *,
+ base_url: str,
+ api_key: str | None = None,
+ json_body: dict[str, object] | None = None,
+ timeout_seconds: float,
+ ) -> dict[str, object]:
+ calls.append((method, path, json_body, api_key))
+ return {"data": {"task_id": "task-123", "status": "queued"}}
+
+ task_id = cloud.add_memory(
+ "我喜欢吃杨梅",
+ base_url="https://api.test",
+ session_id="everos-demo-abc",
+ user_id="everos_demo_abc",
+ api_key="k-1",
+ request_json=fake_request,
+ )
+
+ assert task_id == "task-123"
+ method, path, body, api_key = calls[0]
+ assert (method, path) == ("POST", "/api/v1/memories")
+ assert api_key == "k-1"
+ assert body["user_id"] == "everos_demo_abc"
+ assert body["messages"][0]["role"] == "user"
+ assert body["messages"][0]["content"] == "我喜欢吃杨梅"
+
+
+def test_flush_memory_forces_extraction() -> None:
+ bodies: list[dict[str, object] | None] = []
+
+ def fake_request(
+ method: str,
+ path: str,
+ *,
+ base_url: str,
+ api_key: str | None = None,
+ json_body: dict[str, object] | None = None,
+ timeout_seconds: float,
+ ) -> dict[str, object]:
+ bodies.append(json_body)
+ assert path == "/api/v1/memories/flush"
+ return {"data": {"status": "extracted"}}
+
+ cloud.flush_memory(
+ base_url="https://api.test",
+ session_id="s",
+ user_id="u",
+ api_key="k",
+ request_json=fake_request,
+ )
+
+ assert bodies[0]["force"] is True
+ assert bodies[0]["user_id"] == "u"
+
+
+def test_wait_task_succeeds_and_fails() -> None:
+ def ok(*_: object, **__: object) -> dict[str, object]:
+ return {"task_id": "t", "status": "success"}
+
+ cloud.wait_task("t", base_url="https://api.test", api_key="k", request_json=ok)
+
+ def failed(*_: object, **__: object) -> dict[str, object]:
+ return {"task_id": "t", "status": "failed"}
+
+ with pytest.raises(cloud.CloudDemoError):
+ cloud.wait_task(
+ "t",
+ base_url="https://api.test",
+ api_key="k",
+ request_json=failed,
+ attempts=1,
+ )
+
+
+def test_search_recall_parses_atomic_fact_and_score() -> None:
+ def fake_request(
+ method: str,
+ path: str,
+ *,
+ base_url: str,
+ api_key: str | None = None,
+ json_body: dict[str, object] | None = None,
+ timeout_seconds: float,
+ ) -> dict[str, object]:
+ assert path == "/api/v1/memories/search"
+ assert json_body["filters"] == {"user_id": "everos_demo_abc"}
+ return {
+ "data": {
+ "episodes": [
+ {
+ "id": "ep1",
+ "summary": "long summary",
+ "episode": "long episode text",
+ "score": None,
+ "atomic_facts": [
+ {
+ "id": "af1",
+ "atomic_fact": "You like Yangmei.",
+ "score": 0.57,
+ }
+ ],
+ }
+ ]
+ }
+ }
+
+ story = cloud.search_recall(
+ "我喜欢吃杨梅",
+ "我喜欢吃什么",
+ base_url="https://api.test",
+ user_id="everos_demo_abc",
+ api_key="k",
+ request_json=fake_request,
+ )
+
+ assert story is not None
+ assert story.owner == "everos_demo_abc"
+ assert story.answer == "You like Yangmei." # from atomic_fact, not summary
+ assert story.score == 0.57 # fact-level score (episode score is null)
+ assert story.source_filename == "episode:ep1"
+
+
+def test_search_recall_returns_none_on_miss() -> None:
+ def fake_request(*_: object, **__: object) -> dict[str, object]:
+ return {"data": {"episodes": []}}
+
+ story = cloud.search_recall(
+ "m",
+ "q",
+ base_url="https://api.test",
+ user_id="u",
+ api_key="k",
+ request_json=fake_request,
+ search_attempts=2,
+ search_interval_seconds=0.0,
+ )
+
+ assert story is None
+
+
+def test_request_json_sets_bearer_header(monkeypatch) -> None:
+ captured: dict[str, str | None] = {}
+
+ class FakeResp:
+ def __enter__(self) -> FakeResp:
+ return self
+
+ def __exit__(self, *_: object) -> bool:
+ return False
+
+ def read(self) -> bytes:
+ return b'{"ok": true}'
+
+ def fake_urlopen(req: object, timeout: float) -> FakeResp:
+ captured["auth"] = req.headers.get("Authorization") # type: ignore[attr-defined]
+ return FakeResp()
+
+ monkeypatch.setattr(cloud.urllib.request, "urlopen", fake_urlopen)
+ cloud._request_json(
+ "GET", "/x", base_url="https://api.test", api_key="abc", timeout_seconds=1.0
+ )
+ assert captured["auth"] == "Bearer abc"
+
+
+def test_request_json_maps_401_and_429(monkeypatch) -> None:
+ def boom(code: int):
+ def _raise(*_: object, **__: object) -> object:
+ raise urllib.error.HTTPError("https://api.test", code, "x", {}, None)
+
+ return _raise
+
+ monkeypatch.setattr(cloud.urllib.request, "urlopen", boom(401))
+ with pytest.raises(cloud.CloudAuthError):
+ cloud._request_json(
+ "GET", "/x", base_url="https://api.test", timeout_seconds=1.0
+ )
+
+ monkeypatch.setattr(cloud.urllib.request, "urlopen", boom(429))
+ with pytest.raises(cloud.CloudQuotaError):
+ cloud._request_json(
+ "GET", "/x", base_url="https://api.test", timeout_seconds=1.0
+ )
diff --git a/tests/unit/test_entrypoints/test_tui/test_demo_data.py b/tests/unit/test_entrypoints/test_tui/test_demo_data.py
index 04311f43..4420fbaa 100644
--- a/tests/unit/test_entrypoints/test_tui/test_demo_data.py
+++ b/tests/unit/test_entrypoints/test_tui/test_demo_data.py
@@ -1,29 +1,23 @@
-"""EverOS playable demo story contracts."""
+"""EverOS demo story data contracts."""
from __future__ import annotations
from everos.entrypoints.tui.demo.data import (
DEFAULT_MEMORY_SEED,
DEFAULT_QUERY,
- build_demo_story,
+ DemoStory,
+ default_demo_story,
)
-def test_demo_story_preserves_prompted_memory_and_query() -> None:
- story = build_demo_story(
- "I keep my Monday design review notes in Notion.",
- "Where are my Monday review notes?",
- )
-
- assert story.owner == "you"
- assert story.memory == "I keep my Monday design review notes in Notion."
- assert story.query == "Where are my Monday review notes?"
- assert story.answer == "I keep my Monday design review notes in Notion."
- assert story.source_filename == "episode-demo.md"
- assert story.fact_filename == "atomic_fact-demo.md"
+def test_default_demo_story_is_the_static_showcase() -> None:
+ story = default_demo_story()
+ assert isinstance(story, DemoStory)
+ assert story.memory == DEFAULT_MEMORY_SEED
+ assert story.answer == "Yosemite every spring"
+ assert story.source_filename == "episode-2026-06-20.md"
-def test_demo_story_keeps_default_yosemite_success_moment() -> None:
- story = build_demo_story(DEFAULT_MEMORY_SEED, DEFAULT_QUERY)
- assert story.answer == "Yosemite every spring"
+def test_default_query_constant_is_available() -> None:
+ assert DEFAULT_QUERY
diff --git a/tests/unit/test_entrypoints/test_tui/test_demo_sphere.py b/tests/unit/test_entrypoints/test_tui/test_demo_sphere.py
index 61fa1af7..51058358 100644
--- a/tests/unit/test_entrypoints/test_tui/test_demo_sphere.py
+++ b/tests/unit/test_entrypoints/test_tui/test_demo_sphere.py
@@ -89,7 +89,7 @@ def test_dot_sphere_remembered_state_has_highlighted_node() -> None:
assert len(highlighted) == 1
assert _is_braille_cell(highlighted[0].glyph)
assert highlighted[0].style == "#F9B91C"
- assert frame.caption == "remembered Yosemite preference"
+ assert frame.caption == "found the matching memory"
def test_dot_sphere_celebrating_state_bursts_into_confetti() -> None: