Current conventions for the Cymbal Coffee Oracle 26ai + Vertex AI demo. This file and
.agents/knowledge/are the living surfaces for durable agent guidance.
- Litestar app setup stays thin:
src/app/server/wires plugins, middleware, routes, and templates; domain packages own behavior. - Domain packages use
controllers/,services/, andschemas/subpackages. Cross-domain imports go through package exports, not deep private modules. - Dishka has exactly three user providers in
src/app/ioc.py:LitestarPersistenceProvider,IntegrationsProvider, andDomainServiceProvider. LitestarPersistenceProviderowns APP-scopedOracleAsyncConfigand REQUEST-scopedOracleAsyncDriver.IntegrationsProviderowns APP-scoped external integrations: GenAI client, Oracle ADK store, SQLSpec ADK session service, intent classifier, persona manager, and ADK runner.DomainServiceProviderowns REQUEST-scoped services: products, stores, cache, metrics, tools, Vertex AI, and vector-search orchestration.- SQL lives in
src/app/db/sql/*.sqlas named queries. Domain services calldb_manager.get_sql("name"); inline service SQL is rejected by tests. - List endpoints use
create_filter_dependenciesin controllers andlist_with_count(*filters)in services for pagination/filter wiring. - Product vectors and embedding-cache vectors are
VECTOR(3072, FLOAT32)values generated bygemini-embedding-2. - Oracle feature-demo metadata belongs inline in the baseline DDL when the
branch is still curating the reference schema. Keep
COMMENT ONstatements for compatibility and add 26aiANNOTATIONS(...)on documented table, column, and normal-index surfaces; do not create follow-up migrations or separateALTER ... ANNOTATIONS(...)statements just because local SQL parsers lag Oracle syntax. - ADK conversation state uses SQLSpec's Oracle ADK extension tables
(
adk_session,adk_event, optional memory). Litestar browser sessions use the separate SQLSpec Litestar session table (app_session). - Chat uses classifier-first ADK 2 orchestration.
ADKRunnerchecks response cache, classifies intent, sendsPRODUCT_RAGturns through direct menu grounding (storing recommended products in session state underlast_productsfor subsequent pronoun resolution), and uses the ADK Workflow/BaseNode graph for non-RAG streamed model conversation. - ADK tools are closure-bound per request inside
ADKRunner; they must use the active Dishka request services, not module globals. /api/chat/streamis an SSE endpoint. Streaming runs ADK withRunConfig(streaming_mode=StreamingMode.SSE)for non-RAG turns and preserves the non-streaming response payload keys.- Product RAG turns must not stream speculative model deltas. The runner emits a
single grounded final event.
_compose_grounded_answermay make a bounded Gemini structured-output call to choose candidate product ids and detect an off-menu term, but the model must not write final product copy. Python validates selected ids against retrieved rows, renders names/prices/details from those rows, and falls back to_grounded_product_answeron timeout, malformed output, invalid ids, or non-credential model errors. Credential failures surface asAIServiceUnconfigured. - Chat page history is hydrated from the Litestar-session-bound ADK session.
Display history lives in ADK session state under
display_history, with ADK event replay as a fallback. - The chat sidebar clear button deletes the current ADK session/events and clears only the Litestar ADK bridge keys. It must not clear menu data, metrics, response cache, or embedding cache.
- Product RAG tool calls record
vector_query,embedding_ms,oracle_ms,tool_ms, result counts, embedding cache status, and grounded-answer mode (structured,template,timeout,rejected, orerror); the chat UI shows the stable message-level telemetry along with intent and response-cache status. - Store/location/inventory work stays in the products domain: seed coordinates, Dallas, and curated inventory; route store answers from service facts.
- Store selectors and inventory UI must be driven by
StoreServiceand fixture data. Do not hardcode a few store IDs in Jinja as "presets"; that silently narrows the demo to those locations and drifts from the seeded store list. - Store-aware product RAG must vary or bypass response-cache reads whenever safe location/store context is present, so one store's inventory answer is not reused for another store.
- Maps URLs are the no-key baseline. Browser coordinates are opt-in and request-scoped; embeds require a separate restricted Maps Embed key.
- Frontend pages are HTMX + Jinja + Tailwind v4 + vanilla JavaScript/ApexCharts
through
litestar-vitetemplate mode, not a standalone SPA. src/resources/is the Vite/npm project root. Keeppackage.json,package-lock.json,tsconfig.json,vite.config.ts,public/, generated bridge files, andnode_modules/there; PythonViteConfig.paths.rootmust point at the same directory.- Explore-only educational calculators stay client-only in
src/resources/*.jsand wire to Jinja markup throughdata-*attributes. Keep this as vanilla Vite-bundled JavaScript. - Vector footprint estimates use raw bytes by format (
N * d * 8for FLOAT64,N * d * 4for FLOAT32,N * dfor INT8,N * ceil(d / 8)for BINARY), and Oracle's rough HNSW Vector Pool estimate of1.3 * N * d * element_size. UseDBMS_VECTOR.INDEX_VECTOR_MEMORY_ADVISORfor exact HNSW/IVF sizing; do not model IVF or HNSW neighbors with invented client-side overhead formulas. - EXPLAIN PLAN uses two named SQL calls: one
EXPLAIN PLAN FOR ...and oneDBMS_XPLAN.DISPLAY()read. coffeeis the app CLI. Keepbulk-embed,export-fixtures,load-fixtures,clear-cache,model-info, andrunvisible there.- Keep public CLI modules mostly declarative. Large workflows belong in focused
private
app.cli._helpersmodules; small command-local helpers can live incommands.py. Do not add compatibility shim or facade modules. sanitize_for_jsoncamel-cases msgspec Struct keys for wire encoding. In the chat domain (likeADKRunnerand grounding formatting helpers), check both snake_case and camelCase keys using_get_field(row, snake_name)to remain resilient to case conversion differences between unit test mocks and runtime database responses.- Product availability lookups (
PRODUCT_AVAILABILITY) resolve product names via exact match first, falling back to pronoun resolution fromlast_productsin session history, and finally using Vertex AI vector search to resolve partial/imprecise product names (e.g. 'Gemini' -> 'Gemini Rush') before checking store inventory. - ORDS Stateless Deployment on Cloud Run: Deploy ORDS on Cloud Run stateless, peered via Direct VPC, utilizing Secret Manager for the database system password (
DATABASE_SYSTEM_PASSWORD) to enable automatic schema registration/validation on container boot. (derived from cloudrun-ords-deploy) - Oracle CDN for APEX Assets: Set
IMAGE_PREFIXto Oracle's public APEX CDN (https://apex.oracle.com/i/or version-specific URL) via database script. This keeps the ORDS container stateless and avoids the need for mounting static asset directories or using Cloud Storage FUSE. (derived from cloudrun-ords-deploy) - Image Re-hosting for Private Deployments: When deploying to Cloud Run in restricted environments where external registry authentication is unavailable or unreliable at deployment time, re-host official images (like
container-registry.oracle.com/database/ords:<version>) in GCP Artifact Registry. (derived from cloudrun-ords-deploy) - AutoREST Table Enablement: Utilize Oracle ORDS
AutoRESTcapability to expose database tables (e.g.,PRODUCTS) as REST endpoints directly via SQLSpec migrations or startup scripts using simple PL/SQL calls (ORDS.enable_object), providing a zero-code private data access path. (derived from cloudrun-ords-deploy) - Naming a query parameter argument
queryin a Litestar controller action (e.g.,def handler(query: FromQuery[str])) causes Litestar to pass the entire query parametersMultiDictto it. Use a different Python argument name (e.g.,search_query) and map it explicitly usingQueryParameter(name="query")to avoid this. (derived from fix-explore-search-ux)
- Use SPDX headers on source files:
SPDX-FileCopyrightText: 2026 Google LLCplusSPDX-License-Identifier: Apache-2.0. Ruff'sCPY001rule enforces presence; thelicense-headersprek hook (runningtools/license_headers.py) inserts them on commit. See.agents/code-styleguides/python.mdfor the per-language comment forms. - Use PEP 604 unions (
str | None) and typed public boundaries. - Keep demo-facing public Python modules public-first: exported controllers, services, settings, commands, and lifecycle entry points should appear before private mechanics. Move substantial helper runs into focused private sibling modules or below the public API; avoid compatibility shim or facade modules.
src/app/ioc.pymust not usefrom __future__ import annotations; Dishka introspects provider annotations at runtime.- Prefer handler-argument
Inject[T]over route-level@injectdecorators. - Use
@async_injectfor async Click commands; command modules should not callsqlspec.utils.sync_tools.run_directly or define nested async runners. - SQLSpec reads should pass
schema_type=whenever a typed row/schema exists. - Use
SQLSpecAsyncServicefromapp.lib.serviceas the domain service base. - Use SQLSpec builders for dynamic writes (
sql.update,sql.insert_into) and named SQL for static reads. - Use
from_json/to_jsonfromapp.utils.serialization, not ad hocjson.loads/json.dumps, in app code. - Use
schema_dump(..., wire_format=False)fromapp.utils.serializationwhen dumping msgspec schemas into database writes that require Python/Oracle column names instead of camelized wire names. - Push aggregate null handling into SQL with
COALESCE, not Pythonvalue or 0. - For
gemini-embedding-2, encode query/document purpose in the embedding input text. Do not send the old embeddingtask_typeAPI parameter. - Keep placeholder Vertex project checks on the typed
AIServiceUnconfiguredpath so local misconfiguration returns a clean 503. - Settings stay dataclass/cached, quiet, and effectively immutable; remove future knobs until source behavior wires them.
- Prefer
@pytest.mark.anyiofor async tests. - Test files live under strict behavior and module-path buckets:
src/tests/unit/<module path>/test_*.pyorsrc/tests/integration/<module path>/test_*.py. Do not add new top-level issue buckets such assrc/tests/api, and do not add directsrc/tests/unit/test_*.pyorsrc/tests/integration/test_*.pyfiles. - Before creating a new test file, find the existing module-path test file and add the behavior there. Prefer parameterized cases, shared fixtures, and existing functional sections over one-file-per-issue coverage.
- Nested test directories are real packages with SPDX-bearing
__init__.pyfiles so repo ruff checks do not treat moved tests as implicit namespaces. - Tests should prove application behavior: service logic, controller responses, typed SQL/data contracts, settings parsing, DI resolution, chat payloads, vector telemetry, and HTMX partial behavior.
- Do not add tests whose only subject is repo structure, source ordering,
docs text,
pyproject.toml, workflow YAML, tool scripts, or third-party import surfaces. Use lint, build, smoke checks, or focused tool validation for those concerns instead of growingsrc/tests. coffee upgradeis the packaged/end-user install command and loads committed fixtures after migrations. Raw SQLSpec developer commands such as downgrade and current stay underpython manage.py database ...; do not exposecoffee downgrade.- Put reusable test constants and repo paths in
src/tests/support/instead of hardcoding relative parent depths in moved tests. - Handler tests that hit
Inject[T]must mock the full DI chain when the test is not meant to construct real services. - API tests for HTMX partial branches should send
HX-Request: trueor use the repo's HTMX test fixture. - Real Oracle integration tests use the repo-managed Oracle lifecycle
(
make start-infra, thenuv run python manage.py database upgrade --no-prompt) and share DDL/fixture loading through integration fixtures. Keep SQLSpec pool resets only where needed for pytest event-loop binding. - Vector-search tests should assert typed contextual fields and result counts, not only final answer text.
- Keep cache and Oracle mutation tests deterministic under parallel workers by using unique keys/SKUs plus targeted cleanup instead of truncating shared fixture tables per test.
- Use focused tests while editing, then run
make lintandmake testbefore claiming branch-level completion.
- HNSW
ORGANIZATION INMEMORY NEIGHBOR GRAPHindexes require non-zerovector_memory_sizebefore index creation. The local Oracle startup scripts configure this before migrations create vector indexes. - Oracle Free Edition has a hard SGA/PGA cap; the project default vector pool is sized to fit the demo dataset instead of trying to raise SGA targets.
- Local APEX/ORDS stays optional:
make start-infrais DB-only, while APEX-enabled flows start ORDS through the sharedOrdsSidecarused bymanage.py infra ords. ORDS startup must probe the runtime version and fail clearly below 26.1.1; inconclusive version probes are retryable until timeout./ords/readiness may accept routed non-5xx responses, but/i/static media readiness must reject 403/404 because the smoke check usescurl -fsS. V$SGAINFOreports the vector pool asVector Memory Area; verification SQL should accept that label.VECTOR(N, FLOAT32)DDL,EMBEDDING_DIMENSIONS, and committed fixture vector lengths are co-versioned. Do not mix older lower-dimensional examples into this app.ORACLE_ADK_IN_MEMORYandORACLE_LITESTAR_SESSION_IN_MEMORYdefault to true so SQLSpec extension tables use Oracle INMEMORY where the database supports it.OracleAsyncConfigusesconnection_config=; olderpool_config=wiring can silently drop credentials and surface asDPY-4001..agents/archive/is ignored disposable history; synthesize durable knowledge into.agents/knowledge/or this file before archive cleanup.- Flow sync/status is a Flow skill workflow backed by Beads state and
.agents/docs; do not assume aflow syncshell command exists, and do not runbd syncbecause Beads does not expose that command. - APEXlang lifecycle commands require SQLcl 26.1.2+ and live under
manage.py infra apex generate|export|validate|import. Keep source-controlled APEX assets undersrc/apex/<alias>/. SQLcl 26.1.2 generated this repo's APEXlang project with hyphenated directories such asshared-components/andsupporting-objects/; treat generated SQLcl output as authoritative when docs and older notes disagree. - APEX 26.1 can validate app-level
restDataSourceAPEXlang, but this local runtime currently rolls back import withWWV_WEBSRC_MODULE_RSERVER_FKwhen REST Data Sources reference arestDataSourceServer. Use SQL-backed APEX reports for importable demo pages and keep REST Source Catalog wiring open until a generated App Builder round-trip or Oracle patch proves the server/data-source import path. - Logging must use
app.lib.logprocessors, set Litestar/Granian env defaults, exclude static-asset logs, and filter only known ADK/Authlib warning noise. - PyApp onefile releases use the custom Bundle-Patch-Compile path, not
[tool.pyapp]oruvx pyapp build: build assets and wheel, bundle Python 3.13 dependencies withtools/bundler.py, patch PyApp to install under the XDG config path (~/.config/cymbal-coffeeby default), and compile Linux launchers withcargo zigbuild --target <arch>-unknown-linux-gnu.2.17. make build-onefilemust forceUV_PYTHON/PYAPP_BUILD_PYTHONto the explicit CPython 3.13 build interpreter because the repo-local.python-versionremains3.12for normal development.- Release containers wrap the onefile binary, not a separate Python install
path. Build from the single distroless Dockerfile at
tools/deploy/docker/Dockerfile, allow Docker context access todist/coffee-*-linux-gnu, and expose/app/walletwithTNS_ADMIN/WALLET_LOCATIONpointed there for read-only Oracle wallet mounts. - Release CI builds onefiles and containers on native architecture runners:
ubuntu-24.04for x86_64/amd64 andubuntu-24.04-armfor aarch64/arm64. Cross-compilation via emulation is rejected — both arches build natively in the matrix and the final GitHub Release job downloadsdist/coffee-linux-x86_64dist/coffee-linux-aarch64plus checksums and attaches them withfail_on_unmatched_files: true. Onefile and container smoke checks invokecoffee upgrade --help(the packaged end-user command) rather thancoffee run.
- Docs publish via
.github/workflows/docs.ymlon push to main: builds withsphinx-build -W --keep-going, uploadsactions/upload-pages-artifact@v3, deploys viaactions/deploy-pages@v4. Do not addoracledb,vertexai,google.adk,google.cloud, orgoogle.genaitoautodoc_mock_importsindocs/conf.py— they are hard deps and mocking breaks sqlspec's import-timeoracledb.__version__lookup. - Docs information architecture is three tiers and locked:
index.mdhero +tour.mdwalkthrough at the front,concepts/{vector-search,rag,agent-flow}.mdfor deep dives, andreference/{quickstart,cli,api,internals,developers}.mdfor the reference appendix. Concept page count is exactly 3 — not 2, not 4. Use thesphinx-immaterialtheme (notshibuya) with custom admonitionstour-stop,oracle-internals, andagent-detaildefined inconf.py. - Code embeds in docs use
literalincludewith# docs:start-<name>/# docs:end-<name>anchor pairs in the source file — never copy-paste a snippet inline. Each embed has a one-or-two-sentence framing paragraph above it that names the file/class and its role so readers know what larger thing the snippet is part of. - Autodoc scope is intentionally narrow:
ADKRunner,CacheService,MetricsService,ProductService, and the threedomain/*/schemaspackages. Do NOT add CLI commands, settings dataclasses, plugin wiring, or IoC providers — their shapes are stable and reading the source is faster than reading generated docs. AsyncTestClientMUST be entered withasync with; a bare instance never runs the app lifespan, so Dishka containers (and the Oracle pools they hold) leak across tests in the same process. Fixtures look likeasync def client(app): async with AsyncTestClient(app) as c: yield c.- HTMX endpoints that also need to accept JSON (e.g.
/api/vector-demo) parse the request body directly (await request.json()orawait request.form()) — do NOT declare them as a typeddata:body parameter. Litestar's body-DTO path rejects HTMX form posts when the route declares JSON body data. hx-ext="litestar"belongs only on JSON-templating panels. Scope it out withignore:litestaron partial-HTML-swap surfaces, otherwise the extension intercepts HTML responses and breaks the swap.- The chat form must capture
FormDatabefore applying any busy/disabled state to inputs — disabling a control removes it fromnew FormData(form), which causes the backend to receive an empty message and streamMessage cannot be empty. Regression-check this ordering. - Cached chat responses store only the product-lookup
sql_phases. At read time the cache hit is wrapped in a freshget-cached-responsephase so the UI shows both the cache hit and the original product-lookup context without retaining the original cache-miss phase. - Streaming
/api/chat/streamexception handling lives inside the async generator: catch broadly, log withlogger.aexception(), and yield a sanitized SSEerrorevent. Do not rely on Litestar exception middleware for failures that occur after SSE headers have been sent — it cannot intercept post-start stream errors. - If
make lintfails withfrontend-typecheckdue to missingtsc(TypeScript compiler), ensure frontend assets are installed insrc/resources/by runninguv run python manage.py assets installfirst. (derived from fix-explore-search-ux) - When committing
uv.lock, ensure it has been resolved using the public PyPI index (e.g., by running withUV_CONFIG_FILE=/dev/nulland--default-index https://pypi.org/simpleif your workstation configures a Google-internal repository likeus-python.pkg.dev), otherwise public CI builds will fail with 401 Unauthorized errors. (derived from fix-explore-search-ux)