DEV Community

Sh Raj
Sh Raj

Posted on

CampusLoop Atlas: I Built an Agent That Knows What Is Actually Happening on Campus

Sanity Challenge Path One Submission

This is a submission for the Sanity Challenge, Path One: Ship an Agent That Queries Real Content.

CampusLoop Atlas: I Built an Agent That Knows What Is Actually Happening on Campus

Ask the campus. Get an answer grounded in real, structured campus content.

Most "AI campus assistants" are really just chat UIs sitting on top of a search box.

I wanted to build something harder.

CampusLoop Atlas is an agent that answers questions about clubs, events, people, schedules, eligibility, locations, and opportunities by querying a structured Sanity content system through Sanity Context.

The important part is not that it can answer:

"What clubs exist?"

A keyword search can do that.

The interesting questions are things like:

"I'm a second-year CSE student, I like web development, I can only attend events after 5 PM on Friday, and I want something where beginners are welcome. What should I join, and why?"

That answer requires the agent to connect multiple pieces of structured content.

It has to reason over:

Student profile
     │
     ├──────────────┐
     │              │
     ▼              ▼
interests       availability
     │              │
     └──────┬───────┘
            ▼
          clubs
            │
      ┌─────┼─────┐
      ▼     ▼     ▼
    events people eligibility
      │
      ▼
   locations
Enter fullscreen mode Exit fullscreen mode

That's the part I wanted Sanity to power.


What I Built

CampusLoop Atlas turns a structured campus content graph into a conversational agent.

The content model is intentionally richer than a collection of blog posts.

A typical record looks conceptually like:

Club
├── name
├── slug
├── description
├── categories[]
├── skills[]
├── audience[]
├── beginnerFriendly
├── members[]
├── events[]
└── socials

Event
├── title
├── startAt
├── endAt
├── venue
├── organizer
├── clubs[]
├── eligibility[]
├── skills[]
├── registrationUrl
└── status

Person
├── name
├── role
├── clubs[]
└── areas[]

Venue
├── name
├── building
└── campusArea
Enter fullscreen mode Exit fullscreen mode

The agent can then answer questions that depend on relationships between documents, not just matching words.

Example

User:

"Which web-development opportunities can a first-year student attend this weekend?"

The agent needs to discover:

  1. events occurring this weekend
  2. their organizers
  3. the organizer's associated clubs
  4. the skills/category attached to those events
  5. eligibility requirements
  6. registration state
  7. exact timing and venue

That's precisely the kind of task where structured content becomes useful.


The Problem With Normal Search

Suppose the dataset contains:

Web Development Club
HackQuest
Frontend Workshop
ACM Meetup
React Bootcamp
Enter fullscreen mode Exit fullscreen mode

A search engine can find these words.

But the real question might be:

"Which beginner-friendly event this Saturday is run by a club that actively teaches frontend development, is on campus, and still has registration open?"

The answer is not a document.

It is a join across documents.

          ┌─────────────┐
          │    Events   │
          └──────┬──────┘
                 │
                 │ organizer
                 ▼
          ┌─────────────┐
          │    Clubs    │
          └──────┬──────┘
                 │
        ┌────────┼─────────┐
        ▼        ▼         ▼
     skills   audience   members
        │        │
        └────┬───┘
             ▼
       Candidate events
             │
       ┌─────┴─────┐
       ▼           ▼
   eligibility   schedule
       │           │
       └─────┬─────┘
             ▼
          Answer
Enter fullscreen mode Exit fullscreen mode

Sanity stores the structure. The agent reasons over it.


Why Sanity?

Sanity's Content Lake stores content as structured JSON documents with schemas that I can design around the domain.

For this project, that means I can model the campus as actual entities and relationships rather than forcing everything into flat text.

Sanity Context then exposes that content to an agent through a hosted, read-only MCP server. In GROQ mode, Context provides tools for understanding the schema and querying documents; Knowledge Base mode provides access to pre-built indexed entries. citeturn545668search1turn545668search0

For this challenge I use Knowledge Base + Sanity Context MCP so the agent can retrieve the curated campus knowledge that I selected instead of relying on a generic web search.

