DEV Community

Sandeep Kumar
Sandeep Kumar

Posted on

PitGraph: building an F1 strategist whose memory is a graph

How I built a multi-agent race-strategy assistant on FalkorDB in four days for Graph Hacks, and what the graph made easy (and hard).


Ask a Formula 1 strategist whether a one-stop is viable at Monza if there's an early safety car, and they won't give you a number pulled from thin air. They'll think about what won there before, what the tyres did, what happened the last time the safety car came out, and which other tracks behave like Monza. Then they'll remember the answer for next time.

That's the whole idea behind PitGraph: a team of AI agents that answers F1 strategy questions by reasoning over a graph of real race data, shows the exact evidence behind every answer, and keeps a memory that grows with each conversation. I built it for Graph Hacks by FalkorDB and entered it in two tracks: Agents That Act on Connected Data and Agent Memory and Coordination.

Why a graph?

Race strategy is relational by nature. A driver drove an entry, which was in a race, held at a circuit. That entry had a stint, which was followed by another stint, each on a tyre compound. A safety car happened during a race, and some drivers pitted under it.

In a table, a tyre strategy is a string you have to parse. In a graph, a strategy is a path:

Entry -HAD_STINT-> Stint -NEXT-> Stint -NEXT-> Stint
                     |             |             |
                ON_COMPOUND   ON_COMPOUND   ON_COMPOUND
                     v             v             v
                  MEDIUM         HARD          HARD
Enter fullscreen mode Exit fullscreen mode

So "what did the top five run at Monza?" becomes a short traversal:

MATCH (c:Circuit {circuit_id: $circuit})<-[:HELD_AT]-(r:Race)<-[:IN_RACE]-(e:Entry)
WHERE e.finish_pos <= $top_n
MATCH (e)-[:HAD_STINT]->(s:Stint)-[:ON_COMPOUND]->(k:Compound)
WITH r, e, s, k ORDER BY s.stint_no
RETURN r.race_id AS race_id, e.entry_id AS entry_id, e.finish_pos AS finish_pos,
       collect(k.name) AS strategy, e.stops AS stops
ORDER BY race_id, finish_pos
Enter fullscreen mode Exit fullscreen mode

I loaded three seasons (2023–2025) of public data from Jolpica-F1 and OpenF1: 70 races, ~1,400 driver entries, ~4,000 tyre stints, ~2,400 pit stops and 90 safety car / VSC / red flag periods. That's small enough to fit comfortably in FalkorDB Cloud's free tier, and rich enough to argue about.

The agents

A question goes through five agents, wired together with LangGraph:

  1. Planner: works out the circuits and the kind of question (strategy choice, safety-car impact, circuit comparison, memory recall), recalls what this user already knows, and picks a sequence of tools.
  2. Retriever: calls those tools against the graph.
  3. Analyst: writes a verdict, a confidence score, and the evidence it relied on.
  4. Critic: approves the answer or sends it back once for revision.
  5. MemoryWriter: saves the answer, the evidence and anything worth remembering.

One rule I set early: the LLM never writes Cypher. The tools are exposed over MCP (Model Context Protocol), ten of them, and the model only chooses which tool to call and with what arguments. Every value goes in as a query parameter. That made the system predictable, safe from query injection, and, importantly for a hackathon, something I could explain line by line. Every query is documented in QUERIES.md.

The analysis also leans on FalkorDB's built-in graph algorithms:

  • Label propagation (community detection) groups circuits that win with the same strategy.
  • PageRank finds which past races carry the most weight as evidence.
  • Shortest path shows how two circuits are connected through similarity links.

When the data runs out

Here's my favourite part. Monza has no safety cars in the 2023–2025 data. So what should the agent say about an early safety car there?

A plain LLM would happily make something up. PitGraph's Retriever notices when a circuit's own data is thin (fewer than three rows) and switches to a recovery path. It follows SIMILAR_TO edges to the circuits most like Monza, and pulls in the strategy cluster Monza belongs to. Each SIMILAR_TO edge stores why two circuits match (basis: "street,low_sc,one_stop"), so the answer can say: "Monza has no safety-car history, so this is based on circuits with similar characteristics." That honesty is something the graph structure gives you almost for free.

Every answer shows its working

