Case Study

RAG for Proposals: What We Learned Indexing Past Work in Pinecone

By Expedey Engineering · Updated · 3 min read

Short answer

We built an assistant that gathers a client's needs through chat and forms, retrieves the most relevant past work from a Pinecone index and drafts a tailored proposal or product catalog. The lessons: keep sources in separate namespaces, retrieve a few highly relevant examples rather than many, keep the model swappable, and tell the model when not to use the retrieved context.

The problem

Proposals for a B2B services business are repetitive to write but only work when they're specific: the right past projects, the right description of the team, the client's own words. Copying from old proposals is slow, and generic AI drafts read like everyone else's. We built the assistant for a B2B services business; client details are withheld.

How it works

  1. Intake. A conversation and a short form capture what the prospect needs. Conversations, messages and form submissions are stored in MongoDB.
  2. Reference library. The business's reference material (company bio, past projects, earlier proposals) is split into chunks, embedded with OpenAI embeddings and stored in Pinecone, one namespace per reference set.
  3. Retrieval. For each request, a retriever returns the few most relevant chunks from the right namespace.
  4. Generation. A LangChain chain (prompt template, model, output parser) drafts the proposal or catalog in Markdown. The chain can run on an OpenAI model or a Google Gemini model.
  5. API. A Node.js and Express service exposes conversations, form submissions and generation to the front end.

The retrieval and generation step

A minimal version of the pattern in TypeScript with LangChain. Note the instruction to say so when none of the retrieved work is relevant:

import { Pinecone } from "@pinecone-database/pinecone";
import { PineconeStore } from "@langchain/pinecone";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

const index = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! })
  .Index(process.env.PINECONE_INDEX!);

const store = new PineconeStore(new OpenAIEmbeddings(), {
  pineconeIndex: index,
  namespace: "past-projects",
});
const retriever = store.asRetriever({ k: 3 });

const prompt = PromptTemplate.fromTemplate(`You write proposals for {company}.
Cite experience only from the past work below. If none of it is relevant, say so.

Past work:
{context}

Client request:
{request}`);

export async function draftProposal(company: string, request: string) {
  const docs = await retriever.invoke(request);
  const context = docs.map((doc) => doc.pageContent).join("\n---\n");
  return prompt
    .pipe(new ChatOpenAI({ model: "gpt-4o-mini" }))
    .pipe(new StringOutputParser())
    .invoke({ company, context, request });
}
Keys come from the environment, never from source code.

What we learned

  1. Namespaces keep sources apart. One namespace per reference set means a bio never leaks into another set's project list, and a set can be rebuilt without touching the rest.
  2. A few strong examples beat many weak ones. We kept retrieval tight (two to three chunks). For a proposal, three relevant projects persuade more than ten loosely related ones, and less context means less to contradict.
  3. Tell the model when not to use the context. Retrieval always returns something, even when nothing fits. The prompt has to allow "none of this is relevant".
  4. Keep the model swappable. Building the chain on a common interface let the same flow run on either an OpenAI or a Gemini model, so switching provider is a configuration change.

What we'd do differently today

The first version was built in 2024 with the models of the time. Building it again, we would:

  • Use current models, and route simple steps such as classification to a smaller one.
  • Add metadata filters (industry, service, project size) so retrieval matches on more than wording.
  • Re-rank retrieved chunks before generation.
  • Show which past projects each paragraph drew on, so the sales team can check the claims.
  • Build an evaluation set from proposals the sales team rated, and score every prompt or model change against it.
  • Keep every API key in a secret manager or environment variables, with nothing in source code.

Frequently Asked Questions

Is RAG enough for proposals, or do you need fine-tuning?

For proposals, retrieval over the business's own past work is usually enough and has a big advantage: facts can be updated by re-indexing documents, without retraining anything. Fine-tuning is more useful for a consistent style than for facts.

Why Pinecone?

It's a managed vector database with namespaces, which suited separate reference sets. Other vector stores such as pgvector, Weaviate or Qdrant can do the same job; the retrieval design matters more than the database.

Want an Assistant Trained on Your Own Work?

Expedey is a team of forward-thinking individuals working and servicing clientele across the globe for their software development requirements.

Our Offices

Contact Us

© 2024 Expedey. All Rights Reserved.