Spaces:
Paused
Paused
Download server/chats.py from Bit-Trading-Company/ASE-GLM: direct link, hf CLI and curl.
- Browser
- Download file 5.31 kB
-
https://huggingface.co/spaces/Bit-Trading-Company/ASE-GLM/resolve/main/server/chats.py
- Command line
-
hf download hf://spaces/Bit-Trading-Company/ASE-GLM/server/chats.py
-
curl -L -o chats.py https://huggingface.co/spaces/Bit-Trading-Company/ASE-GLM/resolve/main/server/chats.py
5.31 kB
| """The shared chat store, and the ceiling it stops at. | |
| Ephemeral is the default and needs nothing from here: those chats live in the | |
| viewer's browser. This is the other option -- a Bit Trading Company database a | |
| signed-in viewer's chats can follow them across devices. | |
| Two rules shape the whole module: | |
| * **A chat is scoped to whoever wrote it.** No owner, no store: an unauthenticated | |
| viewer would otherwise be writing into a bucket everyone else can read. | |
| * **Full is a normal state, not an error.** Past the ceiling the store simply | |
| stops accepting writes and the client keeps everything on the device. There is | |
| a real budget behind this, and silently growing past it is how a demo becomes | |
| an invoice. | |
| Enabled by ``ASE_CHAT_DB`` (a directory). Unset -- which is the case on the | |
| Space today -- and every endpoint reports the store as unavailable. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import re | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| # A few gigabytes. Raise it if the data turns out to be worth the space; the | |
| # client needs no change, it reads the ceiling out of /api/config. | |
| DEFAULT_MAX_BYTES = 3 * 1024**3 | |
| # One conversation with a hundred turns and a dozen canvases is well under this. | |
| MAX_CHAT_BYTES = 512 * 1024 | |
| _SAFE_ID = re.compile(r"^[A-Za-z0-9_-]{1,64}$") | |
| class StoreState: | |
| remote: bool | |
| max_bytes: int | |
| used_bytes: int | |
| full: bool | |
| detail: str = "" | |
| def to_json(self) -> dict: | |
| return { | |
| "remote": self.remote, | |
| "maxBytes": self.max_bytes, | |
| "usedBytes": self.used_bytes, | |
| "full": self.full, | |
| "detail": self.detail, | |
| } | |
| def root() -> Path | None: | |
| """Where chats are kept, or None when no store is configured.""" | |
| configured = os.environ.get("ASE_CHAT_DB") | |
| return Path(configured) if configured else None | |
| def max_bytes() -> int: | |
| try: | |
| return max(0, int(os.environ.get("ASE_CHAT_DB_MAX_BYTES", DEFAULT_MAX_BYTES))) | |
| except ValueError: | |
| return DEFAULT_MAX_BYTES | |
| def used_bytes(base: Path) -> int: | |
| total = 0 | |
| for path in base.rglob("*.json"): | |
| try: | |
| total += path.stat().st_size | |
| except OSError: | |
| continue | |
| return total | |
| def state(owner: str | None) -> StoreState: | |
| """What the client should be told about the store, for this viewer.""" | |
| base = root() | |
| if base is None: | |
| return StoreState(False, 0, 0, False, "No shared store is configured on this server.") | |
| if not owner: | |
| return StoreState(False, max_bytes(), 0, False, | |
| "Sign in with Hugging Face to use the shared store.") | |
| try: | |
| base.mkdir(parents=True, exist_ok=True) | |
| used = used_bytes(base) | |
| except OSError as e: | |
| return StoreState(False, max_bytes(), 0, False, f"The store is unreachable: {e}") | |
| cap = max_bytes() | |
| full = used >= cap | |
| return StoreState(True, cap, used, full, | |
| "Full — new chats stay on the device." if full else "") | |
| def _dir(base: Path, owner: str) -> Path: | |
| # The owner is a Hugging Face username, but it arrives from a token rather | |
| # than from our own validation, so it is hashed into a safe segment instead | |
| # of being trusted as a path component. | |
| safe = re.sub(r"[^A-Za-z0-9_-]", "_", owner)[:48] | |
| return base / safe | |
| def load_all(owner: str) -> list[dict]: | |
| base = root() | |
| if base is None or not owner: | |
| return [] | |
| folder = _dir(base, owner) | |
| if not folder.is_dir(): | |
| return [] | |
| out = [] | |
| for path in sorted(folder.glob("*.json")): | |
| try: | |
| out.append(json.loads(path.read_text("utf-8"))) | |
| except (OSError, json.JSONDecodeError): | |
| # One unreadable chat must not take the whole history down with it. | |
| continue | |
| return out | |
| def save(owner: str, chat_id: str, chat: dict) -> StoreState: | |
| """Write one chat. A refusal is reported as state, never as an exception.""" | |
| st = state(owner) | |
| if not st.remote or st.full or not _SAFE_ID.match(chat_id): | |
| return st | |
| if chat.get("ephemeral"): | |
| # An ephemeral chat is never written, whatever the request says. The | |
| # client enforces this too; doing it in both places is deliberate. | |
| return st | |
| body = json.dumps(chat, ensure_ascii=False) | |
| if len(body.encode("utf-8")) > MAX_CHAT_BYTES: | |
| st.detail = "That chat is too large for the shared store." | |
| return st | |
| base = root() | |
| assert base is not None | |
| folder = _dir(base, owner) | |
| try: | |
| folder.mkdir(parents=True, exist_ok=True) | |
| # Written beside the target and moved into place, so a crash mid-write | |
| # leaves the previous version rather than half a file. | |
| tmp = folder / f".{chat_id}.tmp" | |
| tmp.write_text(body, "utf-8") | |
| tmp.replace(folder / f"{chat_id}.json") | |
| except OSError as e: | |
| st.detail = f"Could not write: {e}" | |
| return st | |
| return state(owner) | |
| def delete(owner: str, chat_id: str) -> StoreState: | |
| base = root() | |
| if base is not None and owner and _SAFE_ID.match(chat_id): | |
| try: | |
| (_dir(base, owner) / f"{chat_id}.json").unlink(missing_ok=True) | |
| except OSError: | |
| pass | |
| return state(owner) | |