The challenge explicitly calls for an agent backed by a Sanity Context MCP endpoint and a Knowledge Base, and says the strongest Path One submissions are ones that only work because the content is structured. citeturn545668view0

That is exactly the constraint I designed around.


How I Used Sanity

1. I modeled the campus as connected content

Instead of:

"ACM organizes HackQuest in the Main Auditorium..."
Enter fullscreen mode Exit fullscreen mode

I store:

{
  "_type": "event",
  "title": "HackQuest",
  "organizer": {
    "_ref": "club-acm"
  },
  "venue": {
    "_ref": "venue-main-auditorium"
  },
  "skills": ["web-development", "ai"],
  "eligibility": ["students"],
  "status": "registration-open"
}
Enter fullscreen mode Exit fullscreen mode

That small difference changes what the agent can infer.

The agent doesn't have to guess whether two pieces of text refer to the same entity.

The relationship is data.


2. I pointed Sanity Context at the content I actually care about

The Knowledge Base contains the campus sources used by the agent:

CampusLoop content
├── Clubs
├── Events
├── People
├── Venues
├── Eligibility rules
├── Categories
└── FAQs / announcements
Enter fullscreen mode Exit fullscreen mode

Knowledge Bases are pre-built indexes over selected material and can combine sources so an agent can retrieve from one indexed knowledge layer. Sanity currently documents Knowledge Bases as a beta feature. citeturn545668search3turn545668search2

For this project, that means the agent sees a curated campus knowledge graph rather than an unstructured pile of pages.


3. I used the Context tools as an agent, not as a hidden search endpoint

Sanity Context's GROQ-mode tools include:

initial_context
schema_explorer
groq_query
array_field_reader
Enter fullscreen mode Exit fullscreen mode

The schema-aware tools matter because the agent first needs to understand what kind of content exists and how it is shaped before it can ask useful questions of the data. citeturn545668search0

In Knowledge Base mode, the agent works with indexed entries instead of directly querying the live dataset. citeturn545668search0turn545668search3

The important workflow is:

User question
      │
      ▼
Agent interprets intent
      │
      ▼
Sanity Context
      │
      ├── discover relevant content
      ├── follow structured relationships
      ├── retrieve source-backed facts
      │
      ▼
Agent synthesizes answer
      │
      ▼
Answer + evidence
Enter fullscreen mode Exit fullscreen mode

A Question That Breaks Keyword Search

Here is the type of question Atlas is designed for:

"I am a first-year student interested in AI and frontend development. I don't want competitive coding events, I can only attend after 6 PM, and I want something that doesn't require prior club membership. What can I attend this week?"

Notice how many constraints are hidden inside one sentence.

The agent needs to combine:

Constraint Structured field
First-year eligibility
AI skills[]
Frontend skills[] / categories[]
Avoid competitive coding categories[]
After 6 PM startAt
No membership required eligibility[]
This week event date

A plain text search engine may retrieve relevant pages.

But it isn't naturally performing this structured filtering and relationship traversal.

That's why the content model is part of the product.


The Agent's Answer

Instead of dumping search results, Atlas is designed to return something closer to:

I found 3 opportunities that match your constraints.

1. Frontend Workshop
   Friday • 6:30 PM
   Web Development Club
   Beginner-friendly
   No club membership required
   Main Lecture Hall

   Why it matches:
   ✓ frontend
   ✓ after 6 PM
   ✓ beginner-friendly
   ✓ open to students

2. AI Build Night
   Saturday • 7:00 PM
   AI Club
   ...

3. Open Source Sprint
   Sunday • 6:30 PM
   Developer Community
   ...

I excluded 4 other events because they:
• started before 6 PM
• required prior membership
• were categorized as competitive programming
Enter fullscreen mode Exit fullscreen mode

The final response is generated by the agent.

But the facts come from structured content.


🔍 The "Why?" Button

One feature I care about a lot is showing why the agent reached its conclusion.

For every recommendation, the UI can expose:

Matched because:

✓ skill = frontend
✓ audience = first-year
✓ beginnerFriendly = true
✓ startAt > 18:00
✓ registrationStatus = open

