name: soccer-agent-toolbelt description: Soccer analytics agent toolbelt. Gives Claude Code direct access to the Oracle-backed match data, ML predictions, and three-tier memory. Use when answering questions about football matches, building on the soccer agent, or exploring the World Cup dataset.
Soccer Agent Toolbelt
You have 13 tools available. Invoke each via the dispatcher with JSON args.
Hybrid-first rule for coding agents
When you are building, debugging, or demoing the agent, treat hybrid_retrieve as the default retrieval path for explanatory grounding. It is the workshop showcase: model prediction documents plus football facts stored in Oracle through langchain-oracledb OracleVS, retrieved with native hybrid search when possible or Oracle Text + vector reciprocal-rank fusion on Oracle AI Database Free.
Use vector_search deliberately as the semantic-only baseline over semantic_memory when you want to contrast "plain vector similarity" with hybrid retrieval. Do not replace hybrid_retrieve with vector_search in the agent loop, UI demos, or final Grok chat unless the LangChain OracleVS store is unavailable.
LangGraph OracleDB observability rule
Every live /chat turn should leave an ordered step trace in Oracle through langgraph-oracledb OracleStore, under namespace ("soccer-agent", "agent-steps", session_id). Use this for observability/debugging: it records turn_start, grounding_retrieved, each model_response, each tool_call, each tool_result, and final_response or tool_budget_exhausted.
Inspect it via the API after a chat turn:
curl http://localhost:8000/observability/<session_id> | uv run python -m json.tool
Or from Python:
uv run python - <<'PY'
from soccer_agent.observability.langgraph_steps import list_steps
for step in list_steps("<session_id>"):
print(step.value["step_index"], step.value["event_type"], step.value.get("tool_name"))
PY
If this returns no rows after a real chat turn, run uv run python scripts/init_memory.py and uv run python scripts/verify.py; the verifier must report langgraph-oracledb OracleStore observability setup.
Tools
sql_query
Run a read-only SELECT against the soccer schema.
Allowed tables: MATCH_RESULTS, GOALSCORERS, SHOOTOUTS, WC2026_VENUES, PREDICCIONES_FINAL, SOCCER_LANGCHAIN_DOCS, VW_COMPETITIVE_MATCHES, VW_TEAM_STATISTICS, AGENT_SESSIONS, WORKING_MEMORY, EPISODIC_MEMORY, SEMANTIC_MEMORY.
Args: {"sql": "<SELECT statement>"}
vector_search
Semantic-only similarity over distilled facts in semantic_memory. The embedding is computed in-database via VECTOR_EMBEDDING(ALL_MINILM_L6_V2 USING :t AS DATA) — no external embedding API call is made. Use this to show the baseline that only ranks embedded fact summaries; it does not search cached ML prediction documents.
Args: {"query": "<text>", "limit": 5, "fact_type": "team_decade"}
hybrid_retrieve
Default evidence retrieval path. Hybrid retrieval over the LangChain OracleVS table SOCCER_LANGCHAIN_DOCS, populated by langchain-oracledb after ML inference. It combines ML prediction documents and football facts with native HYBRID VECTOR INDEX when available, or Oracle Text + vector reciprocal-rank fusion on Oracle AI Database Free. Prefer this before vector_search for any explanation that Grok or a coding agent will use.
Args: {"query": "Spain Brazil World Cup prediction", "limit": 5, "search_mode": "hybrid"}
predict_match
On-demand 92-feature XGBoost inference for current or hypothetical matches.
Args: {"home_team": "Spain", "away_team": "Brazil", "neutral": true}
get_elo
FootballElo rating for one team, including tournament-tier ratings.
Args: {"team": "Spain"}
get_team_form
Rolling and weighted recent form plus goal averages.
Args: {"team": "Spain", "n": 10}
get_h2h
Head-to-head record from team_a's perspective.
Args: {"team_a": "Spain", "team_b": "Brazil"}
get_momentum
Streaks, unbeaten run, clean-sheet rate, comeback rate, draw tendency, and blowouts.
Args: {"team": "Spain", "n": 15}
get_poisson_xg
Poisson expected-goals lambdas and outcome probabilities.
Args: {"home_team": "Spain", "away_team": "Brazil", "n": 20}
get_tournament_context
World Cup, continental, qualifying, friendly, and big-game context for one team.
Args: {"team": "Spain"}
lookup_prediction
Precomputed prediction from PREDICCIONES_FINAL.
Args: {"home_team": "Spain", "away_team": "Brazil"}
remember
Write a fact to semantic memory.
Args: {"fact_type": "...", "subject_key": "...", "summary": "...", "source": {}}
recall
Recent N turns of episodic memory for the current session.
Args: {"limit": 8}
How to invoke
From the repo root with the uv env active:
uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py <tool> '<json args>' [--session SID]
Examples:
uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py sql_query \
'{"sql":"SELECT home_team, away_team FROM match_results WHERE tournament = '\''FIFA World Cup'\'' AND ROWNUM <= 5"}'
uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py lookup_prediction \
'{"home_team":"Spain","away_team":"Brazil"}'
uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py hybrid_retrieve \
'{"query":"Spain Brazil World Cup prediction evidence","limit":3}'
Contrast it with the semantic-only baseline when teaching the difference:
uv run python .claude/skills/soccer-agent-toolbelt/tools/run_tool.py vector_search \
'{"query":"Spain Brazil World Cup prediction evidence","limit":3}'
Expected contrast: hybrid_retrieve can return doc_type=prediction rows from PREDICCIONES_FINAL with a retrieval_mode such as native_hybrid or fallback_rrf; vector_search returns only distilled facts from semantic_memory. That difference is the Oracle AI Database vector-store showcase.
The dispatcher prints the result as a single JSON line to stdout. Tool surface mirrors soccer_agent/agent/tools.py exactly — same schemas as the deployed FastAPI agent uses.
Adding your own tool
If you want to add a 14th tool (e.g. one that reads team stats, schedules a future match, or writes to a custom table):
- Append a new schema to
TOOL_SCHEMASinsoccer_agent/agent/tools.py. - Add a
if name == "your_tool":branch indispatch(...). Return a JSON-serializable dict; the loop will surface it back to the model. - The
soccer-agent-toolbeltdispatcher (this skill) and the FastAPI agent both pick it up automatically — they share the sameTOOL_SCHEMASlist.
Pitfalls when building on Oracle AI Database
These are the sharp edges we hit shipping the workshop. They aren't theoretical — every one bit a real session.
python-oracledb (3.x)
IS JSONCLOBs auto-decode to Python dict/list.json.loads(value)on a column declaredCLOB CHECK (... IS JSON)raisesTypeError: the JSON object must be str, bytes or bytearray, not dictbecause the driver already parsed it. Guard withisinstance(val, (str, bytes, bytearray))before decoding.- LOB locators die after connection close. If you fetch CLOB rows inside
with get_connection() as conn: ...and then build dataclasses or call.read()AFTER thewithblock exits, you getDPY-1001: not connected to database. Materialize CLOBs INSIDE thewithblock:with get_connection() as conn: cur = conn.cursor() cur.execute(...) rows = [ {c: (v.read() if hasattr(v, "read") else v) for c, v in zip(cols, row)} for row in cur.fetchall() ] return rows - Pass float32 vectors as
array.array('f', list), not numpy ndarrays.oracledbdoes not bind numpy directly toVECTORcolumns. 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").
Vector, hybrid search, and observability
- Hybrid-first default. For agent grounding, UI demos, and final Grok answers, use
hybrid_retrievefirst so the answer can cite cached ML prediction documents and football facts from the LangChain OracleVS vector store. Usevector_searchas a semantic-only comparison or fallback, not as the primary path. - Observe the loop, not just the final answer.
soccer_agent.observability.langgraph_stepsstores every turn/tool step withlanggraph-oracledbOracleStore.GET /observability/{session_id}is the fastest proof that the agent persisted its execution path. - Lower distance is better.
ORDER BY VECTOR_DISTANCE(embedding, :q, COSINE) ASC(ascending is the default; just don't putDESC). - Use
FETCH FIRST :n ROWS ONLY, notLIMIT.LIMITis not valid Oracle SQL. - Bind the query vector once. Don't compute
embed_one(query)twice — call it once and reuse thearray.array. - Refresh
SOCCER_LANGCHAIN_DOCSafter model changes. Runuv run python scripts/load_langchain_vectors.py --resetafterscripts/load_predictions.pyso hybrid retrieval reflects the latestPREDICCIONES_FINALprobabilities. - HYBRID VECTOR INDEX is optional on Oracle AI Database Free.
hybrid_retrievefalls back to Oracle Text + vector reciprocal-rank fusion if nativeOracleHybridSearchRetrieversupport is unavailable.
Schema and grants
CREATE MINING MODELis required forDBMS_VECTOR.LOAD_ONNX_MODEL. Workshop setup grants it; if you re-create the user manually, add the grant.DBMS_VECTORis alreadyEXECUTEtoPUBLICon Oracle AI Database Free; do NOT try to grant it explicitly — SYSTEM lacksGRANT ANY OBJECT PRIVILEGEand you'll hitORA-01031.
In-DB ONNX embedding model
- Model name in SQL is uppercase with underscores (e.g.
ALL_MINILM_L6_V2), NOT the HuggingFace path. Set the model name via theORACLE_EMBED_MODELenv var. - Vanilla HuggingFace ONNX exports don't work. Oracle requires the tokenizer baked into the graph; otherwise
VECTOR_EMBEDDING(...)returnsORA-54426: Tensor "input_ids" contains multiple dimensions (2) of variable size. Use theonnx2oraclePyPI package (notoml4py, which is a 4-file stub on PyPI). - Embedding dim must match the
VECTOR(384, FLOAT32)column type. If you switch toall-mpnet-base-v2(768 dims), update the schema too.
OCI GenAI Inference (Grok 4)
- Bearer-token auth (sk-...) works against
/actions/chatand/actions/embedTextonly. The control plane (https://generativeai...) rejects the bearer key — you can't list models with it. Look in the OCI Console for the model catalog. - Compartment OCID is required in the request body, not just for auth. Always include
servingMode.compartmentId(the workshop'sgrok_client.pydoes this). - Native tool calling (the
toolsarray) returns HTTP 400 on every model tested with this auth path. Use the prompt-protocol pattern: append tool schemas to the system message, instruct the model to emit{"tool": "...", "args": {...}}as a single JSON object when calling a tool, parse it out of the response text. Seesoccer_agent/agent/grok_client.py:_inject_tool_protocoland_parse_tool_calls. role: "tool"messages get rejected withouttoolCallId. Since the bearer-auth endpoint never emits atoolCallId, route tool results back asrole: "system"messages. Skip any persistedrole: "tool"turns when rebuilding the message list for the next iteration — they will break the next API call.