{"slug": "building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone", "title": "Building a Personal Notes Assistant with RAG, Amazon Bedrock, and Pinecone", "summary": "A developer built a personal notes assistant using RAG with Amazon Bedrock and Pinecone, enabling users to upload .txt files and ask questions answered strictly from their notes. The system uses embeddings for semantic search and includes validation checks to prevent corrupted files from crashing downstream services.", "body_md": "Have you ever saved hundreds of digital notes only to spend twenty minutes hunting for one tiny detail?\n\nThat exact frustration led me to build a custom notes assistant. I wanted a private API where I could upload my personal files, ask a question in plain English, and get an answer drawn directly from my notes—not just a generic guess from a public AI model.\n\nIn this guide, I’ll break down how I built this project using Python, Flask, Amazon Bedrock, Pinecone, S3, AWS Lambda, and API Gateway. I’ll keep the explanations clear and conversational while walking through the real code and key takeaways.\n\nProject Goal:Upload plain text (`.txt`\n\n) notes and receive accurate answers strictly grounded in their content.\n\nRAG stands for **Retrieval-Augmented Generation**.\n\nNormally, when you ask an AI model a question, it relies entirely on its training data. If your information is private, recent, or highly specific, the model won't know it.\n\n`Question -> Model -> Answer`\n\n`Question -> Search My Notes -> Grab Relevant Snippets -> Model -> Answer`\n\nBy adding a retrieval step, we feed the model relevant passages from our own documents alongside our question. We aren't retraining the AI model; we're giving it an open-book test using documents we control.\n\nImagine walking into a library and asking a librarian: *\"What do my project notes say about serverless memory limits?\"*\n\nThe librarian doesn't read every book on the shelves from cover to cover. Instead, they:\n\nHere is how that physical library maps directly to our technical setup:\n\n| Physical Library | RAG System Component |\n|---|---|\n| Books on shelves | Original text files stored in Amazon S3 |\n| Individual pages | Document chunks |\n| Catalog cards | Embeddings (numerical representations of meaning) |\n| Searching the catalog | Pinecone similarity search |\n| Top three selected pages | Top three matching text chunks |\n| Librarian giving the answer | Amazon Nova Lite generating the response |\n\nAn **embedding** is just a snippet of text converted into a list of numbers (a vector) that represents its core meaning. Words or phrases with similar meanings end up near each other in digital space.\n\nFor instance, a traditional keyword search might miss the connection between these two sentences:\n\nBecause they share few identical words, keyword search struggles. But a **semantic search** using embeddings recognizes that both sentences ask about file storage.\n\nThe application handles two main workflows: **Ingestion** (saving and indexing notes) and **Querying** (searching notes and answering questions).\n\n`.txt`\n\nfile to the Flask backend.`/ask`\n\nendpoint.Security starts at the entry point. The `/ingest`\n\nendpoint accepts multipart form data and runs several checks before touching the rest of our system:\n\n```\n# Check for file presence and secure filename\nif \"file\" not in request.files:\n    return error_response(\"MISSING_FILE\", \"No file was provided\", 400)\n\nuploaded_file = request.files[\"file\"]\nfilename = secure_filename(uploaded_file.filename or \"\")\n\nif not filename or os.path.splitext(filename)[1].lower() != \".txt\":\n    return error_response(\"INVALID_FILE_TYPE\", \"Only UTF-8 .txt files are accepted\", 415)\n\n# Validate encoding and content readability\ncontent = uploaded_file.read()\nif not content:\n    return error_response(\"EMPTY_FILE\", \"Uploaded text file is empty\", 400)\n\ntry:\n    decoded_content = content.decode(\"utf-8\")\nexcept UnicodeDecodeError:\n    return error_response(\"INVALID_TEXT_ENCODING\", \"Text file must use UTF-8 encoding\", 400)\n\nif not decoded_content.strip() or \"\\x00\" in decoded_content:\n    return error_response(\"INVALID_TEXT_CONTENT\", \"File must contain valid text\", 400)\n```\n\nCatching bad requests early with specific errors like `INVALID_FILE_TYPE`\n\nprevents corrupted files from crashing downstream services like Pinecone or S3.\n\nOnce validated, the file goes to S3. To avoid accidental overwrites when uploading multiple files with generic names like `notes.txt`\n\n, the application generates a unique ID (UUID) for each storage key while keeping the real filename in S3 metadata:\n\n``` php\ndef upload_text_file(file_path: str, original_filename: str) -> tuple[str, str]:\n    bucket = os.getenv(\"S3_DOCUMENT_BUCKET\")\n    # Create a unique path key\n    key = f\"uploads/{uuid4().hex}.txt\"\n\n    boto3.client(\"s3\").upload_file(\n        file_path,\n        bucket,\n        key,\n        ExtraArgs={\n            \"ContentType\": \"text/plain; charset=utf-8\",\n            \"Metadata\": {\"original-filename\": Path(original_filename).name},\n        },\n    )\n    return bucket, key\n```\n\nSending entire long documents directly into vector search reduces precision. We split text into chunks using LangChain's `RecursiveCharacterTextSplitter`\n\n:\n\n```\ntext_splitter = RecursiveCharacterTextSplitter(\n    chunk_size=1000,\n    chunk_overlap=100,\n)\ndocs = text_splitter.split_documents(documents)\n```\n\n**Why include overlap?**\n\nIf a key idea gets split right at character 1,000, half of the context ends up in Chunk A and half in Chunk B. Overlapping neighboring chunks by 100 characters preserves complete sentences and context across boundaries.\n\nNext, we convert text chunks into numbers using Amazon Titan Text Embeddings V2 and save them into Pinecone:\n\n```\nembedding = BedrockEmbeddings(\n    model_id=\"amazon.titan-embed-text-v2:0\",\n    dimensions=512,\n    normalize=True,\n    region_name=\"ap-south-1\",\n)\n\nPineconeVectorStore.from_documents(\n    docs,\n    index_name=index_name,\n    embedding=embedding,\n    namespace=\"default\",\n)\n```\n\n*Golden Rule:* Your document chunks and your incoming search questions **must** use the exact same embedding model, dimension count, and normalization settings. Otherwise, vector distances become meaningless.\n\nWhen asking a question via `/ask`\n\n, Pinecone finds the three nearest chunks:\n\n```\n# Grab top 3 matching snippets\ndocuments = docsearch.as_retriever(search_kwargs={\"k\": 3}).invoke(question.strip())\ncontext_str = \"\\n\\n\".join(doc.page_content for doc in documents)\n```\n\nWe pass those snippets into Amazon Nova Lite with strict prompt instructions:\n\n```\nPROMPT = ChatPromptTemplate.from_template(\n    \"\"\"Answer the question using only the context below.\n\nContext:\n{context}\n\nQuestion: {question}\n\"\"\"\n)\n\nllm = ChatBedrockConverse(\n    model_id=\"amazon.nova-lite-v1:0\",\n    temperature=0.2, # Low temperature keeps answers factual\n    max_tokens=512,\n)\n\nchain = PROMPT | llm | StrOutputParser()\nanswer = chain.invoke({\"question\": question, \"context\": context_str})\n```\n\nFor local development, Flask handles traditional HTTP calls. When deploying to AWS, we run Flask inside AWS Lambda behind an API Gateway using `serverless-wsgi`\n\n:\n\n``` python\nimport serverless_wsgi\nfrom server import app\n\ndef handler(event, context):\n    return serverless_wsgi.handle_request(app, event, context)\n```\n\nThe underlying infrastructure is configured in AWS CloudFormation:\n\n`/health`\n\n, `/ingest`\n\n, `/ask`\n\n).My GitHub repo : [my-rag-notes-app](https://github.com/d3vjamal/my-notes-rag)", "url": "https://wpnews.pro/news/building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone", "canonical_source": "https://dev.to/d3vjamal/-building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone-4jg3", "published_at": "2026-08-26 05:34:19+00:00", "updated_at": "2026-08-26 05:43:19.479474+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "large-language-models", "ai-products", "developer-tools"], "entities": ["Amazon Bedrock", "Pinecone", "Amazon S3", "AWS Lambda", "API Gateway", "Flask", "Python", "Amazon Nova Lite"], "alternates": {"html": "https://wpnews.pro/news/building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone", "markdown": "https://wpnews.pro/news/building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone.md", "text": "https://wpnews.pro/news/building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone.txt", "jsonld": "https://wpnews.pro/news/building-a-personal-notes-assistant-with-rag-amazon-bedrock-and-pinecone.jsonld"}}