Source:
→ Frontend Workshop
→ Web Development Club
→ Main Lecture Hall
Enter fullscreen mode Exit fullscreen mode

This makes the result inspectable.

Sanity Context is read-only and the connection is scoped by its configured sources and filters, which is useful for keeping the agent inside the content boundary I explicitly gave it. citeturn545668search10


🧠 Handling Conflicting Information

Another reason I chose Sanity Context instead of simply scraping pages at runtime is provenance.

The challenge specifically highlights that Knowledge Base entries remain linked to their source and that contradictory source claims can be surfaced together. citeturn545668view0

That matters on a campus.

Imagine:

Club page:
"Workshop starts at 5 PM"

Official event page:
"Workshop starts at 6 PM"
Enter fullscreen mode Exit fullscreen mode

A generic chatbot might confidently choose one.

Atlas can treat those as conflicting source-backed claims and surface the discrepancy for resolution instead of silently inventing certainty.

That is a much safer interaction model for real-world content.


🛠️ Architecture

The production architecture is intentionally small:

                   ┌─────────────────────┐
                   │   CampusLoop Atlas  │
                   │      Web App        │
                   └──────────┬──────────┘
                              │
                              ▼
                   ┌─────────────────────┐
                   │      AI Agent       │
                   │                     │
                   │ intent + planning   │
                   │ answer synthesis    │
                   └──────────┬──────────┘
                              │
                              │ MCP
                              ▼
                   ┌─────────────────────┐
                   │   Sanity Context    │
                   │                     │
                   │ Knowledge Base      │
                   └──────────┬──────────┘
                              │
                       indexed content
                              │
                              ▼
                   ┌─────────────────────┐
                   │   Sanity Content    │
                   │       Lake          │
                   └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The frontend is only the interface.

The interesting system is:

structured content
       +
retrieval
       +
agent reasoning
       =
grounded campus answers
Enter fullscreen mode Exit fullscreen mode

💻 The Data Model

The schema is deliberately relationship-heavy.

A simplified version:

defineType({
  name: "event",
  type: "document",
  fields: [
    defineField({
      name: "title",
      type: "string",
      validation: Rule => Rule.required()
    }),

    defineField({
      name: "startAt",
      type: "datetime",
      validation: Rule => Rule.required()
    }),

    defineField({
      name: "organizer",
      type: "reference",
      to: [{ type: "club" }]
    }),

    defineField({
      name: "venue",
      type: "reference",
      to: [{ type: "venue" }]
    }),

    defineField({
      name: "skills",
      type: "array",
      of: [{ type: "string" }]
    }),

    defineField({
      name: "eligibility",
      type: "array",
      of: [{ type: "string" }]
    }),

    defineField({
      name: "beginnerFriendly",
      type: "boolean"
    })
  ]
})
Enter fullscreen mode Exit fullscreen mode

The exact production schema is available in the repository linked below.


🔗 Why References Matter

Consider these two representations.

Flat content

HackQuest is hosted by ACM in Main Auditorium.
ACM is a technical club.
Main Auditorium is on Central Campus.
Enter fullscreen mode Exit fullscreen mode

Structured content

HackQuest
 ├── organizer → ACM
 └── venue → Main Auditorium

ACM
 └── category → Technical

Main Auditorium
 └── campusArea → Central Campus
Enter fullscreen mode Exit fullscreen mode

Now a question like:

"Which technical events are happening in Central Campus?"

can be answered from relationships rather than semantic coincidence.

That is the difference between searching text and querying a content model.


🤖 Agent Loop

The agent roughly follows this pattern:

1. Understand the question
2. Identify constraints
3. Retrieve relevant entities
4. Traverse relationships
5. Filter candidates
6. Resolve conflicts
7. Generate an answer
8. Cite the supporting content
Enter fullscreen mode Exit fullscreen mode

Pseudo-code:

def answer(question):
    intent = parse_question(question)

    candidates = sanity_context.retrieve(
        entities=intent.entities,
        constraints=intent.constraints
    )

    filtered = apply_constraints(
        candidates,
        intent.constraints
    )

    conflicts = detect_conflicts(filtered)

    return synthesize(
        question=question,
        results=filtered,
        conflicts=conflicts,
        sources=get_sources(filtered)
    )
