Rewrite Python comments in Google developer documentation style (#12)
* Rewrite comments in Google developer documentation style Rewrite the comments and docstrings across the backend core modules so they read plainly. The previous prose was accurate but dense and figurative, which made it slow to skim. Applies the Google developer documentation style guide: short sentences, active voice, present tense, American spelling, and no metaphors, idioms, or rhetorical asides. Replaces em-dash chains with separate sentences.
This commit is contained in:
+591
-524
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
"""Visit analytics: one endpoint the browser writes to, one the owner reads.
|
||||
|
||||
The split matters. `/collect` is public and takes exactly one fact — which
|
||||
page was viewed — because anything a stranger can POST is a number a stranger
|
||||
can invent. Everything the dashboard actually relies on (turns, adventures,
|
||||
sign-ups, demo spend, errors) is recorded server-side by the code performing
|
||||
it, so those counts are as trustworthy as the app itself.
|
||||
The split matters. `/collect` is public and accepts one fact, which page was
|
||||
viewed, because anything a stranger can POST is a number a stranger can invent.
|
||||
Everything the dashboard relies on, meaning turns, adventures, sign-ups, demo
|
||||
spend, and errors, is recorded on the server by the code that performs it, so
|
||||
those counts are as trustworthy as the app itself.
|
||||
|
||||
The two reading endpoints are owner-only and 404 for everyone else, the same
|
||||
way the AI Chat router does: a feature nobody else can use is better off not
|
||||
@@ -27,7 +27,8 @@ def owner(
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = Depends(auth.get_current_user),
|
||||
) -> models.User:
|
||||
"""Gate for the reading half. 404, not 403 — see the module docstring."""
|
||||
"""Gates the reading half. It returns 404 rather than 403. See the module
|
||||
docstring."""
|
||||
if not auth.is_owner(user):
|
||||
raise HTTPException(404, "Not found")
|
||||
return user
|
||||
@@ -39,10 +40,10 @@ Owner = Depends(owner)
|
||||
class Pageview(BaseModel):
|
||||
"""What the SPA reports on a page load or a route change.
|
||||
|
||||
`first` marks a real page load rather than a client-side navigation: the
|
||||
things that describe a *visit* rather than a *view* — where it came from,
|
||||
on what kind of device, from which country — are recorded only then, so a
|
||||
visitor who clicks around five pages is still one referral.
|
||||
`first` marks a real page load rather than a client-side navigation. The
|
||||
facts that describe a visit rather than a view, which are where it came from,
|
||||
on what kind of device, and from which country, are recorded only on a page
|
||||
load, so a visitor who clicks through five pages is still one referral.
|
||||
"""
|
||||
|
||||
path: str = Field("", max_length=300)
|
||||
@@ -67,9 +68,9 @@ def collect(
|
||||
if auth.MULTI_USER
|
||||
else auth.local_user(db)
|
||||
)
|
||||
# The operator's own clicking is not traffic. Only in multi-user mode —
|
||||
# locally everyone is the owner, and excluding them would leave the
|
||||
# dashboard permanently empty on the machine it is developed on.
|
||||
# The operator's own clicks are not traffic. This applies only in
|
||||
# multi-user mode. Locally every user is the owner, and excluding them would
|
||||
# leave the dashboard empty on the machine the app is developed on.
|
||||
if auth.MULTI_USER and user is not None and auth.is_owner(user):
|
||||
return Response(status_code=204)
|
||||
|
||||
@@ -95,8 +96,10 @@ def summary(
|
||||
db: Session = Depends(get_db),
|
||||
_user: models.User = Owner,
|
||||
) -> dict:
|
||||
"""The whole dashboard in one aggregate response — a few kilobytes however
|
||||
much traffic sits behind it."""
|
||||
"""Returns the whole dashboard in one aggregate response.
|
||||
|
||||
The response is a few kilobytes however much traffic is behind it.
|
||||
"""
|
||||
return analytics.summary(db, days)
|
||||
|
||||
|
||||
@@ -111,9 +114,9 @@ def access_log(
|
||||
) -> dict:
|
||||
"""A page of the access log, newest first.
|
||||
|
||||
Unlike /summary this returns rows about people, which is the whole point of
|
||||
it — so it is behind the same owner gate, paged rather than dumped, and has
|
||||
no counterpart the people it describes can reach.
|
||||
Unlike `/summary`, this returns rows about people, which is what it is for.
|
||||
It is therefore behind the same owner gate, it is paged rather than returned
|
||||
in full, and the people it describes have no endpoint that reaches it.
|
||||
"""
|
||||
page = accesslog.recent(
|
||||
db, limit=limit, before_id=before_id, kind=kind, query=q
|
||||
|
||||
@@ -53,15 +53,19 @@ def me_payload(user: models.User, db: Session) -> dict:
|
||||
|
||||
@router.get("/me")
|
||||
def me(request: Request, response: Response, db: Session = Depends(get_db)):
|
||||
"""Who am I? In multi-user mode this also bootstraps the session: with no
|
||||
(or an invalid) cookie it creates a guest user and sets one — the
|
||||
frontend calls this on load and after any 401."""
|
||||
"""Returns the current user.
|
||||
|
||||
In multi-user mode this also establishes the session. If the cookie is
|
||||
missing or invalid, the endpoint creates a guest user and sets a cookie. The
|
||||
frontend calls it on load and after any 401.
|
||||
"""
|
||||
if not auth.MULTI_USER:
|
||||
user = auth.local_user(db)
|
||||
else:
|
||||
user = auth.resolve_session_user(request, db)
|
||||
if user is None:
|
||||
# Each new guest is a database row — cap how fast one IP can mint them.
|
||||
# Each new guest is a database row, so cap how fast one IP can
|
||||
# create them.
|
||||
limits.rate_limit("guest", request)
|
||||
user = models.User(is_guest=True)
|
||||
db.add(user)
|
||||
@@ -80,8 +84,11 @@ def register(
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = Depends(auth.get_current_user),
|
||||
):
|
||||
"""Upgrade the current guest in place — same user_id, so every adventure,
|
||||
scenario, script and setting they created as a guest is kept."""
|
||||
"""Upgrades the current guest in place.
|
||||
|
||||
The `user_id` does not change, so every adventure, scenario, script, and
|
||||
setting they created as a guest is kept.
|
||||
"""
|
||||
if not auth.MULTI_USER:
|
||||
raise HTTPException(400, "Accounts are disabled in local mode.")
|
||||
limits.rate_limit("auth", request)
|
||||
|
||||
+28
-18
@@ -1,14 +1,15 @@
|
||||
"""AI Chat — a plain scratchpad for talking to a model directly.
|
||||
"""AI Chat: a plain scratchpad for talking to a model directly.
|
||||
|
||||
Power users only (the AIDND_POWER_USERS email allowlist). Deliberately thin:
|
||||
no story context, no scripts, no world state, and nothing persisted — the
|
||||
conversation lives in the browser and is posted up whole on each turn. It
|
||||
exists to poke at models, prompts and endpoints without starting an adventure.
|
||||
Power users reach it, which means the `AIDND_POWER_USERS` email allowlist. It is
|
||||
deliberately thin. It adds no story context, no scripts, and no world state, and
|
||||
it persists nothing. The conversation lives in the browser and is posted in full
|
||||
on each turn. It exists for testing models, prompts, and endpoints without
|
||||
starting an adventure.
|
||||
|
||||
Model choice is free-form when the user brought their own API key. On the
|
||||
shared demo key it stays pinned to the AIDND_DEMO_MODELS whitelist, exactly as
|
||||
turns are: the server funds that key, so it must not be able to reach paid
|
||||
models by way of this page.
|
||||
Model choice is free when the user brought their own API key. On the shared demo
|
||||
key the model stays pinned to the `AIDND_DEMO_MODELS` allowlist, exactly as it is
|
||||
for turns. The server funds that key, so this page must not let it reach paid
|
||||
models.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
@@ -41,10 +42,13 @@ PowerUser = Depends(power_user)
|
||||
def _resolve_model(
|
||||
settings: models.Settings, requested: str | None
|
||||
) -> tuple[auth.ProviderConfig, str | None]:
|
||||
"""Provider config for this chat, plus a note when the requested model was
|
||||
not honoured. The pinning rule itself lives in resolve_provider_config —
|
||||
this only reports the substitution it made, so there is exactly one place
|
||||
that decides what the demo key is allowed to talk to."""
|
||||
"""Returns the provider config for this chat, plus a note when the requested
|
||||
model was not used.
|
||||
|
||||
The pinning rule lives in `resolve_provider_config`. This function only
|
||||
reports the substitution that call made, so one place decides what the demo
|
||||
key may talk to.
|
||||
"""
|
||||
cfg = auth.resolve_provider_config(settings, model_override=requested)
|
||||
wanted = (requested or "").strip()
|
||||
if wanted and wanted != cfg.model:
|
||||
@@ -60,9 +64,12 @@ async def chat_config(
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = PowerUser,
|
||||
):
|
||||
"""What this page can talk to: the resolved endpoint/model, whether model
|
||||
choice is pinned to the demo whitelist, and the endpoint's model listing
|
||||
(best effort — an unreachable endpoint just yields an empty list)."""
|
||||
"""Returns what this page can talk to.
|
||||
|
||||
The response holds the resolved endpoint and model, whether model choice is
|
||||
pinned to the demo allowlist, and the endpoint's model listing. The listing
|
||||
is best effort, and an unreachable endpoint returns an empty list.
|
||||
"""
|
||||
settings = get_settings(db, user)
|
||||
cfg = auth.resolve_provider_config(settings)
|
||||
listing = await list_endpoint_models(cfg)
|
||||
@@ -82,8 +89,11 @@ async def chat_config(
|
||||
|
||||
async def run_chat(cfg: auth.ProviderConfig, settings: models.Settings, payload: schemas.ChatRequest,
|
||||
note: str | None, db: Session, user: models.User):
|
||||
"""SSE generator mirroring the turn stream's event shape: reasoning/chunk
|
||||
while generating, then done — so the frontend reuses the same plumbing."""
|
||||
"""Streams the reply as SSE, using the turn stream's event shape.
|
||||
|
||||
The generator emits `reasoning` and `chunk` events while generating and then
|
||||
a `done` event, so the frontend reuses the same code.
|
||||
"""
|
||||
if note:
|
||||
yield sse({"type": "note", "detail": note})
|
||||
provider = OpenAICompatibleProvider(
|
||||
|
||||
@@ -9,9 +9,11 @@ router = APIRouter(prefix="/api/debug", tags=["debug"])
|
||||
def recent_requests():
|
||||
"""Most-recent-first log of provider requests/responses (no API keys).
|
||||
|
||||
The log is a single process-wide ring buffer with no per-user
|
||||
attribution, so in multi-user (hosted) mode it would leak other players'
|
||||
prompts — disabled there, available on local installs."""
|
||||
The log is a single process-wide ring buffer with no per-user attribution,
|
||||
so in multi-user mode, which is how a hosted deployment runs, it would expose
|
||||
other players' prompts. It is disabled there and available on a local
|
||||
install.
|
||||
"""
|
||||
if auth.MULTI_USER:
|
||||
raise HTTPException(403, "The debug log is only available on local installs.")
|
||||
return debuglog.recent()
|
||||
|
||||
@@ -54,9 +54,10 @@ def get_scenario(
|
||||
user: models.User = Depends(auth.get_current_user),
|
||||
):
|
||||
scenario = get_scenario_or_404(scenario_id, db, user)
|
||||
# Funnel step. Only for shared scenarios: opening one is the first sign a
|
||||
# visitor is interested, whereas someone editing their own is already past
|
||||
# this point — and their titles are theirs, not a statistic.
|
||||
# A funnel step, recorded for shared scenarios only. Opening one is the
|
||||
# first sign that a visitor is interested, and someone editing their own
|
||||
# scenario is already past this point. Their titles are theirs rather than a
|
||||
# statistic.
|
||||
if scenario.is_public:
|
||||
analytics.record_event(analytics.EV_SCENARIO_OPEN, user)
|
||||
return scenario
|
||||
@@ -204,8 +205,9 @@ def import_scenario(
|
||||
if isinstance(schema, dict):
|
||||
fields["stat_schema"] = schema
|
||||
|
||||
# AI Dungeon bundles carry an `image` too, so this is worth honouring — but
|
||||
# it's untrusted input, hence sanitize() rather than a straight assignment.
|
||||
# An AI Dungeon bundle also carries an `image`, so this reads it. The value
|
||||
# is untrusted input, so it goes through `sanitize()` rather than a direct
|
||||
# assignment.
|
||||
image = images.sanitize(bundle.get("image"), schemas.IMAGE_MAX)
|
||||
if image:
|
||||
fields["image"] = image
|
||||
@@ -216,10 +218,10 @@ def import_scenario(
|
||||
scenario = models.Scenario(**fields, user_id=user.id)
|
||||
if not scenario.title:
|
||||
scenario.title = "Imported Scenario"
|
||||
# Raw-dict import bypasses the schemas — clamp to VARCHAR widths
|
||||
# (Postgres enforces them; see schemas.py). Column defaults haven't been
|
||||
# applied yet at this point (that happens at flush), so a bundle with no
|
||||
# `tags` key leaves the attribute None — hence the `or ""`.
|
||||
# A raw-dict import bypasses the schemas, so truncate to the VARCHAR widths.
|
||||
# Postgres enforces them. See `schemas.py`. Column defaults have not been
|
||||
# applied yet, because that happens at flush, so a bundle with no `tags` key
|
||||
# leaves the attribute None, which is why the code says `or ""`.
|
||||
scenario.title = scenario.title[:schemas.NAME_MAX]
|
||||
scenario.tags = (scenario.tags or "")[:schemas.TAGS_MAX]
|
||||
db.add(scenario)
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_script(
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = Depends(auth.get_current_user),
|
||||
):
|
||||
"""Dry-run one hook against sample text — no AI call, no persistence."""
|
||||
"""Runs one hook against sample text, making no AI call and storing nothing."""
|
||||
script = get_script_or_404(script_id, db, user)
|
||||
limits.rate_limit("script-test", request, user)
|
||||
result = run_hook(
|
||||
@@ -145,7 +145,8 @@ def import_script(
|
||||
|
||||
script = models.Script(
|
||||
user_id=user.id,
|
||||
# Raw-dict import bypasses the schemas — clamp to the VARCHAR width.
|
||||
# A raw-dict import bypasses the schemas, so truncate to the VARCHAR
|
||||
# width.
|
||||
name=(pick("name") or "Imported Script")[:schemas.NAME_MAX],
|
||||
description=pick("description"),
|
||||
library_js=pick("library", "library_js", "sharedLibrary"),
|
||||
|
||||
@@ -10,8 +10,11 @@ router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
|
||||
def get_settings(db: Session, user: models.User) -> models.Settings:
|
||||
"""Per-user settings row, created on first access (Phase 8: settings —
|
||||
endpoint, key, models, memory config — are per user, not global)."""
|
||||
"""Returns the user's settings row, creating it on first access.
|
||||
|
||||
Phase 8 made settings per user rather than global. They cover the endpoint,
|
||||
the key, the models, and the memory configuration.
|
||||
"""
|
||||
settings = (
|
||||
db.query(models.Settings).filter(models.Settings.user_id == user.id).first()
|
||||
)
|
||||
@@ -50,14 +53,16 @@ def update_settings(
|
||||
if embedding_model_changed:
|
||||
# Vectors from the old model have a different dimensionality/space;
|
||||
# clear them so the post-turn task re-embeds with the new model.
|
||||
# (This user's adventures only — settings are per-user now.)
|
||||
# This covers only this user's adventures, because settings are per
|
||||
# user now.
|
||||
#
|
||||
# Both columns, and the flag. This is the one place that clears vectors
|
||||
# in bulk rather than through memorybank.set_vector, and when the
|
||||
# vectors moved to embedding_blob it kept nulling the old JSON column
|
||||
# alone: the blob survived, `embedded` stayed true, and _embed_pending
|
||||
# — which looks for embedded IS FALSE — never picked the rows up. The
|
||||
# bank went on ranking against the previous model's vectors forever.
|
||||
# alone. The blob survived, `embedded` stayed true, and
|
||||
# `_embed_pending`, which selects rows where `embedded IS FALSE`, never
|
||||
# found the rows. The bank kept ranking against the previous model's
|
||||
# vectors.
|
||||
owned = (
|
||||
db.query(models.Adventure.id)
|
||||
.filter(models.Adventure.user_id == user.id)
|
||||
@@ -70,15 +75,18 @@ def update_settings(
|
||||
# `embedded` drops these rows out of the catalogue query, so retrieval
|
||||
# stops asking for them, and by the time _embed_pending puts one back
|
||||
# it has gone through set_vector, which evicts that entry. The rule
|
||||
# holds — anything that removes a memory from play self-corrects.
|
||||
# holds: anything that removes a memory from play corrects itself.
|
||||
db.commit()
|
||||
return settings
|
||||
|
||||
|
||||
async def list_endpoint_models(cfg: auth.ProviderConfig) -> dict:
|
||||
"""GET the endpoint's /models listing. Doubles as a connectivity check, so
|
||||
failures come back as {"ok": False, "detail": ...} rather than raising."""
|
||||
# SSRF guard: never probe a non-public address the user pointed us at.
|
||||
"""Fetches the endpoint's /models listing.
|
||||
|
||||
The call also serves as a connectivity check, so a failure returns
|
||||
`{"ok": False, "detail": ...}` rather than raising.
|
||||
"""
|
||||
# SSRF guard. Never probe a non-public address the user supplied.
|
||||
reason = await run_in_threadpool(netguard.endpoint_block_reason, cfg.endpoint_url)
|
||||
if reason:
|
||||
return {"ok": False, "detail": f"Can't reach that endpoint — {reason}."}
|
||||
@@ -100,7 +108,8 @@ async def list_endpoint_models(cfg: auth.ProviderConfig) -> dict:
|
||||
data = resp.json()
|
||||
models_available = [m.get("id", "?") for m in data.get("data", [])]
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
pass # non-JSON or unexpected shape — connectivity is still confirmed
|
||||
pass # The body is not JSON or has an unexpected shape. The endpoint
|
||||
# is still reachable.
|
||||
return {"ok": True, "models": models_available}
|
||||
|
||||
|
||||
@@ -110,8 +119,11 @@ async def test_connection(
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = Depends(auth.get_current_user),
|
||||
):
|
||||
"""Cheap connectivity check against whatever the turn engine would actually
|
||||
use — including the shared demo endpoint when the user has no key."""
|
||||
"""Runs a cheap connectivity check against whatever the turn engine would use.
|
||||
|
||||
That includes the shared demo endpoint, when the user has no key of their
|
||||
own.
|
||||
"""
|
||||
limits.rate_limit("connection-test", request, user)
|
||||
settings = get_settings(db, user)
|
||||
return await list_endpoint_models(auth.resolve_provider_config(settings))
|
||||
|
||||
@@ -6,15 +6,19 @@ from ..database import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/story-cards", tags=["story-cards"])
|
||||
|
||||
# AI Dungeon world-info / story-card array format. Its field names differ from
|
||||
# our columns: value<->entry, title<->name, description<->notes. The extra
|
||||
# `useForCharacterCreation` flag has no equivalent here — ignored on import,
|
||||
# emitted as false on export so round-tripping through AI Dungeon stays valid.
|
||||
# The AI Dungeon world-info and story-card array format. Its field names differ
|
||||
# from this app's columns: `value` maps to `entry`, `title` maps to `name`, and
|
||||
# `description` maps to `notes`. The extra `useForCharacterCreation` flag has no
|
||||
# equivalent here. It is ignored on import and written as false on export, so a
|
||||
# round trip through AI Dungeon stays valid.
|
||||
|
||||
|
||||
def _visible_owner(scenario_id, adventure_id, db, user):
|
||||
"""Resolve the scenario/adventure a caller may *read* cards from (public
|
||||
demo scenarios included), or 404/422."""
|
||||
"""Resolves the scenario or adventure a caller may read cards from.
|
||||
|
||||
Public demo scenarios are included. The function raises a 404 or a 422 when
|
||||
it cannot resolve one.
|
||||
"""
|
||||
if (scenario_id is None) == (adventure_id is None):
|
||||
raise HTTPException(422, "Provide exactly one of scenario_id or adventure_id")
|
||||
if scenario_id is not None:
|
||||
|
||||
Reference in New Issue
Block a user