{"slug": "build-a-q-a-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10", "title": "Build a Q&A Bot over Your Company Docs with Pinecone + Claude (Python, SDK v10)", "summary": "A developer published a walkthrough for building a command-line retrieval-augmented generation Q&A bot over internal company documents using Pinecone's Python SDK v10.3.0 and Anthropic's anthropic 2.4.0 with the claude-sonnet-5-5 model. The pipeline chunks documents, embeds and stores them in a Pinecone index via hosted llama-text-embed-v2, retrieves and reranks results with bge-reranker-v2-m3, then prompts Claude to answer only from retrieved chunks with numbered citations, and it maps deprecated v5–v7 SDK calls to their v10 equivalents.", "body_md": "**AI disclosure:** This article was written with the help of AI and reviewed by me. Every API call in it was checked against the SDK source of `pinecone==10.3.0` and `anthropic==2.4.0`, and prices and limits were taken from the official pages linked inline (as of November 2026). Treat the code as a starting point: run it against your own keys before relying on it.\n\nOnce a company has enough internal docs, the same problems show up: new hires can't find the leave policy, nobody remembers which version of the expense rules is current, and wiki search only works if you guess the exact words the author used (\"PTO\" won't find the page that says \"annual leave\").\n\nRetrieval-augmented generation (RAG) is the usual fix: split documents into chunks, embed them into a vector database, retrieve the most relevant chunks for each question, and have an LLM answer **only** from those chunks, with citations.\n\nThis walkthrough builds a command-line Q&A bot with **Pinecone** (managed vector database with hosted embedding and rerank models) and **Claude** (Anthropic's LLM, for answer generation):\n\n**Versions used** (latest on PyPI in November 2026):\n\n| Component | Version | \n|---|---|\n| Python | 3.10+ (Pinecone SDK v10 supports 3.10–3.14) | \n| `pinecone` (Pinecone Python SDK) | 10.3.0, API version `2026-07` | \n| `anthropic` (Anthropic Python SDK) | 2.4.0 | \n| Claude model | `claude-sonnet-5-5` | \n\nThe Pinecone SDK changed a lot in v10. Many tutorials still use the v5–v7 style (`create_index_for_model`, `upsert_records`, `ServerlessSpec`), which may fail or emit deprecation warnings. The pitfalls section maps old calls to new ones.\n\n```\n         ┌──────────── Offline ingest ─────────────────┐\n .md/.txt ──> chunk ──> documents.upsert ──> Pinecone index\n                              │              (llama-text-embed-v2\n                              │               embeds on write)\n         └─────────────────────────────────────────────┘\n\n         ┌──────────── Online ask ─────────────────────┐\n question ──> documents.search (embed scoring, recall 12)\n          ──> inference.rerank (bge-reranker-v2-m3, keep 4)\n          ──> build prompt ──> Claude Messages API ──> answer with [n] citations\n         └─────────────────────────────────────────────┘\n```\n\nEmbedding, storage and reranking all happen on Pinecone's side, so you don't install PyTorch or host an embedding model. The trade-off: your text is processed by Pinecone's hosted models (more on that under data residency).\n\nCreate an account at [pinecone.io](https://www.pinecone.io/), then create a key on the API Keys page of the console ([https://app.pinecone.io](https://app.pinecone.io)). The free Starter plan is enough for everything here, but note its limits (as of November 2026, see [Pricing](https://www.pinecone.io/pricing/) and [Rate limits](https://docs.pinecone.io/reference/api/database-limits/rate-limits.md)):\n\n`us-east-1`` bge-reranker-v2-m3`: Create a key in the Claude Console ([https://platform.claude.com](https://platform.claude.com)). Check Anthropic's [supported countries list](https://www.anthropic.com/supported-countries) for your region first.\n\n`requirements.txt`:\n\n```\npinecone==10.3.0\nanthropic==2.4.0\npython -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\n```\n\nThe package is `pinecone`, not the old `pinecone-client`.\n\n| Variable | Required | Notes | \n|---|---|---|\n| `PINECONE_API_KEY` | yes | From the Pinecone console | \n| `ANTHROPIC_API_KEY` | yes | From the Claude Console; `anthropic.Anthropic()` reads it by default | \n| `PINECONE_INDEX` | no | Index name, default `company-docs` | \n| `PINECONE_NAMESPACE` | no | Namespace, default `default-tenant` | \n| `USE_RERANK` | no | `1` to rerank (default),`0` to skip and save Starter rerank quota | \n\n```\nexport PINECONE_API_KEY=\"your-pinecone-key\"\nexport ANTHROPIC_API_KEY=\"your-anthropic-key\"\n```\n\nDon't hard-code keys or commit them to Git.\n\nChunking is the first thing that makes or breaks RAG quality. Chunks that are too large mix several topics, so the embedding gets \"averaged out\" and you waste prompt tokens. Chunks that are too small lose context and the model gets half a sentence.\n\nA simple strategy that works for Markdown or plain-text policy docs:\n\n``` php\ndef split_text(text: str, size: int = 500, overlap: int = 80) -> list[str]:\n    paragraphs = [p.strip() for p in text.split(\"\\n\\n\") if p.strip()]\n    chunks, buf = [], \"\"\n    for p in paragraphs:\n        if len(p) > size:\n            if buf:\n                chunks.append(buf)\n                buf = \"\"\n            step = size - overlap\n            chunks.extend(p[i:i + size] for i in range(0, len(p), step))\n            continue\n        if len(buf) + len(p) + 1 <= size:\n            buf = f\"{buf}\\n{p}\" if buf else p\n        else:\n            chunks.append(buf)\n            buf = (buf[-overlap:] + \"\\n\" + p) if overlap else p\n    if buf:\n        chunks.append(buf)\n    return chunks\n```\n\nThis counts characters, not tokens, to avoid depending on a tokenizer. 500 characters of English is roughly 100–150 tokens, well under both limits that matter here:\n\n`llama-text-embed-v2`: max 2048 tokens per input, truncated by default beyond that (\nFor finer strategies (by sentence, by token, by heading), see Pinecone's [Chunking Strategies](https://www.pinecone.io/learn/chunking-strategies/).\n\nEach chunk carries metadata: `source` (relative path), `category` (top-level folder such as `hr/` or `finance/`) and `chunk_index`. `category` is used for filtering, `source` for citations and for cleaning up on re-ingest.\n\nAnthropic doesn't offer an embedding model ([Embeddings](https://platform.claude.com/docs/en/build-with-claude/embeddings)), so you need one from somewhere else. Three common options:\n\n| Option | How | Pros | Cons | \n|---|---|---|---|\n| A. Pinecone integrated embedding (used here) | Declare `embed` on a text field in the index schema; write and query plain text | Least code, no dimension mismatches | Limited to Pinecone-hosted models; text is processed by those models | \n| B. Pinecone Inference, called separately | `pc.inference.embed(...)` returns vectors you write to a`dense_vector` field | You own the vectors and can reuse them | You must keep the model and dimension consistent between write and query | \n| C. Third-party or self-hosted model | Voyage AI (one of the providers Anthropic's docs point to), another cloud API, or an open-source model | Any model, can be fully private | Another service to run; you align dimensions and normalization yourself | \n\nPinecone-hosted dense embedding models (docs as of November 2026):\n\n| Model | Dimensions | Max input | Notes | \n|---|---|---|---|\n| `llama-text-embed-v2` | 1024 (default) / 2048 / 768 / 512 / 384 | 2048 tokens | Model page lists 26 supported languages | \n| `multilingual-e5-large` | 1024 | 507 tokens | Multilingual; good for short queries + medium passages | \n| `text-embedding-3-small` /`-large` | up to 1536 / 3072, configurable | 8192 tokens | OpenAI models processed via Microsoft Azure; document indexes only | \n| `gemini-embedding-2` | 128–3072 | 8192 tokens | Google model; not available on Starter | \n\nOn the Standard plan, `llama-text-embed-v2` is listed at $0.16 per million tokens and `multilingual-e5-large` at $0.08 ([Pricing](https://www.pinecone.io/pricing/)).\n\nThis walkthrough uses `llama-text-embed-v2`: multilingual, a roomier input limit than e5, and available on Starter. If your docs are mostly short Q&A pairs, try `multilingual-e5-large`: change the model name in the schema and rebuild the index.\n\nIn SDK v10 the recommended index type is the **document index**. You declare the *scoring* fields with `SchemaBuilder`; every other field you write is stored as metadata and indexed for filtering automatically. Adding an `embed` config to a `string` field makes Pinecone embed that field's text on write, and embed the query text at search time ([Integrated embedding](https://docs.pinecone.io/guides/search/integrated-embedding.md)).\n\n``` python\nimport os\nfrom pinecone import Pinecone, SchemaBuilder\n\npc = Pinecone(api_key=os.environ[\"PINECONE_API_KEY\"])\n\nschema = (\n    SchemaBuilder()\n    .add_string_field(\n        name=\"chunk_text\",\n        embed={\"model\": \"llama-text-embed-v2\", \"dimension\": 1024},\n    )\n    .build()\n)\n\nif not pc.indexes.exists(name=\"company-docs\"):\n    pc.indexes.create(\n        name=\"company-docs\",\n        schema=schema,\n        deployment={\"deployment_type\": \"managed\", \"cloud\": \"aws\", \"region\": \"us-east-1\"},\n    )\n```\n\nNotes:\n\n`deployment_type: \"managed\"` means serverless.`spec=ServerlessSpec(...)` is deprecated (the SDK source says it's translated into `deployment`). If you omit `pc.indexes.create` `timeout=-1` to return immediately.`source` or `category` in it returns a 400 at creation.`embed` field also stores the original text and enables full-text search by default (`store_text` defaults to true), so search results can return `chunk_text`. Set `store_text=False` if you don't want the raw text stored in Pinecone, but then you'll have to look it up by ID in your own storage.\n\n```\nindex = pc.index(name=\"company-docs\")\n\ndocuments = [\n    {\n        \"_id\": \"3be9998a6b0d7b83#0\",          # ASCII only\n        \"chunk_text\": \"Employees get 5 days of paid annual leave after one year of service...\",\n        \"source\": \"hr/annual-leave.md\",       # metadata, filterable automatically\n        \"category\": \"hr\",\n        \"chunk_index\": 0,\n    },\n    # ...\n]\n\nfor i in range(0, len(documents), 96):\n    index.documents.upsert(namespace=\"default-tenant\", documents=documents[i:i + 96])\n```\n\nKey points:\n\n`_id` must be 1–512 ASCII characters.`usage.embed_total_tokens` in the response tells you how many embedding tokens the write used, which helps reconcile costs.\n**Re-ingesting:** if a file shrinks and produces fewer chunks, the old extra chunks linger. The script calls `documents.delete(filter=...)` by `source` before writing, to clear a file's old chunks.\n\n```\nresp = index.documents.search(\n    namespace=\"default-tenant\",\n    top_k=12,\n    score_by=[{\"type\": \"embed\", \"fields\": [\"chunk_text\"], \"query\": \"How much annual leave do I get?\"}],\n    include_fields=[\"chunk_text\", \"source\", \"chunk_index\"],\n    filter={\"category\": {\"$eq\": \"hr\"}},     # optional metadata filter\n)\nfor m in resp.matches:\n    print(m.id, m.score, m.get(\"source\"))\n```\n\n`include_fields` you only get `_score`. This is a classic beginner trap: the search \"works\" but returns no text. `[\"*\"]` returns every field.`score_by`; it can't be combined with a BM25 `text` clause. For hybrid search, the docs suggest running both searches and merging with RRF (\nVector recall uses a bi-encoder: question and documents are embedded separately and compared by distance. It's fast but coarse. A rerank model (cross-encoder) scores the question and each document together, which is more accurate but slower. The usual pattern is to over-recall with vectors (12 here) and rerank down to the best few (4 here) for the prompt.\n\n```\nranked = pc.inference.rerank(\n    model=\"bge-reranker-v2-m3\",\n    query=\"How much annual leave do I get?\",\n    documents=[{\"id\": m.id, \"chunk_text\": m.get(\"chunk_text\", \"\")} for m in resp.matches],\n    rank_fields=[\"chunk_text\"],\n    top_n=4,\n    return_documents=False,\n    parameters={\"truncate\": \"END\"},\n)\nfor r in ranked.data:\n    print(r.index, r.score)   # r.index is the position in the documents you passed in\n```\n\nModel choice: Pinecone currently hosts three rerank models: `bge-reranker-v2-m3` (multilingual, available on Starter), `cohere-rerank-4-fast` (Standard and above, processed via Azure, possibly outside the US), and `pinecone-rerank-v0` (deprecated; don't use it in new projects).\n\n`bge-reranker-v2-m3` defaults to `truncate: NONE`, which errors on over-long input, so `END` is set explicitly.\n\nOlder tutorials often do recall + rerank in one call via `index.search(..., rerank={...})`. That's for **legacy integrated indexes** (created with `create_index_for_model`, using the Records API). With document indexes, rerank is a separate `pc.inference.rerank` call.\n\nWrap each retrieved chunk in numbered XML tags so the model has anchors to cite:\n\n```\nSYSTEM_PROMPT = \"\"\"You are an internal knowledge-base assistant. Answer employees' questions using ONLY the material inside <documents>.\nRules:\n1. If the answer isn't in the material, say \"I couldn't find this in the knowledge base.\" Don't make anything up.\n2. After each key statement, cite the source as [n], where n matches <document index=\"n\">.\n3. If sources conflict, list the conflict instead of picking one.\n4. Answer concisely.\"\"\"\n\ndef build_user_message(question, hits):\n    blocks = \"\\n\".join(\n        f'<document index=\"{i}\" source=\"{h[\"source\"]}\">\\n{h[\"text\"]}\\n</document>'\n        for i, h in enumerate(hits, start=1)\n    )\n    return f\"<documents>\\n{blocks}\\n</documents>\\n\\nQuestion: {question}\"\n```\n\nCalling the Messages API:\n\n``` python\nimport anthropic\n\nclient = anthropic.Anthropic()   # reads ANTHROPIC_API_KEY\n\nresponse = client.beta.messages.create(\n    model=\"claude-sonnet-5-5\",\n    max_tokens=16000,\n    system=SYSTEM_PROMPT,\n    messages=[{\"role\": \"user\", \"content\": build_user_message(question, hits)}],\n    output_config={\"effort\": \"medium\"},\n    fallbacks=\"default\",\n    betas=[\"server-side-fallback-2026-07-01\"],\n)\n\nif response.stop_reason == \"refusal\":\n    print(\"The model declined to answer.\")\nelse:\n    print(\"\".join(b.text for b in response.content if b.type == \"text\"))\n```\n\nWhat's different from older code:\n\n`response.content` is a list of blocks`thinking` blocks. Filter on `type == \"text\"` instead of reading `content[0].text`.` output_config.effort``low` / `medium` / `high` / `xhigh` / `max`. Sonnet 5.5 defaults to `temperature`.` 2026-10-21`, the default in Anthropic SDK 2.x, rejects any request containing `top_p` or `top_k` with a 400. Delete the `temperature=0` that old code often has.`stop_reason == \"refusal\"`` fallbacks=\"default\"` plus the beta header `server-side-fallback-2026-07-01` lets the server retry a refused request on a recommended fallback model (`client.beta.messages.create`. To skip the beta, drop the last two arguments and use `client.messages.create`; keep the `refusal` check either way.`max_tokens`\nSave as `rag_bot.py`:\n\n```\n\"\"\"\nrag_bot.py - company-docs Q&A bot on Pinecone (integrated embedding + rerank) + Claude\n\nUsage:\n    python rag_bot.py ingest ./docs                        # chunk and upsert\n    python rag_bot.py ask \"How much annual leave do I get?\"\n    python rag_bot.py ask \"expense process\" --category finance   # search one category only\n\nVersions at time of writing: pinecone==10.3.0, anthropic==2.4.0, python>=3.10\n\"\"\"\n\nimport argparse\nimport hashlib\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nimport anthropic\nfrom pinecone import Pinecone, SchemaBuilder\nfrom pinecone.errors import NotFoundError\n\n# ---------- config ----------\nINDEX_NAME = os.environ.get(\"PINECONE_INDEX\", \"company-docs\")\nNAMESPACE = os.environ.get(\"PINECONE_NAMESPACE\", \"default-tenant\")\nEMBED_MODEL = \"llama-text-embed-v2\"      # Pinecone-hosted embedding model\nEMBED_DIM = 1024                          # this model supports 384/512/768/1024/2048\nRERANK_MODEL = \"bge-reranker-v2-m3\"      # Pinecone-hosted rerank model (multilingual)\nUSE_RERANK = os.environ.get(\"USE_RERANK\", \"1\") == \"1\"\nCLAUDE_MODEL = \"claude-sonnet-5-5\"\n\nCHUNK_SIZE = 500        # target characters per chunk\nCHUNK_OVERLAP = 80      # overlap between adjacent chunks\nUPSERT_BATCH = 96       # max docs per upsert for indexes with an embed field\nRECALL_TOP_K = 12       # vector recall size\nFINAL_TOP_N = 4         # chunks sent to the prompt after rerank\n\npc = Pinecone(api_key=os.environ[\"PINECONE_API_KEY\"])\nclaude = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY\n\n# ---------- 1. chunking ----------\ndef split_text(text: str, size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list[str]:\n    \"\"\"Split on blank lines, pack paragraphs up to `size`, hard-split long paragraphs with overlap.\"\"\"\n    paragraphs = [p.strip() for p in text.split(\"\\n\\n\") if p.strip()]\n    chunks, buf = [], \"\"\n    for p in paragraphs:\n        if len(p) > size:                       # long paragraph: flush buffer, then sliding window\n            if buf:\n                chunks.append(buf)\n                buf = \"\"\n            step = size - overlap\n            chunks.extend(p[i:i + size] for i in range(0, len(p), step))\n            continue\n        if len(buf) + len(p) + 1 <= size:\n            buf = f\"{buf}\\n{p}\" if buf else p\n        else:\n            chunks.append(buf)\n            # carry the tail of the previous chunk to reduce answers cut in half\n            buf = (buf[-overlap:] + \"\\n\" + p) if overlap else p\n    if buf:\n        chunks.append(buf)\n    return chunks\n\ndef load_documents(root: Path) -> list[dict]:\n    \"\"\"Read .md / .txt files under root and build the documents to upsert.\"\"\"\n    docs = []\n    for path in sorted(root.rglob(\"*\")):\n        if path.suffix.lower() not in {\".md\", \".txt\"} or not path.is_file():\n            continue\n        rel = path.relative_to(root).as_posix()\n        text = path.read_text(encoding=\"utf-8\")\n        # _id must be ASCII; hash the path so non-ASCII file names work\n        file_key = hashlib.sha1(rel.encode(\"utf-8\")).hexdigest()[:16]\n        category = rel.split(\"/\")[0] if \"/\" in rel else \"general\"\n        for i, chunk in enumerate(split_text(text)):\n            docs.append({\n                \"_id\": f\"{file_key}#{i}\",\n                \"chunk_text\": chunk,        # Pinecone embeds this field\n                \"source\": rel,              # the rest is metadata, filterable automatically\n                \"category\": category,\n                \"chunk_index\": i,\n            })\n    return docs\n\n# ---------- 2. index ----------\ndef ensure_index() -> None:\n    if pc.indexes.exists(name=INDEX_NAME):\n        return\n    schema = (\n        SchemaBuilder()\n        .add_string_field(\n            name=\"chunk_text\",\n            embed={\"model\": EMBED_MODEL, \"dimension\": EMBED_DIM},\n        )\n        .build()\n    )\n    print(f\"Creating index {INDEX_NAME} ...\")\n    pc.indexes.create(\n        name=INDEX_NAME,\n        schema=schema,\n        # managed = serverless. The free Starter plan only allows aws / us-east-1\n        deployment={\"deployment_type\": \"managed\", \"cloud\": \"aws\", \"region\": \"us-east-1\"},\n    )  # blocks until the index is ready by default\n\n# ---------- 3. ingest ----------\ndef ingest(root: Path) -> None:\n    ensure_index()\n    index = pc.index(name=INDEX_NAME)\n    docs = load_documents(root)\n    if not docs:\n        print(\"No .md / .txt files found\")\n        return\n\n    # on re-ingest, delete each file's old chunks first so none are left behind\n    for source in sorted({d[\"source\"] for d in docs}):\n        try:\n            index.documents.delete(namespace=NAMESPACE, filter={\"source\": {\"$eq\": source}})\n        except NotFoundError:\n            pass  # namespace doesn't exist yet (first ingest)\n\n    for i in range(0, len(docs), UPSERT_BATCH):\n        batch = docs[i:i + UPSERT_BATCH]\n        resp = index.documents.upsert(namespace=NAMESPACE, documents=batch)\n        tokens = resp.usage.embed_total_tokens if resp.usage else \"?\"\n        print(f\"Upserted {i + len(batch)}/{len(docs)} chunks, embedding tokens this batch: {tokens}\")\n\n    time.sleep(5)  # indexing is asynchronous; wait a moment before querying\n    print(\"Ingest complete\")\n\n# ---------- 4. retrieve + rerank ----------\ndef retrieve(question: str, category: str | None = None) -> list[dict]:\n    index = pc.index(name=INDEX_NAME)\n    resp = index.documents.search(\n        namespace=NAMESPACE,\n        top_k=RECALL_TOP_K,\n        score_by=[{\"type\": \"embed\", \"fields\": [\"chunk_text\"], \"query\": question}],\n        include_fields=[\"chunk_text\", \"source\", \"chunk_index\"],\n        filter={\"category\": {\"$eq\": category}} if category else None,\n    )\n    hits = [\n        {\n            \"id\": m.id,\n            \"score\": m.score,\n            \"text\": m.get(\"chunk_text\", \"\"),\n            \"source\": m.get(\"source\", \"\"),\n        }\n        for m in resp.matches\n    ]\n    if not hits or not USE_RERANK:\n        return hits[:FINAL_TOP_N]\n\n    ranked = pc.inference.rerank(\n        model=RERANK_MODEL,\n        query=question,\n        documents=[{\"id\": h[\"id\"], \"chunk_text\": h[\"text\"]} for h in hits],\n        rank_fields=[\"chunk_text\"],\n        top_n=FINAL_TOP_N,\n        return_documents=False,\n        parameters={\"truncate\": \"END\"},\n    )\n    # r.index is the document's position in the list we passed in\n    return [dict(hits[r.index], score=r.score) for r in ranked.data]\n\n# ---------- 5. prompt Claude ----------\nSYSTEM_PROMPT = \"\"\"You are an internal knowledge-base assistant. Answer employees' questions using ONLY the material inside <documents>.\nRules:\n1. If the answer isn't in the material, say \"I couldn't find this in the knowledge base.\" Don't make anything up.\n2. After each key statement, cite the source as [n], where n matches <document index=\"n\">.\n3. If sources conflict, list the conflict instead of picking one.\n4. Answer concisely.\"\"\"\n\ndef build_user_message(question: str, hits: list[dict]) -> str:\n    blocks = \"\\n\".join(\n        f'<document index=\"{i}\" source=\"{h[\"source\"]}\">\\n{h[\"text\"]}\\n</document>'\n        for i, h in enumerate(hits, start=1)\n    )\n    return f\"<documents>\\n{blocks}\\n</documents>\\n\\nQuestion: {question}\"\n\ndef answer(question: str, category: str | None = None) -> None:\n    hits = retrieve(question, category)\n    if not hits:\n        print(\"Nothing was retrieved from the knowledge base.\")\n        return\n\n    response = claude.beta.messages.create(\n        model=CLAUDE_MODEL,\n        max_tokens=16000,\n        system=SYSTEM_PROMPT,\n        messages=[{\"role\": \"user\", \"content\": build_user_message(question, hits)}],\n        output_config={\"effort\": \"medium\"},\n        # if a safety classifier refuses, the server retries on a recommended model (beta)\n        fallbacks=\"default\",\n        betas=[\"server-side-fallback-2026-07-01\"],\n    )\n\n    if response.stop_reason == \"refusal\":\n        print(\"The model declined to answer.\")\n        return\n\n    text = \"\".join(b.text for b in response.content if b.type == \"text\")\n    print(text)\n    print(\"\\n---- Sources ----\")\n    for i, h in enumerate(hits, start=1):\n        score = f\"{h['score']:.4f}\" if h[\"score\"] is not None else \"n/a\"\n        print(f\"[{i}] {h['source']}  (score={score})\")\n    u = response.usage\n    print(f\"\\n(model={response.model}, input_tokens={u.input_tokens}, output_tokens={u.output_tokens})\")\n\n# ---------- entry point ----------\ndef main() -> None:\n    parser = argparse.ArgumentParser(description=\"Pinecone + Claude docs Q&A\")\n    sub = parser.add_subparsers(dest=\"cmd\", required=True)\n    p_ingest = sub.add_parser(\"ingest\")\n    p_ingest.add_argument(\"path\", type=Path)\n    p_ask = sub.add_parser(\"ask\")\n    p_ask.add_argument(\"question\")\n    p_ask.add_argument(\"--category\", default=None)\n    args = parser.parse_args()\n\n    if args.cmd == \"ingest\":\n        ingest(args.path)\n    else:\n        answer(args.question, args.category)\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except anthropic.APIStatusError as e:\n        sys.exit(f\"Claude API error {e.status_code}: {e.message}\")\n```\n\nPrepare a folder whose top-level subfolders act as categories:\n\n```\ndocs/\n├── hr/\n│   ├── annual-leave.md\n│   └── onboarding.md\n└── finance/\n    └── expenses.md\npython rag_bot.py ingest ./docs\npython rag_bot.py ask \"Do I get annual leave in my first year?\"\npython rag_bot.py ask \"What's the hotel limit on business trips?\" --category finance\nUSE_RERANK=0 python rag_bot.py ask \"How far ahead do I need to request leave?\"   # no rerank quota used\n```\n\nThe output shows the answer, the source file behind each `[n]` with its score, and the token usage for the call so you can check it against your bill.\n\nThese are list-price estimates, not a measured bill; your console usage is the source of truth.\n\n**Pinecone** (as of November 2026, [Pricing](https://www.pinecone.io/pricing/)):\n\n| Plan | Price | Notes | \n|---|---|---|\n| Starter | Free | Limits in 2.1; over the limit requests get 429, never billed | \n| Builder | $20/month flat | Higher limits; overages are also blocked, not billed | \n| Standard | $50/month minimum, then usage | Storage $0.33/GB/month; reads $16–18 per million RU; writes $4–4.5 per million WU (varies by cloud/region); `llama-text-embed-v2` $0.16 per million tokens;`bge-reranker-v2-m3` $2 per 1,000 requests; egress $0.10/GB (100 GB/month included) | \n| Enterprise | $500/month minimum | 99.95% uptime SLA | \n\nAn internal knowledge base of a few hundred policy docs usually fits within Starter's 2 GB and 5M embedding tokens a month. The first limit you'll hit is usually **500 reranks a month**. Either disable rerank on Starter, call it only when recall scores are too close to separate, or upgrade. Read/write unit accounting is detailed in [Understanding cost](https://docs.pinecone.io/guides/manage-cost/understanding-cost.md).\n\n**Claude** (as of November 2026, [Pricing](https://platform.claude.com/docs/en/about-claude/pricing)): `claude-sonnet-5-5` is $2 per million input tokens, $10 per million output tokens, and $0.10 per million cache-hit tokens.\n\nRough math for one question with these settings: four chunks plus system prompt and question is a few thousand input tokens; the answer is a few hundred. Input cost is well under a cent; output cost depends on answer length, and **thinking tokens are billed as output**, which is why `effort` is worth tuning. For real numbers, multiply the `input_tokens` / `output_tokens` the script prints by the unit prices.\n\nWays to save:\n\n`FINAL_TOP_N` aren't automatically better: 10 chunks instead of 4 is more than double the input tokens.\nIf you generate vectors yourself (options B/C), the index `dimension` must match the model output. Writing with model A and querying with model B gives essentially random results even if the dimensions happen to match. Using an `embed` field with `embed` scoring avoids this structurally, because Pinecone uses the model fixed at creation for both. Remember: **the schema and embedding config can't change after creation**; a new model means a new index and a full re-ingest.\n\nWith option B, the quickstart pattern is `pc.inference.embed(model=..., inputs=[...], parameters={\"input_type\": \"passage\"})`, written into a field declared with `add_dense_vector_field`, and `input_type=\"query\"` at search time. Omitting or swapping `input_type` won't error, but retrieval quality drops.\n\nIf the write and query namespaces differ, you get no error, just empty results. The script keeps the namespace in one env var. Namespaces are good for multi-tenant isolation (one per department or customer), but Starter caps an index at 100 namespaces, so many tenants need a higher plan or metadata filtering instead.\n\n`$eq`, `$in`, `$gte` and friends.`$match_phrase` / `$match_all` / `$match_any`.` chunk_index` always an integer.\n**Metadata filters are not access control.** If finance policies should only be visible to finance staff, the server must add the filter based on the logged-in user; never trust a `category` sent from the client. For stricter isolation, use separate namespaces or even separate indexes.\n\n`truncate=END`), so the tail never makes it into the vector.\nIndexing is asynchronous, so there's a short eventual-consistency window. A few seconds of `sleep` is enough in an ingest script; if your app needs read-after-write, handle it with a retry or a UI hint.\n\n| What old tutorials show | In v10 | \n|---|---|\n| `pip install pinecone-client` | Package renamed to `pinecone` | \n| `pc.create_index(..., spec=ServerlessSpec(...))` | `spec` deprecated; use`pc.indexes.create(..., deployment={...})` | \n| `pc.create_index_for_model(...)` +`index.upsert_records(...)` +`index.search(..., rerank=...)` | Creates a \"legacy integrated index\" on the old Records API; still works, but no text updates or import | \n| `from pinecone import ...preview...` | `pinecone.preview` removed in v10; document indexes and`SchemaBuilder` import from`pinecone` directly | \n| Testing locally with Pinecone Local | Pinecone Local reached end of life in 2026 ( [Release notes](https://docs.pinecone.io/release-notes/2026.md) ) | \n\n`messages` to steer formatting; current models don't support prefill and return 400. Use `output_config.format` for structured output.`stop_reason == \"max_tokens\"`).\nCheck these before rolling this out inside a company:\n\n`ap-southeast-1` and Europe) need a paid plan (`global`, possibly outside the US (\nIf your docs contain personal data or sensitive business information, get legal and security review first. If data can't leave your environment, this managed stack isn't the right fit; look at self-hostable options like pgvector or Milvus with a model you can run or contract for in-region.\n\n`string` field of a document index, write plain text, let Pinecone embed it with `documents.search` with `pc.inference.rerank` with `<documents>` tags; Claude Sonnet 5.5 answers only from them, with citations.\nThe upside is very little code and no models or databases to operate, which makes it a good way to test whether \"Q&A over company docs\" is worth building. The downsides are just as clear: Pinecone is a closed managed service and your data lives in its cloud; Starter's rerank and region limits are tight; and costs grow linearly with data and query volume, so reconcile your bill regularly.\n\nWhere to go next:\n\n`CHUNK_SIZE`, `effort`.\nPinecone and Anthropic iterate quickly; check versions before running, and where this article and the official docs disagree, trust the docs.", "url": "https://wpnews.pro/news/build-a-q-a-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10", "canonical_source": "https://dev.to/_579356bdff348c05e9ae17/build-a-qa-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10-4ane", "published_at": "2026-11-04 01:04:51+00:00", "updated_at": "2026-11-04 01:22:24.526303+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "generative-ai", "ai-products", "developer-tools"], "entities": ["Pinecone", "Anthropic", "Claude", "claude-sonnet-5-5", "llama-text-embed-v2", "bge-reranker-v2-m3", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/build-a-q-a-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10", "markdown": "https://wpnews.pro/news/build-a-q-a-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10.md", "text": "https://wpnews.pro/news/build-a-q-a-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10.txt", "jsonld": "https://wpnews.pro/news/build-a-q-a-bot-over-your-company-docs-with-pinecone-claude-python-sdk-v10.jsonld"}}