Enter fullscreen mode Exit fullscreen mode

The key point is that Sanity is the source of truth for the campus knowledge used by the agent.


🚦 Guardrails

I don't want a campus assistant that confidently fabricates event information.

So Atlas follows a few rules:

NO SOURCE
   ↓
Do not present the claim as campus fact.

CONFLICTING SOURCES
   ↓
Surface the conflict.

MISSING CONSTRAINT
   ↓
Ask a follow-up question.

STALE / PAST EVENT
   ↓
Label it clearly instead of presenting it as upcoming.

NO MATCH
   ↓
Say no matching opportunity was found.
Enter fullscreen mode Exit fullscreen mode

This is especially important because Sanity Context itself is a read-only content interface; it exposes the content configured for the agent rather than letting the agent silently mutate the dataset. citeturn545668search1turn545668search10


🧪 How I Tested It

I created test questions in increasing levels of difficulty.

Level 1 — Direct lookup

"What is the next HackQuest event?"
Enter fullscreen mode Exit fullscreen mode

Level 2 — Filtering

"Which events are happening after 6 PM?"
Enter fullscreen mode Exit fullscreen mode

Level 3 — Multi-constraint

"Which AI events after 6 PM are beginner-friendly?"
Enter fullscreen mode Exit fullscreen mode

Level 4 — Relationship reasoning

"Which beginner-friendly events are hosted by clubs
that teach web development?"
Enter fullscreen mode Exit fullscreen mode

Level 5 — Real-world ambiguity

"Which event should I attend if I'm a first-year student,
want to learn frontend development, cannot attend before 6 PM,
and don't want competitive programming?"
Enter fullscreen mode Exit fullscreen mode

Level 6 — Conflict handling

"Why does one source say 5 PM and another say 6 PM?"
Enter fullscreen mode Exit fullscreen mode

The higher-level tests are the important ones.

A benchmark that only measures Level 1 is basically measuring search.


📈 What I Measure

I evaluate more than whether an answer "sounds right."

Metric What I check
Grounding Are important claims supported by Sanity content?
Constraint accuracy Did the answer respect every user constraint?
Relationship accuracy Did it follow the right references?
Freshness Did it distinguish upcoming from past content?
Conflict awareness Did it surface contradictions?
Abstention Did it avoid inventing an answer when data was missing?
Source coverage Can I trace recommendations back to content?

A useful agent should be correct because it found the right content, not merely because the language model produced a plausible sentence.


🔥 The coolest use case

The feature I ultimately want CampusLoop Atlas to become is a personal campus navigator.

Imagine opening the app at the beginning of a semester and asking:

"I have 6 hours free this week. I want to meet developers, learn React, attend one AI event, avoid anything before 5 PM, and I'd prefer events that are free."

Atlas could construct a schedule from structured campus content.

That is not a search result.

It is a plan built from a content graph.

And that is exactly why Sanity is useful here.


🎬 Demo

Live demo: [ADD YOUR DEPLOYED AGENT URL]

Recommended demo flow:

1. Ask a simple question.

2. Ask a multi-constraint question.

3. Open the "Why this answer?" evidence.

4. Ask a question whose answer depends on two or more linked documents.

5. Demonstrate a conflict or missing-data case.

6. Show the underlying Sanity content that produced the answer.

The sixth step is important.

I don't just want to show that the agent talks.

I want to show where the answer came from.


💻 Code

Repository: [ADD YOUR PUBLIC GITHUB REPOSITORY]

The repository contains:

/apps
  /web
  /agent

/sanity
  /schemas
  /seed

/lib
  /context
  /retrieval
  /evaluation

/README.md
Enter fullscreen mode Exit fullscreen mode

The README includes setup instructions, environment variables, the Sanity schema, and the agent integration.


🧩 How I Used Sanity Context

The key integration is the Sanity Context MCP endpoint.

The agent is configured to access the Sanity knowledge surface rather than being given a giant prompt containing campus data.