Each answer stores the race nodes it cited, and the Analyst can't cite anything that wasn't actually retrieved (made-up IDs are dropped). The Why? panel in the UI draws that evidence as a subgraph, together with the full agent handoff chain, every tool call (including the failed ones), and the Critic's review.

The handoffs themselves are stored in the graph as Task nodes linked by HANDED_OFF_TO. Because the trace reads that chain as a variable-length path, a "Critic sent it back" round needed no special handling. It's just one more hop.

Memory as a graph

This was the part that made FalkorDB feel like the right tool rather than just a capable one. One FalkorDB instance holds several graphs:

  • f1: the race data, read-only at runtime
  • mem_<user>: one isolated memory graph per user
  • mem_global: shared "what works" knowledge, with no user data

Inside each user's graph live three kinds of memory:

  • Episodic: sessions, messages, questions and answers. Follow-up questions use the conversation's NEXT chain, so "And what if there's an early safety car there?" becomes a full standalone question about Monza.
  • Semantic: insights about circuits. When a new answer agrees with an old insight, it confirms it (raising its count and confidence). When it disagrees, it contradicts it, and both are kept and the conflict is shown. When it replaces it, it supersedes it: the old insight leaves recall but stays as history.
  • Procedural: which tool sequences tend to get approved for each type of question, ranked by a smoothed success rate. This is shared across users, so a brand-new user's very first question already benefits from plans that worked for others, without seeing anyone else's memories.

Keeping users in separate graphs means isolation is structural, not a WHERE user_id = ... I might forget. The cost is that Cypher can't traverse between graphs, so memory points into the race data through small stub nodes (Evidence, EntityRef) holding a label and an ID, which the trace resolves with a second query. That turned out to be a fair trade.

Things that surprised me

  • FalkorDB's algorithms run on stored edges only. I couldn't cluster circuits on a computed relationship; I had to write FAVOURS edges (circuit → its dominant strategy) into the graph first. My first attempt linked every strategy a circuit had ever seen, and label propagation put every circuit in one giant cluster. Linking only the dominant dry-race strategy fixed it.
  • Cypher's shortestPath() is directed-only in FalkorDB, so it couldn't follow similarity links both ways. algo.SPpaths with relDirection: 'both' did the job.
  • Raw PageRank scores are tiny when they're spread over a whole graph, so I rescaled them (1.0 = most central circuit) to make them readable for both the model and humans.
  • Measuring "success" is harder than it looks. I first counted a tool plan as successful only if the Critic approved the answer on the first try. But the Critic asks for one revision on most answers, so every plan scored zero. "Approved after at most one revision" turned out to be the useful signal.
  • Containers are honest. Everything worked locally, then every tool call failed in Docker. The MCP SDK starts the tool-server subprocess with a minimal environment, and without a .env file on disk the database credentials never arrived. The fix was to forward them explicitly.
  • Free tiers shape design. Oracle Cloud had no free capacity, so the app runs on Render's free tier (512 MB RAM). I simulated those limits locally before deploying, and capped concurrent questions to stay safely within memory.

How it was built

I used Claude Code as a coding assistant throughout, and every AI-assisted step is disclosed in AI_USAGE.md. The design decisions, such as the graph model, which algorithms to use where, the hosting choice, and what counted as "done" each day, were mine. I kept the code simple enough to explain every query in it.

The stack: Python 3.11, FalkorDB Cloud, an MCP server for the graph tools, LangGraph for the agent flow, Claude as the model, FastAPI, and a single-page UI with vis-network for the evidence graph. There are 38 integration tests, run against the live database.

What's next

  • Real authentication. Right now the client chooses its user ID, which is fine for a demo but not beyond it.
  • Lap-level telemetry, so the agents can reason about tyre degradation, not just the sequence of compounds.
  • More seasons and sessions, such as qualifying and sprints.

Try it

Open the live demo and ask:

  • Best strategy at Monza for a top-5 finish?
  • Is a one-stop viable at Monza if there's an early safety car? (watch the recovery path kick in)
  • Then open a new session and ask: What do you know about Monza?

Click Why? on any answer to see the graph behind it.

The thing I'll take away from this hackathon is that a graph is about more than storage. It's a format an agent can explain itself in. Every answer PitGraph gives is a path you can follow back to the races it came from.

Top comments (0)