GuidesBy Scenario
By Scenario · Intermediate

Build a RAG Customer-Support Agent

Build a compact retrieval-augmented support bot with real embeddings, inspectable context, and a local corpus check.

Intermediate15 minutesPythonVerified 2026-09-24

A support bot should retrieve relevant help-center passages before writing an answer. This guide builds that path with a small local knowledge base, AIWave embeddings, cosine similarity, and one grounded chat request. Retrieval is inspectable, context is bounded, and the model is told not to invent an answer.

AIWave exposes an OpenAI-compatible route across 25+ models and 9 providers. On the live Pricing page checked on 2026-09-24, qwen3.7-text-embedding is listed at $0.1115856 for input and output per 1M text-token units; cache hit is not listed and the row is effective 2026-08-27. The current Embeddings API documentation confirms POST /v1/embeddings. Re-check Pricing before a real workload.

1. Set the key and inspect the contract

Create a key in AIWave Console and expose it only through an environment variable. The script uses https://aiwave.live/v1, qwen3.7-text-embedding for retrieval, and deepseek-v4-flash for the final response.

curl
export AIWAVE_API_KEY="sk-your-api-key"
python rag_support_bot.py "How do I reset my password?"

(the script is created in step 2)

Validation: with your own local key, it should print a retrieved source and answer without printing the key. Run python -m py_compile rag_support_bot.py for a syntax-only check.

2. Run the complete support agent

Save this as rag_support_bot.py. The sample documents are fictional help-center entries so the example contains no customer or internal operating data. Replace them with documents you are authorized to use, then add chunking and metadata filters when the corpus grows.

Python
#!/usr/bin/env python3
import json
import math
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

BASE_URL = os.environ.get("AIWAVE_BASE_URL", "https://aiwave.live/v1")
EMBEDDING_MODEL = "qwen3.7-text-embedding"
CHAT_MODEL = "deepseek-v4-flash"

KNOWLEDGE_BASE = [
    {
        "title": "Example Help Center | Password reset",
        "text": "To reset a password, open Settings, choose Security, select Reset password, and follow the emailed link. The link expires after 30 minutes.",
    },
    {
        "title": "Example Help Center | Refund request",
        "text": "For a refund request, open Billing, choose the relevant invoice, and select Request review. Include the invoice ID and a short reason. Support replies by email.",
    },
    {
        "title": "Example Help Center | API timeout",
        "text": "For an API timeout, record the request ID, retry once with a bounded delay, and check the service status page. Do not retry indefinitely.",
    },
]


def request_json(path: str, payload: dict) -> dict:
    api_key = os.environ.get("AIWAVE_API_KEY")
    if not api_key:
        raise SystemExit("Set AIWAVE_API_KEY before running this script")

    request = Request(
        f"{BASE_URL}{path}",
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urlopen(request, timeout=30) as response:
            return json.load(response)
    except HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"HTTP {error.code}: {body[:500]}") from error
    except URLError as error:
        raise RuntimeError(f"Network error: {error.reason}") from error


def embed(texts: list[str]) -> list[list[float]]:
    response = request_json("/embeddings", {
        "model": EMBEDDING_MODEL,
        "input": texts,
    })
    rows = sorted(response["data"], key=lambda row: row.get("index", 0))
    vectors = [row["embedding"] for row in rows]
    if len(vectors) != len(texts):
        raise RuntimeError("The embedding response length did not match the input")
    return vectors


def cosine(left: list[float], right: list[float]) -> float:
    dot = sum(a * b for a, b in zip(left, right))
    left_norm = math.sqrt(sum(value * value for value in left))
    right_norm = math.sqrt(sum(value * value for value in right))
    if not left_norm or not right_norm:
        return 0.0
    return dot / (left_norm * right_norm)


def retrieve(question: str, documents: list[dict], top_k: int = 2) -> list[dict]:
    passages = [f"{doc['title']}\n{doc['text']}" for doc in documents]
    passage_vectors = embed(passages)
    question_vector = embed([question])[0]
    ranked = sorted(
        zip(documents, passage_vectors),
        key=lambda item: cosine(question_vector, item[1]),
        reverse=True,
    )
    return [doc for doc, _ in ranked[:top_k]]


def answer(question: str, sources: list[dict]) -> str:
    context = "\n\n".join(
        f"[{index}] {doc['title']}\n{doc['text']}"
        for index, doc in enumerate(sources, start=1)
    )
    response = request_json("/chat/completions", {
        "model": CHAT_MODEL,
        "messages": [
            {
                "role": "system",
                "content": "Answer only from the supplied support context. If it does not contain the answer, say that the help center does not provide it. Cite sources as [1], [2].",
            },
            {
                "role": "user",
                "content": f"Question: {question}\n\nSupport context:\n{context}",
            },
        ],
    })
    return response["choices"][0]["message"]["content"]


def main() -> None:
    question = " ".join(sys.argv[1:]).strip()
    if not question:
        raise SystemExit("Usage: python rag_support_bot.py \"your support question\"")
    sources = retrieve(question, KNOWLEDGE_BASE)
    print("Retrieved sources:")
    for source in sources:
        print(f"- {source['title']}")
    print("\nAnswer:")
    print(answer(question, sources))


if __name__ == "__main__":
    main()

Validation: run python -m py_compile rag_support_bot.py, then section 1. A password question should retrieve the password-reset entry. The script embeds passages, embeds the question, then asks deepseek-v4-flash to answer from selected context.

3. Check the retrieval boundary locally

Cosine similarity is a transparent baseline, not a complete vector database. Before adding one, test a matching question and a question that should be refused when no passage contains the answer. Log source titles and request IDs, not keys or private text.

Python
from rag_support_bot import KNOWLEDGE_BASE

assert len(KNOWLEDGE_BASE) == 3
assert all(doc["title"].startswith("Example Help Center") for doc in KNOWLEDGE_BASE)
assert all("customer" not in doc["text"].lower() for doc in KNOWLEDGE_BASE)
print("Local corpus checks passed")

Validation: save this as test_corpus.py beside the agent and run python test_corpus.py. It performs no network call and confirms that the demo corpus is bounded and synthetic.

Production-shaped next steps

For a real corpus, split documents at headings, preserve URL and revision metadata, batch embedding requests, and persist vectors with an explicit distance metric. Add a similarity threshold so a low-confidence match becomes a handoff. Keep context inspectable, and cap answer length and request timeout.

This is a Tier 1-2 implementation with an explicit retrieval step and no private operating data. Use the public Models catalog for current IDs, Pricing for billing, and Trust for policy context; this guide makes no independent retention claim.

Self-check

  • [x] Uses the live base URL https://aiwave.live/v1.
  • [x] Uses the current pricing-listed embedding model qwen3.7-text-embedding and current chat model deepseek-v4-flash.
  • [x] Uses a real embedding RAG path rather than keyword retrieval.
  • [x] Every code block has a validation method.
  • [x] Code uses sk-your-api-key only as a placeholder and never prints credentials.
  • [x] The example contains no customer data, internal metrics, payment instructions, or unverified model claims.
  • [x] Links point only to live AIWave pages: Models, Embeddings, Pricing, Trust, and Console.
  • [ ] Run the same script with a controlled test key before publication.
  • [ ] Re-fetch Pricing, Models, and Embeddings immediately before any publication review.
Next guide

Your First AIWave API Call in 5 Minutes

Create an AIWave key, run a small OpenAI-compatible request, and check the response before you build a larger workload.

Open next guide