Conceptually:

Agent
  │
  │ MCP
  ▼
Sanity Context
  │
  ├── initial context
  ├── schema / content understanding
  ├── retrieval
  └── source-linked knowledge
       │
       ▼
   Sanity Knowledge Base
Enter fullscreen mode Exit fullscreen mode

Sanity documents the Context MCP as a hosted, structured, read-only interface for agents. GROQ mode exposes schema and query tools, while Knowledge Base mode exposes indexed entries. citeturn545668search0turn545668search1

For the challenge, I use the Knowledge Base path because I want the agent to work from a curated campus corpus.


🗂️ Sanity Project Details

Sanity Project ID: [ADD YOUR SANITY PROJECT ID]

Public dataset / project link: [ADD PUBLIC DATASET URL, IF USED]

The Sanity workspace contains the structured content models used by CampusLoop Atlas:

club
event
person
venue
category
announcement
Enter fullscreen mode Exit fullscreen mode

The important part of the model is not the number of document types.

It is the relationships between them.


🧾 Agent Session

Agent Session: [ADD PUBLIC DEV AGENT SESSION URL]

The session should show:

prompt
  ↓
agent planning
  ↓
Sanity Context calls
  ↓
retrieved content
  ↓
reasoning
  ↓
final grounded answer
Enter fullscreen mode Exit fullscreen mode

Agent Sessions on DEV are unlisted by default, so the session must be made public for judges to access it. The challenge also recommends curating the useful portions and checking the transcript for secrets before publishing. citeturn545668view0


🧠 What I Learned

The biggest lesson was surprisingly simple:

Retrieval quality is partly a schema-design problem.

I started from the agent.

I ended up thinking much more about the data.

Questions such as:

"What events should I attend?"
Enter fullscreen mode Exit fullscreen mode

became schema questions:

What is an event?
Who organizes it?
Who can attend?
What skills does it teach?
Where is it?
When does it happen?
What club does it belong to?
What else is related to it?
Enter fullscreen mode Exit fullscreen mode

Once those relationships existed as structured data, the agent had something much stronger than a pile of documents.

It had a model of the campus.


⚔️ Why this is more than RAG

A lot of AI applications can be described as:

chunk text
  ↓
embed
  ↓
vector search
  ↓
LLM
Enter fullscreen mode Exit fullscreen mode

That pattern is powerful.

But it doesn't automatically understand:

event → organizer → club → skill
event → venue → campus area
event → eligibility → student year
event → time → availability
Enter fullscreen mode Exit fullscreen mode

CampusLoop Atlas is intentionally built around structured relationships plus retrieval.

The content model is part of the agent's intelligence.


🌐 What I Want to Build Next

The next version could add:

calendar-aware planning
        +
personal preferences
        +
real-time event state
        +
notifications
        +
agent-generated weekly plans
Enter fullscreen mode Exit fullscreen mode

For example:

"Build me a campus week."

The response could become:

MON
6:00 PM — React Workshop

WED
7:00 PM — AI Build Night

SAT
5:30 PM — Open Source Meetup
Enter fullscreen mode Exit fullscreen mode

Each recommendation would remain connected to its underlying Sanity content.


🏁 Final Takeaway

I didn't build CampusLoop Atlas because campus information needs another chatbot.

I built it because campus information is structured.

Clubs have members.

Events have organizers.

Events have venues.

Venues belong to areas.

Events have dates.

Events have eligibility.

Clubs have skills.

And students have preferences.

Those relationships are exactly what makes the problem interesting.

Sanity gives me a place to model those relationships.

Sanity Context gives the agent a controlled way to access them.

The agent turns those facts into an answer.

STRUCTURED CONTENT
        ↓
   SANITY CONTEXT
        ↓
   AGENT RETRIEVAL
        ↓
RELATIONSHIP REASONING
        ↓
  GROUNDED ANSWER
Enter fullscreen mode Exit fullscreen mode

The goal isn't to make an agent that knows everything.

It's to make an agent that knows where its answers came from.


🔗 Links

📚 References

sanitychallenge #agents #ai #webdev

Top comments (0)