SmartStore AI — Phase 3 Implementation Guide
Core RAG Query Pipeline
The actual /ask endpoint — the feature SmartStore AI exists to provide.
What got built
backend/app/rag.py — build_user_message() + answer_question(), grounded prompting
backend/app/api/ask.py — the POST /ask endpoint
backend/tests/test_rag.py
Key design decisions
build_user_message is a pure function, separated from answer_question. Prompt construction (does the context actually get included? does an empty retrieval result still produce a well-formed prompt?) is testable on its own, with zero API calls — this is exactly the kind of thing worth unit-testing directly rather than only testing end-to-end.
The system prompt explicitly handles the empty-retrieval case. build_user_message([], question) produces "(No matching products found.)" inside the context block — not an empty string. An empty context block risks the model filling the silence with a guess; an explicit "nothing found" statement, combined with the system prompt's "say plainly you don't have that information," is what actually makes the grounding instruction effective (Volume 2, Ch.10 of the bootcamp).
client and retrieve_fn are injectable, same pattern as Phase 2. This is what let the test suite verify the real grounding behavior — confirming the retrieved product text actually lands inside the prompt sent to Claude — without a live API key.
Verified test results
tests/test_rag.py::test_build_user_message_includes_context_and_question PASSED
tests/test_rag.py::test_build_user_message_handles_no_results_gracefully PASSED
tests/test_rag.py::test_answer_question_grounds_in_retrieved_context PASSED
tests/test_rag.py::test_ask_endpoint_returns_well_formed_response PASSED
The third test is the one that matters most: it doesn't just check that an answer comes back — it inspects the exact message sent to the fake client and confirms the retrieved product text was actually present. That's the difference between "the function ran" and "the function did the right thing."
Running this for real
export ANTHROPIC_API_KEY="sk-ant-..."
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"question": "where is the olive oil?", "store_id": "<a real store UUID from Phase 1 seed data>"}'
This requires a real Anthropic key and python -m app.ingest (Phase 2) having actually run against real Qdrant data first — neither was live-tested here for the same reason as Phase 2's embedding call.
What's next
Phase 4 — SwiftUI MVP Chat UI replaces Phase 0's placeholder ContentView with a real chat interface calling this /ask endpoint.