name: soccer-workshop-setup description: Bootstrap the soccer analytics agent workshop. Starts the Oracle AI Database Free container, applies schema, loads the FIFA dataset, optionally trains models, populates LangChain OracleVS hybrid retrieval plus semantic memory, applies LangGraph OracleDB observability, and verifies OCI GenAI access. Use when starting the workshop or resetting a stale environment.
Soccer Workshop Setup
You are bootstrapping the soccer-analytics-agent workshop. Follow these steps strictly in order. Stop and surface the error on any failure.
Each step has a "If it fails:" hint pointing at the most common root cause we hit while building this. Skim them once before running so you know what to watch for.
Hybrid retrieval contract
This workshop is hybrid-first after ML inference. Coding agents must build and verify the LangChain OracleVS vector store (SOCCER_LANGCHAIN_DOCS) after PREDICCIONES_FINAL is loaded, and the final Grok 4 chat must ground explanatory answers with hybrid_retrieve or the startup hybrid_search(...) context before falling back to semantic-only memory. vector_search remains in the workshop as the baseline contrast: semantic similarity over semantic_memory only, without cached prediction documents or keyword/text scoring.
LangGraph OracleDB observability contract
Every workshop build must also initialize langgraph-oracledb OracleStore tables and prove the agent stores individual execution steps in Oracle. The current agent remains a small Grok prompt-protocol loop, but each turn writes ordered step records (turn_start, grounding_retrieved, model_response, tool_call, tool_result, final_response) into the LangGraph OracleDB store under namespace ("soccer-agent", "agent-steps", session_id). The API exposes these rows at GET /observability/{session_id} for demo/debugging.
Required workshop-day OCI values
The Grok 4 final chat is a required workshop capability, so setup must not silently proceed with placeholder OCI values. When building the workshop, inspect .env after it exists. If any of these three values are missing or still contain REPLACE_ME, ask the user to provide them before running scripts/verify.py, scripts/smoke_test.py, or declaring the workshop ready:
OCI_GENAI_ENDPOINT— regional OCI GenAI Inference endpoint.OCI_GENAI_API_KEY— bearer API key beginning withsk-.OCI_COMPARTMENT_ID— compartment OCID used in the GenAI request body.
If the user says they will provide these on the day of the workshop, leave placeholders in .env/.env.workshop.local, complete only the local Oracle/data/model setup, and explicitly report that Grok verification, smoke testing, and final public readiness remain blocked until those three values are pasted locally. Never write real OCI values into tracked files.
Steps
Check the container engine (Docker or Podman)
- The setup script auto-detects Docker (preferred) or Podman, so either works. To confirm one is present:
- Run:
docker info >/dev/null 2>&1 && echo docker || (podman info >/dev/null 2>&1 && echo podman)
- Run:
- If it prints neither: install Docker (https://docs.docker.com/get-docker/) or Podman (https://podman.io/). On Docker, make sure the daemon is running and the user is in the
dockergroup. Don't try to fix it from inside Claude Code — surface it.
- The setup script auto-detects Docker (preferred) or Podman, so either works. To confirm one is present:
Ensure
.envexists and gate on required OCI values- If
.envis missing at the repo root, copy from.env.example. - Inspect
.envforOCI_GENAI_ENDPOINT,OCI_GENAI_API_KEY, andOCI_COMPARTMENT_ID. If any are missing or still containREPLACE_ME, ask the user for those exact three values. The instructor may say they will provide them on workshop day; in that case, keep placeholders and continue only through local setup, but do not claim Grok/final readiness until they are supplied. - Oracle values use the defaults that match
docker/docker-compose.yml. - Never write real OCI values into tracked docs or examples; keep them in local ignored
.envor.env.workshop.localonly. - If it fails: stray BOM or wrong line endings in
.envwill makepython-dotenvsilently load nothing. Save as plain UTF-8 LF.
- If
Start the Oracle container
- Run:
bash .claude/skills/soccer-workshop-setup/scripts/01_start_oracle.sh - The script auto-detects Docker or Podman, picks an Apple-Silicon-native image on arm64 Macs (and the official amd64 image everywhere else), then polls until the container is healthy (up to 7.5 min).
- If it fails on port
1525: another container is bound to host port1525. Checkdocker ps(orpodman ps). The compose file deliberately picks1525because1521-1524are commonly taken; if1525is also taken you need to editdocker-compose.ymlandORACLE_DSN. - If it fails on an Apple Silicon Mac with an image/platform error: the launcher should have selected
gvenzl/oracle-free:latest. Confirmuname -mreportsarm64; if you are forcing the official image viaORACLE_IMAGE, unset it and re-run.
- Run:
Create the soccer user with workshop grants
- Run:
bash .claude/skills/soccer-workshop-setup/scripts/setup.sh - Idempotent: drops then recreates the user.
- Required grants are not just CONNECT/RESOURCE.
CREATE MINING MODELis also granted here — without it, step 8 below fails withORA-01031: insufficient privilegesdeep insideDBMS_VECTOR.LOAD_ONNX_MODEL. Do NOT remove that grant.
- Run:
Load match data
- Run:
uv run python scripts/setup_db.py - This needs CSV files (
results.csv,goalscorers.csv,shootouts.csv) indata/. - Canonical source (always use this — no substitutes): https://www.kaggle.com/datasets/martj42/international-football-results-from-1872-to-2017 (CC0). Download with
kaggle datasets download -d martj42/international-football-results-from-1872-to-2017 -p data/ --unzip(needs~/.kaggle/kaggle.json), or fetch the ZIP from the URL and unzip intodata/. - Stub-data path: if
results.csvis < 1 KB the workshop will still run, but later steps (embed_match_facts.py, ML feature engineering, bulk predictions) will produce mostly empty output. Do not "fix" zero-row counts in stub mode — load the real Kaggle dataset instead.
- Run:
Prepare model artifacts
- Run:
uv run python scripts/prepare_artifacts.py - The hub workshop ships production artifacts in
models/. This step validates those files and falls back to training from the loaded Oracle data only if the artifacts are missing or invalid. - If it fails:
MATCH_RESULTS/GOALSCORERSwere not loaded, dependencies are missing, or the local machine cannot train the XGBoost model. Re-run step 5 and inspect the training error.
- Run:
Apply memory schema and LangGraph observability store
- Run:
uv run python scripts/init_memory.py - This recreates the custom memory tables and calls
langgraph-oracledbOracleStore.setup()so the Oracle-backed step-observability tables exist before the app starts. - If it fails: the script drops custom memory tables in dependency order and recreates them. If it stops mid-way you may have a half-applied schema; just re-run it. If the failure mentions
langgraph-oracledb, confirm the uv environment installed the package frompyproject.toml/uv.lock.
- Run:
Load the ONNX embedding model (~30-90s)
- Run:
uv run python scripts/load_onnx_model.py - First run downloads from HuggingFace and uploads the augmented ONNX to Oracle.
- Why this is the trickiest step: Oracle AI Database's
VECTOR_EMBEDDING(...)SQL function expects an ONNX model with the tokenizer baked into the graph, NOT a vanilla transformer export. We learned this the hard way:- HuggingFace
optimum-cli export onnxproduces a model withinput_ids: [batch_size, sequence_length]— both variable. Oracle rejects it withORA-54426: Tensor "input_ids" contains multiple dimensions (2) of variable size. - The
oml4pypackage on PyPI is a 4-file stub with noEmbeddingModelhelper. The realoml4pyships with Oracle Database client, not pip. - The working PyPI tool is
onnx2oracle(this is whatscripts/load_onnx_model.pyuses). It builds the right augmented ONNX (tokenizer + transformer + pooling + normalize).
- HuggingFace
- If it fails with
ORA-01031: step 4 was skipped or the soccer user lacksCREATE MINING MODEL. - If it fails with
ORA-54426: something rewroteload_onnx_model.pyto skiponnx2oracle. Don't go back to rawoptimum-cliONNX — it will not work.
- Run:
Load precomputed predictions
- Run:
uv run python scripts/load_predictions.py - This should load roughly 2,500+ rows. Do not accept a 2-row test fixture as workshop-ready.
- Run:
Populate the LangChain OracleVS hybrid retrieval store
- Run:
uv run python scripts/load_langchain_vectors.py --reset --demo-query "Spain Brazil World Cup prediction evidence" - This happens after the ML prediction table exists: it turns
PREDICCIONES_FINAL,VW_TEAM_STATISTICS, and World Cup team/decade aggregates into LangChainDocumentrows inSOCCER_LANGCHAIN_DOCSusing thelangchain-oracledbPyPI package and the in-DB ONNX model. - The demo query must return at least one relevant prediction/fact document. This is the key evidence that the coding-agent build path prepared the same hybrid store Grok will use after the app starts.
- The script attempts a vector index, a native HYBRID VECTOR INDEX, and an Oracle Text index. Hybrid-index support is image/version-sensitive; it is okay if one optional index reports
skippedbecause the agent still retrieves with the best available native hybrid or Oracle Text + vector reciprocal-rank-fusion path. - If it fails before inserting rows: steps 8 or 9 were skipped,
langchain-oracledbis missing, or the ONNX model name inORACLE_EMBED_MODELis wrong.
- Run:
Embed match facts into semantic memory
- Run:
uv run python scripts/embed_match_facts.py - Tiny dataset gotcha: the SQL has
HAVING COUNT(*) >= 3, so if you've only loaded a 3-row stub CSV (no real FIFA data), this inserts zero facts and reportsInserted 0 semantic facts.That's expected, not a failure.
- Run:
Optionally showcase Oracle Agent Memory SDK
- Run:
uv run python scripts/showcase_oracle_agent_memory.py - This demonstrates the
oracleagentmemoryPyPI package in the same database with user/agent/thread-scoped durable memory. If the alpha SDK changes its API, surface the error but do not block the core workshop; the custom memory tables remain the primary path.
- Run:
Run the verifier, observability check, and hybrid-vs-semantic contrast check
- Run:
uv run python scripts/verify.py - All checks must be green. A red on
PREDICCIONES_FINALmeans the real prediction parquet was not prepared or loaded; re-run steps 6 and 9. A red onSOCCER_LANGCHAIN_DOCSmeans step 10 did not populate the hybrid retrieval store. A red onlanggraph-oracledb OracleStore observability setupmeans step 7 did not initialize the LangGraph OracleDB store. - Run the direct showcase pair:
uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py hybrid_retrieve '{"query":"Spain Brazil World Cup prediction evidence","limit":3}'uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py vector_search '{"query":"Spain Brazil World Cup prediction evidence","limit":3}'
- Explain the expected difference:
hybrid_retrievereturns LangChain OracleVS documents and can surface cached ML prediction rows withretrieval_mode;vector_searchreturns semantic-only facts fromsemantic_memory. - Run:
uv run python scripts/smoke_test.py. In addition to a realpredict_matchcall, this must print LangGraph OracleDB observability events includingturn_start,tool_call,tool_result, andfinal_response.
- Run:
Build and polish the React front-end with the
taste-skill(automatic for every attendee — do not skip unless--skip-ui-polishwas passed)- The workshop now ships a premium Vite + React + Tailwind + Framer Motion front-end under
frontend/. FastAPI serves the builtfrontend/dist/if it exists, and falls back to the bundledsoccer_agent/api/static/index.htmlotherwise. - Step 1 — build the React app first (requires Node 18+):
- Run:
cd frontend && npm ci && npm run build - This produces
frontend/dist/index.html+dist/assets/index-*.js,*.css, whichsoccer_agent/api/main.pymounts at/. - If Node is missing or too old: surface it clearly (print the
node --versionyou got and that Node 18+ is required) and fall back to the committed default UI —main.pyautomatically servessoccer_agent/api/static/index.htmlwhenfrontend/dist/is absent. Do NOT fail the whole setup over a missing Node; the API still runs against the bundled static page.
- Run:
- Step 2 — invoke the
taste-skill(vendored in this repo at.claude/skills/taste-skill/, so it is available in the repo's Claude Code session) via theSkilltool, then refine the React front-end source infrontend/src/against it (single accent, off-black base, suggestion chips, per-tool trace cards, typing skeleton, live status pill, tactile feedback, no emoji). Rebuild after refining:cd frontend && npm run build. - HARD CONSTRAINTS — the UI MUST keep working against the live API. Do NOT change:
- The fetch contract:
POST /chatwith body{session_id, message}→ renders{session_id, reply, tool_trace[]}, where each trace item is{name, args, result}. GET /health→{oracle, grok_configured}(used to drive the status indicator).GET /observability/{sessionId}for ordered LangGraph OracleDB step records.DELETE /memory/{sessionId}for reset.- The front-end calls these via relative paths (
/chat,/predict,/health,/memory/...), so it is port-agnostic — keep it that way (do not hardcode a host/port).
- The fetch contract:
- Verify after building: start the API, load
http://localhost:8000/, send "Predict Spain vs Brazil at a neutral venue.", and confirm the probability bar renders with thepredict_matchtool trace showingfeatures_used: 92. Copy the returnedsession_idand callGET /observability/{session_id}; it must show ordered LangGraph OracleDB step records for the turn. Then send "Use hybrid retrieval to explain the evidence for Spain vs Brazil, and contrast it with semantic-only memory." Expand the trace and confirmhybrid_retrieveappears or the reply cites the startup hybrid context fromSOCCER_LANGCHAIN_DOCS;vector_searchshould appear only for the requested semantic-only contrast. Then stop the server. - If it fails: revert the front-end source (
git checkout -- frontend/src/) and rebuild, or removefrontend/dist/to fall back to the committed defaultsoccer_agent/api/static/index.html— a polished-but-broken UI is worse than the shipped default. The workshop must remain runnable.
- The workshop now ships a premium Vite + React + Tailwind + Framer Motion front-end under
Done
- Print: "Workshop environment ready. Start the API with:
uv run uvicorn soccer_agent.api.main:app --reloadand open http://localhost:8000/"
- Print: "Workshop environment ready. Start the API with:
Flags (optional, when invoked with arguments)
--retrain: Pass--force-retraintoscripts/prepare_artifacts.pyin step 6.--skip-embeddings: Skip step 8 and the vector-store population in step 10. This disables the hybrid showcase; do not use it for the final workshop demo.--skip-ui-polish: Skip the taste-skill refinement of the React front-end in step 14. The React app is still built (frontend/dist/) so FastAPI serves it; this only skips re-applying the latest taste-skill standards tofrontend/src/.
Pitfalls & lessons learned (read this first if you're building on top)
Why these are here
This workshop was built end-to-end; every item below corresponds to a failure mode that broke a real session. They are not theoretical.
Oracle AI Database
CREATE MINING MODELis required forDBMS_VECTOR.LOAD_ONNX_MODEL. It's not implied byCONNECT, RESOURCE. The default workshop user gets it viasetup.sh(step 4).DBMS_VECTORis alreadyEXECUTEto PUBLIC on Oracle AI Database Free. You do NOT need to grantEXECUTE ON SYS.DBMS_VECTOR; in fact SYSTEM cannot grant on SYS-owned objects withoutGRANT ANY OBJECT PRIVILEGE, so trying will give youORA-01031.VECTOR(384, FLOAT32)is the type to use for embeddings. Pick the dim that matches your loaded ONNX model; the workshop uses 384 becauseall-MiniLM-L6-v2outputs 384.VECTOR_DISTANCE(a, :q, COSINE)returns lower-is-better. Order ascending. The query embedding (:q) must be passed as a Pythonarray.array('f', ...)of the right length; passing a numpy ndarray directly raises a type error.- LangChain OracleVS table is additive and workshop-critical.
SOCCER_LANGCHAIN_DOCSis managed bylangchain-oracledb; usescripts/load_langchain_vectors.py --resetafter retraining so cached prediction documents match the latest model. The final Grok 4 chat should use this table viahybrid_retrieve/startup grounding for evidence, not plain semantic memory alone. - LangGraph OracleDB observability is per-store-instance.
OracleStore.setup()must be called on each freshOracleStore(conn)object beforeput()/search()so the package initializes its internal table-name map, even after the schema tables already exist. - Native HYBRID VECTOR INDEX is version-sensitive. When the database can create the hybrid index,
OracleHybridSearchRetrieveris used directly. If an image cannot create it, the workshop still showcases hybrid retrieval by fusing Oracle Text results with vector similarity in Python, while all data and indexes remain in Oracle.
In-DB ONNX embedding models
- Use
onnx2oracle(PyPI). Notoptimum-clidirectly. Notoml4py(PyPI stub). Not a hand-rolledDBMS_VECTOR.LOAD_ONNX_MODELcall against a HuggingFace export. - Presets (
onnx2oracle presets):all-MiniLM-L6-v2(384, ~90MB),all-MiniLM-L12-v2(384, ~130MB),all-mpnet-base-v2(768, ~420MB),bge-small-en-v1.5(384, ~130MB),nomic-embed-text-v1(768, ~540MB). - The Oracle model name is uppercase with underscores (e.g.
ALL_MINILM_L6_V2). It is NOT the HuggingFace repo path.
python-oracledb (3.x) sharp edges
IS JSONCLOBs auto-decode to Python dict/list. If you wrotejson.loads(value)you'll getTypeError: the JSON object must be str, bytes or bytearray, not dict. Guard withisinstance(val, (str, bytes, bytearray)).- LOB locators die after connection close. If you build dataclasses inside a list comprehension AFTER the
with get_connection()block exits,.read()on any returned CLOB raisesDPY-1001: not connected to database. Fix: materialize all CLOBs (val.read() if hasattr(val, 'read') else val) INSIDE thewithblock. - Pass float32 vectors as
array.array('f', list). numpy ndarrays don't bind toVECTORcolumns directly. load_dotenv()with no args needs a stack frame. If you pipe Python to stdin (uv run python - <<EOF),find_dotenv()raisesAssertionError. Pass an explicit path:load_dotenv(Path.cwd() / ".env").
OCI Generative AI Inference
- The
sk-...bearer key authenticates against the inference endpoint only, not the control plane. You can call/20231130/actions/chatand/actions/embedText, but you can'tLISTmodels with that key. To learn what model IDs are valid, look in the OCI Console under Generative AI → Models. - Compartment OCID is required in the request body (under
servingMode.compartmentId), even though authentication is by bearer key. Both must be set. - Tool calling is NOT exposed through this endpoint with the bearer key. Including a
toolsarray (in either GENERIC or COHERE apiFormat, with or without the OpenAI-style{"type":"FUNCTION","function":{...}}wrapper) returns400: Please pass in correct format of request.on every model we tested —xai.grok-4,xai.grok-3,cohere.command-r-plus-08-2024. The agent loop works around this with a prompt protocol (see next item). - Prompt-protocol tool calling pattern: append tool schemas to the system message, instruct the model to emit a single JSON object
{"tool": "...", "args": {...}}when it wants to call one, and parse JSON tool calls out of the response text. Seesoccer_agent/agent/grok_client.pyfor the working implementation. role: "tool"messages get rejected withouttoolCallId. Since we never receive atoolCallId(tool calling isn't native), surface tool results back to the model asrole: "system"messages instead. Also: skip persistedrole: "tool"turns when rebuilding the message list for the next iteration.
Container networking
- Bind Oracle to
127.0.0.1:1525:1521, not0.0.0.0. The workshop image'ssystempassword is well-known; never expose port 1521/1525 to a public interface. - Healthcheck must use a sentinel value, not
1. Searching for1in sqlplus output matches the release banner (Release 23.x.x) and connection failures (ORA-01017), giving false positives. UseSELECT 424242andgrep -Eq '^[[:space:]]*424242[[:space:]]*$'.