Official code repository for the paper "Beyond Single-Signal Retrieval: A Graph-Aware Agentic Framework for Conversational Music Recommendation".
RecSys Challenge 2026 - Team Poliba T1 b | Student members: Di Bitetto Bianca, De Palma Gianmichele, Como Berardino, Russo Roberta, Dicataldo Francesco Maria, Desiderato Francesco Salvatore, Falcone Francesco
Solution by team Poliba T1 b for the RecSys Challenge 2026, built on the TalkPlayData conversational music recommendation dataset. Given a multi-turn dialogue between a user, a music agent, and an assistant, the system must recommend a ranked list of K = 20 tracks at each conversational turn, while generating a grounded natural-language response justifying why each track fits the evolving intent.
The pipeline mirrors a modern retrieve-and-generate recommender, and is now driven end-to-end by a single entry point, run_pipeline.py, at the repository root:
- Graph Embeddings - a heterogeneous GCN jointly encodes users, sessions, and tracks via relation-aware message passing (a parameter-free LightGCN-style convolution on userβsession edges, plus a feedback-aware operator that Hadamard-modulates sessionβtrack edges with a learnable feedback embedding), capturing long-term collaborative preferences. Cold-start users/tracks are handled via graph-based imputation (mean / multi-hop / personalized PageRank / heat-diffusion).
- Retrieval - a multi-agent retrieval system complements the graph signal with a sparse lexical retriever (BM25 over titles/artists/tags), a dense audio retriever (LAION-CLAP), and a dense lyrics retriever (Qwen3), each fed by an LLM query-rewriting agent; a learned WRRF router (an MLP predicting query-dependent weights) fuses the per-modality candidate lists via Weighted Reciprocal Rank Fusion.
- Reflective Reranking - a two-phase agent. LLM reranking: conditioned on the conversation, user profile, and item metadata, an LLM reranks the fused candidates and selects the top-K tracks with an initial rationale for each. Reflection: a feedback-aware self-critique pass over that list - the agent classifies every track as positive/negative, and rejected tracks are replaced by turning the rejection reason into a corrective query and running a secondary similarity search.
- Response Generation - a two-stage LLM pipeline (reasoning-driven content generation + persona-prompted stylistic refinement) produces the final grounded, explained recommendations, merged back into a
predictions.jsonsubmission file.
run_pipeline.py chains all of the above automatically and, for stages 1 and 2, lets you choose between loading pretrained checkpoints (fast, deterministic - "inference mode") or training from scratch ("training mode"). A companion script, download_model_weights.py, fetches the pretrained checkpoints from the Hugging Face Hub for you.
- Team
- Repository structure
- Installation
- API keys
- Running the pipeline
- Configuration files at a glance
- Reference
This project has been developed by:
| Name | Contact |
|---|---|
| Marco Valentini | m.valentini7@phd.poliba.it |
| Bianca Di Bitetto | b.dibitetto@studenti.poliba.it |
| Gianmichele De Palma | g.depalma12@studenti.poliba.it |
| Berardino Como | b.como@studenti.poliba.it |
| Roberta Russo | r.russo14@studenti.poliba.it |
| Francesco Maria Dicataldo | f.dicataldo1@studenti.poliba.it |
| Francesco Salvatore Desiderato | f.desiderato1@studenti.poliba.it |
| Francesco Falcone | f.falcone3@studenti.poliba.it |
| Chiara Mallamaci | c.mallamaci@phd.poliba.it |
| Antonio Ferrara | antonio.ferrara@poliba.it |
| Daniele Malitesta | dmalitesta@luiss.it |
| Tommaso Di Noia | tommaso.dinoia@poliba.it |
| Fedelucio Narducci | fedelucio.narducci@poliba.it |
RecSysChallenge26/
βββ GraphEmbeddings/ # Heterogeneous GCN: user/session/track embeddings + FAISS indexes
β βββ gcn/ # Graph construction, model, training loop
βββ Retrieval/ # Multi-agent retrieval pipeline + learned router
β βββ agents/ # LLM query-rewriting agents (sparse/audio/lyrics)
β βββ tools/ # Retriever implementations (sparse, audio, lyrics, cf, graph)
β βββ router/ # WRRF query router (training, inference, evaluation)
β βββ data/ # Dataset/subset loading, states DB, track DB, vector DB
β βββ config/ # retrieval_config.yml
βββ Reranker/ # Reflective reranking (LLM reranker + reflector) and response generation
β βββ LLM_reranker/ # Candidate reranking via LLM (Gemini/Ollama)
β βββ Reflector/ # Reflection/self-critique utilities
β βββ entry_points/ # conversion_utils.py: parquet/json format conversions
β βββ conversation_llm/ # Conversation description/augmentation
β βββ user_description/ # User profile description generation
β βββ item_query_similarity/ # Item/query embedding similarity search
βββ ResponseGenerator/ # Two-stage response generation: content generation + stylistic refinement
β βββ baseline/ # Response construction, Ollama client, similarity/metrics utils
β βββ LexicalEnricher/ # Bigram/dictionary-based lexical diversity enrichment
β βββ scripts/ # Blind dataset construction helpers
β βββ sys_prompts/ # System prompts (first-turn / follow-up / rewriter)
βββ artifacts/ # Intermediate JSON artifacts produced during a run
β βββ retrieval_candidates.json
β βββ submission_ready.json
β βββ generation_input.json
βββ cache/ # Generated artifacts: indexes, models, predictions, states
βββ download_model_weights.py # Downloads pretrained GCN + router checkpoints from HF Hub
βββ run_pipeline.py # Single entry point: runs the full pipeline end-to-end
βββ keys.json # LLM API key (JSON or plain-text), shared by every LLM stage - see API keys
βββ reranker_output.json # Generated by run_pipeline.py: LLM reranker output
βββ predictions.json # Default final output of run_pipeline.py
βββ environment.yml # Conda environment definition
βββ .gitignore # Git ignore rules
βββ README.md # This file
The project targets conda exclusively (no plain pip/venv workflow is supported).
Prerequisites
- Miniconda or Anaconda
- An NVIDIA GPU + CUDA drivers is strongly recommended (GCN training,
faiss-gpu,torchare built against CUDA 12.1) huggingface_hub(used bydownload_model_weights.py) - included inenvironment.yml- model weights can be downloaded here
Setup
git clone https://github.com/sisinflab/GraphAgenticCMR_PolibaT1.git
cd RecSysChallenge26
# Create the environment from the provided environment.yml
conda env create -f environment.yml
# Activate it
conda activate GACMR_T1All commands below (run_pipeline.py, download_model_weights.py) are run from the repository root.
All LLM-backed stages - the retrieval query-rewriting agents, the reranker, the reflector, and the response generator - call an LLM (Gemini or a local Ollama server) and share a single credential file, passed everywhere via run_pipeline.py --key-path:
| File | Used by | Notes |
|---|---|---|
keys.json (repo root, default), or any plain-text file such as keys.txt passed via --key-path |
Every LLM-driven stage: the Retrieval query-rewriting agents (stage 2), the Reflective Reranking agent - reranker + reflector - (stage 3), and the Response Generator (stage 4) | JSON (checked first - looks for an api_key/key/token/OPENAI_API_KEY field, in that order) or plain text containing only the key. Not committed to the repo - create it yourself before running. |
If you prefer a local LLM for retrieval, set provider: "ollama" (and api_url) in Retrieval/config/retrieval_config.yml instead of providing a Gemini key; the reranker/reflector/response-generator configs likewise expose an OLLAMA_CONFIG field to point at a local Ollama model.
run_pipeline.py runs everything end-to-end: graph embeddings β retrieval + router fusion β export candidates β reflective reranking (LLM reranking + reflection) β response generation β predictions.json. Choose one of the two modes below depending on whether you want to reuse pretrained checkpoints or train from scratch.
Use this mode to reproduce results quickly using the team's pretrained checkpoints, without training anything.
# 1. Download the pretrained GCN checkpoint and the two router checkpoints
python download_model_weights.py
# 2. Run the full pipeline against the blind evaluation dataset
python run_pipeline.py--graph-weights pretrained and --retriever-weights pretrained are the defaults, so the second command above is equivalent to:
python run_pipeline.py --graph-weights pretrained --retriever-weights pretrainedWhat download_model_weights.py does: it pulls three files from the giammidp/RecSysChallenge26_models Hugging Face repo and places them where run_pipeline.py expects them:
| Remote file | Local destination |
|---|---|
music_hetero_gcn_mean_heat.pt |
GraphEmbeddings/<MODEL_DIR>/heat/ (GCN checkpoint) |
query_router_30.pt |
cache/router/models/ |
query_router_500.pt |
cache/router/models/ |
--aggr mean --imputation heat (the pipeline defaults). If you run run_pipeline.py with a different --aggr/--imputation, --graph-weights pretrained will fail with a FileNotFoundError since no matching pretrained checkpoint was downloaded - either keep the defaults or switch to --graph-weights scratch for that combination.
If either checkpoint is missing when you request pretrained weights, run_pipeline.py fails fast with a clear error telling you to run download_model_weights.py (or fall back to scratch).
Use this mode to train both the GCN and the retrieval router yourself - no call to download_model_weights.py is required.
python run_pipeline.py --graph-weights scratch --retriever-weights scratchWith --graph-weights scratch, run_pipeline.py:
- deletes any cached checkpoint at
cache/models/<imputation>/music_hetero_gcn_<aggr>_<imputation>.pt(if present), then - runs
python -m gcn.maininsideGraphEmbeddings/with your--aggr/--imputation/--graph-*options, training a fresh heterogeneous GCN and exporting embeddings, FAISS indexes, and blind-B user embeddings.
With --retriever-weights scratch, it then trains the WRRF query router from scratch - no prerequisite step needed, the training data is computed automatically (if absent)
You can also mix modes - e.g. train the GCN from scratch but reuse the pretrained router, or vice versa:
python run_pipeline.py --graph-weights scratch --retriever-weights pretrained
python run_pipeline.py --graph-weights pretrained --retriever-weights scratch(For the second case above, you'd still run python download_model_weights.py first to obtain the GCN checkpoint, since only the router is being retrained.)
Once graph and retriever weights are ready (pretrained or freshly trained), run_pipeline.py continues identically in either mode:
- Mixed-mode retrieval - runs
Retrieval.main --mode mixedagainst--dataset-name/--split(default:talkpl-ai/TalkPlayData-Challenge-Blind-B, splittest), using the LLM query-rewriting agents (key:--key-path), producing a fusedrouter_500predictions parquet undercache/results/. - Export candidates - converts the top
--top-k-rerankcandidates per query intoartifacts/retrieval_candidates.json. - Reflective reranking - runs the reranker (config:
--reranker-config, key:--key-path) against the conversation and user profile, writingreranker_output.json(repo root); then runs the reflector (config:--reflector-config, key:--key-path) as a feedback-aware self-critique pass over that reranked list against the retrieval parquet. - Response generation - converts the reranker output into submission and generation-ready formats (
--submission-ready,--generation-input), then runs the response generator (config:--response-config, key:--key-path) to produce--response-output. - Final predictions - merges the reranker output and generated responses into the final submission file at
--output(default:predictions.json).
python run_pipeline.py [OPTIONS]Dataset
| Argument | Default | Description |
|---|---|---|
--dataset-name |
talkpl-ai/TalkPlayData-Challenge-Blind-B |
HF dataset repo for the blind evaluation set |
--split |
test |
Dataset split to run inference on |
Credentials (shared across all LLM stages)
| Argument | Default | Description |
|---|---|---|
--key-path |
keys.json (repo root) |
JSON or plain-text file with the LLM API key, passed down to the Retrieval query-rewriting agents (stage 2), the Reflective Reranking agent - reranker + reflector - (stage 3), and the Response Generator (stage 4) alike - see API keys |
Graph embeddings (stage 1)
| Argument | Choices | Default | Description |
|---|---|---|---|
--graph-weights |
pretrained, scratch |
pretrained |
Load the cached GCN checkpoint, or retrain it from scratch. The graph retriever always runs off these embeddings either way. |
--aggr |
mean, sum |
mean |
Sessionβtrack aggregation in the GCN |
--imputation |
mean, neighmean, multihop, ppr, heat |
heat |
Cold-start user CF-embedding imputation strategy (selects model_dir/index_dir suffix) |
--graph-top-n |
int | 10 |
Top-N neighbors used when building graph-based imputation |
--graph-hops |
int | 3 |
Number of hops for multi-hop/PPR/heat imputation |
--graph-alpha |
float | 0.1 |
Alpha for personalized PageRank (PPR) imputation |
--graph-b |
float | 5.0 |
Diffusion parameter for heat-kernel imputation |
--graph-interactions |
all, positive |
all |
Which sessionβtrack edges to use when propagating |
Retrieval + router (stage 2)
| Argument | Choices | Default | Description |
|---|---|---|---|
--retriever-weights |
pretrained, scratch |
pretrained |
Load the cached WRRF router checkpoints, or retrain them from scratch before running mixed-mode retrieval |
--top-k-retrieval |
int | 500 |
Candidates kept per query at the retrieval/router stage |
--top-k-rerank |
int | 30 |
Candidates exported to retrieval_candidates.json for the LLM reranker |
Reranker + reflector (stage 3)
| Argument | Default | Description |
|---|---|---|
--reranker-config |
Reranker/LLM_reranker/config.json |
Reranker config (paths, model, temperature); writes reranker_output.json (repo root) |
--reflector-config |
Reranker/Reflector/config.json |
Reflector config; runs the feedback-aware self-critique pass over the reranker output |
Response generation (stage 4)
| Argument | Default | Description |
|---|---|---|
--response-config |
ResponseGenerator/config_inf.json |
Response-generation config (paths, prompts, checkpointing) |
--submission-ready |
artifacts/submission_ready.json |
Intermediate file: reranker output converted to submission format |
--generation-input |
artifacts/generation_input.json |
Intermediate file produced by submission_to_generation, fed to the response generator |
--response-output |
artifacts/responses.json |
File the response generator is expected to write to |
Final output
| Argument | Default | Description |
|---|---|---|
--output |
predictions.json (repo root) |
Path the final predictions are written to - the reranker output merged with the generated responses |
| Flag | Effect |
|---|---|
--skip-graph-pipeline |
Skip re-running GCN/main.py; assumes the graph, FAISS index, and blind-B embeddings are already up to date on disk |
--skip-rerank |
Stop right after exporting retrieval candidates, and write them directly to --output (bypasses reflective reranking and response generation entirely) |
--skip-response-gen |
Run the reflective reranking stage (reranker + reflector), then write predictions directly from the reranker output (bypasses response generation) |
run_pipeline.py's flags only cover the parameters above; anything specific to a single module (prompt files, LLM provider/model, cache subpaths, router/GCN architecture details, ...) is controlled by that module's own config file below - edit the .yml/.json directly for that level of detail.
| File | Governs |
|---|---|
GraphEmbeddings/config.yaml |
Embedding dimensions, GCN training hyperparameters, FAISS/index paths |
Retrieval/config/retrieval_config.yml |
Dataset/split/subset, retriever top-k, router retriever order, LLM query-rewriting provider, cache paths |
Reranker/LLM_reranker/config.json |
Reranking inputs, LLM model/provider, prompt paths |
Reranker/Reflector/config.json |
Reflection/self-critique inputs and prompts |
ResponseGenerator/config_inf.json |
Response-generation inputs, prompts, checkpointing |
If you use this repository, please cite:
@inproceedings{TBD,
title = {TBD},
author = {TBD},
booktitle = {RecSys Challenge 2026},
year = {2026}
}