chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:47 +08:00
commit 7653f56fed
1422 changed files with 359026 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
MONGODB_URI="<mongodb-connection-string>"
VOYAGE_API_KEY="<voyage-api-key>"
OPENAI_API_KEY= "<openai-api-key>"
+110
View File
@@ -0,0 +1,110 @@
# Database Memory Agent
We're building a Database Memory Agent with RAG (Retrieval Augmented Generation) capabilities that integrates MongoDB Atlas Vector Search for semantic document retrieval, Voyage AI for embeddings, and OpenAI for intelligent responses. The agent uses tools (vector search and calculator) to answer questions from uploaded documents and perform calculations, with context-aware memory across conversations.
We use:
- [MongoDB Atlas Vector Search](https://www.mongodb.com/products/platform/atlas-vector-search) for semantic search and document storage
- [Voyage AI](https://www.voyageai.com/) for generating embeddings (voyage-3-large model)
- [OpenAI](https://openai.com/) for LLM responses (gpt-4o)
- [Streamlit](https://streamlit.io/) to wrap the logic in an interactive UI
## Set Up
### Prerequisites
You must have the following:
- One of the following MongoDB cluster types:
- An [Atlas cluster](https://www.mongodb.com/docs/atlas/tutorial/create-new-cluster/) running MongoDB version 6.0.11, 7.0.2, or later. Ensure that your IP address (Internet Protocol address) is included in your Atlas project's [access list](https://www.mongodb.com/docs/atlas/security/ip-access-list/).
- A local Atlas deployment created using the Atlas CLI. To learn more, see [Create a Local Atlas Deployment](https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-local/).
- A MongoDB Community or Enterprise cluster with [Search and Vector Search](https://www.mongodb.com/docs/manual/administration/install-community/#std-label-community-search-deploy) installed.
- A Voyage AI API key.
- An OpenAI API key.
### Configure Environment Variables
Copy `.env.example` to `.env` and configure the following environment variables:
```env
MONGODB_URI="<mongodb-connection-string>"
VOYAGE_API_KEY="<your-voyage-api-key>"
OPENAI_API_KEY="<your-openai-api-key>"
```
Replace `<mongodb-connection-string>` with the connection string for your Atlas cluster or local Atlas deployment.
**Atlas Cluster:**
Your connection string should use the following format:
```
mongodb+srv://<db_username>:<db_password>@<clusterName>.<hostname>.mongodb.net
```
To learn more, see [Connect to a Cluster via Drivers](https://www.mongodb.com/docs/atlas/driver-connection/).
**Local or Self-Managed:**
Your connection string should use the following format:
```
mongodb://localhost:<port-number>/?directConnection=true
```
To learn more, see [Connection Strings](https://www.mongodb.com/docs/manual/reference/connection-string/).
### Install Dependencies
```bash
uv sync
```
### Run the Application
Run the application with:
```bash
streamlit run app.py
```
Or use the CLI version:
```bash
python main.py
```
[Get your Voyage AI API keys here](https://dashboard.voyageai.com/)
[Get your OpenAI API keys here](https://platform.openai.com/api-keys)
## Project Structure
```
database-memory-agent/
├── .env # Environment variables (create from .env.example)
├── config.py # MongoDB and API configuration
├── ingest_data.py # PDF ingestion and vector index creation
├── tools.py # Agent tools (vector search, calculator)
├── memory.py # Chat history storage
├── planning.py # Agent planning and response generation
├── app.py # Streamlit web application
├── main.py # CLI application
├── pyproject.toml # Project dependencies
└── README.md # This file
```
## 📬 Stay Updated with Our Newsletter!
**Get a FREE Data Science eBook** 📖 with 150+ essential lessons in Data Science when you subscribe to our newsletter! Stay in the loop with the latest tutorials, insights, and exclusive resources. [Subscribe now!](https://join.dailydoseofds.com)
[![Daily Dose of Data Science Newsletter](https://github.com/patchy631/ai-engineering/blob/main/resources/join_ddods.png)](https://join.dailydoseofds.com)
## Contribution
Contributions are welcome! Feel free to fork this repository and submit pull requests with your improvements.
+260
View File
@@ -0,0 +1,260 @@
import streamlit as st
import base64
import uuid
import sys
from io import StringIO
from config import vector_collection
from ingest_data import ingest_data
from planning import generate_response, tool_selector
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Set up page configuration
st.set_page_config(page_title="Database Memory Agent", layout="wide")
# Initialize session state variables
if "session_id" not in st.session_state:
st.session_state.session_id = str(uuid.uuid4())[:8]
if "messages" not in st.session_state:
st.session_state.messages = []
if "vector_index_ready" not in st.session_state:
st.session_state.vector_index_ready = False
if "data_ingested" not in st.session_state:
st.session_state.data_ingested = False
session_id = st.session_state.session_id
def reset_chat():
"""Reset chat history."""
st.session_state.messages = []
st.session_state.session_id = str(uuid.uuid4())[:8]
def display_pdf(file):
"""Display PDF preview in sidebar."""
st.markdown("### PDF Preview")
base64_pdf = base64.b64encode(file.read()).decode("utf-8")
pdf_display = f"""<iframe src="data:application/pdf;base64,{base64_pdf}" width="400" height="100%" type="application/pdf"
style="height:100vh; width:100%"
>
</iframe>"""
st.markdown(pdf_display, unsafe_allow_html=True)
def check_vector_index():
"""Check if vector index exists and is ready."""
if st.session_state.vector_index_ready:
return True
try:
existing_indexes = list(vector_collection.list_search_indexes("vector_index"))
if existing_indexes and existing_indexes[0].get("queryable"):
st.session_state.vector_index_ready = True
return True
except Exception as e:
st.error(f"Error checking vector index: {e}")
return False
return False
def process_pdf_upload(uploaded_file):
"""Process uploaded PDF and ingest into MongoDB."""
with st.spinner("🔄 Processing..."):
try:
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
ingest_data()
finally:
sys.stdout = old_stdout
if check_vector_index():
st.session_state.data_ingested = True
st.success("✅ Document processed and ready for queries!")
return True
return False
except Exception as e:
st.error(f"Error processing PDF: {str(e)}")
return False
def ingest_sample_data():
"""Ingest sample MongoDB earnings report."""
try:
with st.spinner("🔄 Processing..."):
# Suppress print output from ingest_data()
old_stdout = sys.stdout
sys.stdout = StringIO()
try:
ingest_data()
finally:
sys.stdout = old_stdout
# ingest_data() already creates the index, just check if it's ready
if check_vector_index():
st.session_state.data_ingested = True
st.success("✅ Sample data ingested and ready for queries!")
return True
return False
except Exception as e:
st.error(f"Error ingesting sample data: {str(e)}")
return False
# Sidebar for configuration and document upload
with st.sidebar:
st.header("🔧 Configuration")
st.markdown("**Session ID:**")
st.code(session_id)
if st.button("🔄 New Session"):
reset_chat()
st.rerun()
st.markdown("---")
# Document upload section
st.header("📄 Upload Document")
st.markdown("Upload a PDF document or use sample data")
col1, col2 = st.columns(2)
with col1:
if st.button("📊 Use Sample Data", use_container_width=True):
ingest_sample_data()
with col2:
if st.button("🗑️ Clear Data", use_container_width=True):
st.session_state.data_ingested = False
st.session_state.vector_index_ready = False
st.info("Data cleared. Upload a new document to continue.")
uploaded_file = st.file_uploader("Or upload your PDF file", type="pdf")
if uploaded_file:
if process_pdf_upload(uploaded_file):
display_pdf(uploaded_file)
st.markdown("---")
# System status
st.header("📊 System Status")
if st.session_state.data_ingested:
st.success("🟢 Data Ready")
else:
st.info("🔵 No Data Loaded")
if st.session_state.vector_index_ready:
st.success("🟢 Vector Index Ready")
else:
st.warning("🟡 Vector Index Not Ready")
# Main chat interface
col1, col2 = st.columns([6, 1])
with col1:
st.markdown('''
<h1 style='color: #2E86AB; margin-bottom: 10px; font-size: 2.5em;'>
Database Memory Agent
</h1>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 20px;">
<span style='color: #A23B72; font-size: 16px;'>Powered by</span>
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://www.mongodb.com/" style="display: inline-block; vertical-align: middle;">
<img src="https://webimages.mongodb.com/_com_assets/cms/mongodb_logo1-76twgcu2dm.png"
alt="MongoDB" style="height: 40px;">
</a>
<a href="https://www.voyageai.com/" style="display: inline-block; vertical-align: middle;">
<img src="https://www.voyageai.com/favicon.ico"
alt="Voyage AI" style="height: 32px;">
</a>
</div>
</div>
''', unsafe_allow_html=True)
with col2:
if st.button("Clear Chat ↺", on_click=reset_chat):
st.rerun()
# System info
if st.session_state.data_ingested and st.session_state.vector_index_ready:
st.success("🟢 System Ready - You can ask questions about your document!")
elif st.session_state.data_ingested:
st.warning("🟡 Data loaded but vector index is not ready. Please wait...")
else:
st.info("🔵 Upload a PDF document or use sample data to get started")
# Display chat messages from history
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Accept user input
if prompt := st.chat_input("Ask a question about your document..."):
if not st.session_state.data_ingested or not st.session_state.vector_index_ready:
st.error("⚠️ Please upload a document or use sample data first.")
st.stop()
# Add user message to chat history
st.session_state.messages.append({
"role": "user",
"content": prompt
})
# Display user message
with st.chat_message("user"):
st.markdown(prompt)
# Generate response
with st.chat_message("assistant"):
message_placeholder = st.empty()
try:
with st.spinner("🔄 Processing..."):
# Get tool info for display (simple check)
session_history = [{"role": msg["role"], "content": msg["content"]} for msg in st.session_state.messages[-5:]]
tool, _ = tool_selector(prompt, session_history if session_history else None)
# Generate response
response = generate_response(session_id, prompt)
message_placeholder.markdown(response)
# Show simple tool indicator
if tool == "vector_search_tool":
st.info("📚 Using document search")
elif tool == "calculator_tool":
st.info("🔢 Using calculator")
metadata = {"tool": tool}
except Exception as e:
st.error(f"❌ Error processing your question: {str(e)}")
response = "I apologize, but I encountered an error while processing your question. Please try again."
message_placeholder.markdown(response)
metadata = {}
# Add assistant response to chat history
st.session_state.messages.append({
"role": "assistant",
"content": response,
"metadata": metadata
})
# Footer
st.markdown("---")
st.markdown(
"<p style='text-align: center; color: #666; font-size: 12px;'>"
"Database Memory Agent • Built with Streamlit, MongoDB Atlas Vector Search, and Voyage AI"
"</p>",
unsafe_allow_html=True
)
+27
View File
@@ -0,0 +1,27 @@
from pymongo import MongoClient
from openai import OpenAI
import voyageai
from dotenv import load_dotenv
import os
# Load environment variables from .env file
load_dotenv()
# Environment variables (private)
MONGODB_URI = os.getenv("MONGODB_URI")
VOYAGE_API_KEY = os.getenv("VOYAGE_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
# MongoDB cluster configuration
mongo_client = MongoClient(MONGODB_URI)
agent_db = mongo_client["ai_agent_db"]
vector_collection = agent_db["embeddings"]
memory_collection = agent_db["chat_history"]
# Model configuration
voyage_client = voyageai.Client(api_key=VOYAGE_API_KEY)
openai_client = OpenAI(api_key=OPENAI_API_KEY)
VOYAGE_MODEL = "voyage-3-large"
OPENAI_MODEL = "gpt-4o"
+69
View File
@@ -0,0 +1,69 @@
from config import vector_collection, voyage_client, VOYAGE_MODEL
from pymongo.operations import SearchIndexModel
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
import time
def get_embedding(data, input_type = "document"):
embeddings = voyage_client.embed(
data, model = VOYAGE_MODEL, input_type = input_type
).embeddings
return embeddings[0]
def ingest_data():
loader = PyPDFLoader("https://investors.mongodb.com/node/13176/pdf")
data = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=20)
documents = text_splitter.split_documents(data)
print(f"Successfully split PDF into {len(documents)} chunks.")
print("Generating embeddings and ingesting documents...")
docs_to_insert = []
for i, doc in enumerate(documents):
embedding = get_embedding(doc.page_content)
if embedding:
docs_to_insert.append({
"text": doc.page_content,
"embedding": embedding
})
if docs_to_insert:
result = vector_collection.insert_many(docs_to_insert)
print(f"Inserted {len(result.inserted_ids)} documents into the collection.")
else:
print("No documents were inserted. Check embedding generation process.")
index_name = "vector_index"
search_index_model = SearchIndexModel(
definition = {
"fields": [
{
"type": "vector",
"numDimensions": 1024,
"path": "embedding",
"similarity": "cosine"
}
]
},
name=index_name,
type="vectorSearch"
)
try:
vector_collection.create_search_index(model=search_index_model)
print(f"Search index '{index_name}' creation initiated.")
except Exception as e:
print(f"Error creating search index: {e}")
return
print("Polling to check if the index is ready. This may take up to a minute.")
predicate=None
if predicate is None:
predicate = lambda index: index.get("queryable") is True
while True:
indices = list(vector_collection.list_search_indexes(index_name))
if len(indices) and predicate(indices[0]):
break
time.sleep(5)
print(index_name + " is ready for querying.")
+28
View File
@@ -0,0 +1,28 @@
from config import mongo_client
from ingest_data import ingest_data
from planning import generate_response
if __name__ == "__main__":
try:
run_ingest = input("Ingest sample data? (y/n): ")
if run_ingest.lower() == 'y':
ingest_data()
session_id = input("Enter a session ID: ")
while True:
user_query = input("\nEnter your query (or type 'quit' to exit): ")
if user_query.lower() == 'quit':
break
if not user_query.strip():
print("Query cannot be empty. Please try again.")
continue
answer = generate_response(session_id, user_query)
print("\nAnswer:")
print(answer)
finally:
mongo_client.close()
+25
View File
@@ -0,0 +1,25 @@
from config import memory_collection
from datetime import datetime
from typing import List
def store_chat_message(session_id: str, role: str, content: str) -> None:
message = {
"session_id": session_id, # Unique identifier for the chat session
"role": role, # Role of the sender (user or system)
"content": content, # Content of the message
"timestamp": datetime.now(), # Timestamp of when the message was sent
}
memory_collection.insert_one(message)
def retrieve_session_history(session_id: str) -> List:
# Query the collection for messages with a specific "session_id" in ascending order
cursor = memory_collection.find({"session_id": session_id}).sort("timestamp", 1)
# Iterate through the cursor and return a JSON object with the message role and content
if cursor:
messages = [{"role": msg["role"], "content": msg["content"]} for msg in cursor]
else:
messages = []
return messages
+101
View File
@@ -0,0 +1,101 @@
from config import openai_client, OPENAI_MODEL
from tools import vector_search_tool, calculator_tool
from memory import store_chat_message, retrieve_session_history
def tool_selector(user_input, session_history=None):
messages = [
{
"role": "system",
"content": (
"Select the appropriate tool from the options below. Consider the full context of the conversation before deciding.\n\n"
"Tools available:\n"
"- vector_search_tool: Retrieve specific context from the MongoDB earnings report document. Use this for questions about MongoDB, its products, programs, acquisitions, financials, or any topics that might be covered in the document\n"
"- calculator_tool: For mathematical operations\n"
"- none: Only for general questions that are clearly unrelated to the document content\n\n"
"Process for making your decision:\n"
"1. When in doubt, prefer vector_search_tool - it can answer questions about MongoDB, its programs (like MAAP), products, acquisitions, financials, and announcements\n"
"2. Analyze if the current question relates to or follows up on a previous vector search query\n"
"3. For follow-up questions, incorporate context from previous exchanges to create a comprehensive search query\n"
"4. Only use calculator_tool for explicit mathematical operations\n"
"5. Default to none only when certain the question is completely unrelated to the document\n\n"
"When continuing a conversation:\n"
"- Identify the specific topic being discussed\n"
"- Include relevant details from previous exchanges\n"
"- Formulate a query that stands alone but preserves conversation context\n\n"
"Return a JSON object only: {\"tool\": \"selected_tool\", \"input\": \"your_query\"}"
)
}
]
if session_history:
messages.extend(session_history)
messages.append({"role": "user", "content": user_input})
response = openai_client.chat.completions.create(
model=OPENAI_MODEL,
messages=messages
).choices[0].message.content
try:
tool_call = eval(response)
return tool_call.get("tool"), tool_call.get("input")
except:
return "none", user_input
def generate_response(session_id: str, user_input: str) -> str:
store_chat_message(session_id, "user", user_input)
llm_input = []
session_history = retrieve_session_history(session_id)
llm_input.extend(session_history)
user_message = {
"role": "user",
"content": user_input
}
llm_input.append(user_message)
tool, tool_input = tool_selector(user_input, session_history)
print("Tool selected: ", tool)
if tool == "vector_search_tool":
context = vector_search_tool(tool_input)
system_message_content = (
f"Answer the user's question based on the retrieved context and conversation history.\n"
f"1. First, understand what specific information the user is requesting\n"
f"2. Then, locate the most relevant details in the context provided\n"
f"3. Finally, provide a clear, accurate response that directly addresses the question\n\n"
f"If the current question builds on previous exchanges, maintain continuity in your answer.\n"
f"Only state facts clearly supported by the provided context. If information is not available, say 'I DON'T KNOW'.\n\n"
f"Context:\n{context}"
)
response = get_llm_response(llm_input, system_message_content)
elif tool == "calculator_tool":
response = calculator_tool(tool_input)
else:
system_message_content = "You are a helpful assistant. Respond to the user's prompt as best as you can based on the conversation history."
response = get_llm_response(llm_input, system_message_content)
store_chat_message(session_id, "system", response)
return response
def get_llm_response(messages, system_message_content):
system_message = {
"role": "system",
"content": system_message_content,
}
if any(msg.get("role") == "system" for msg in messages):
messages.append(system_message)
else:
messages = [system_message] + messages
response = openai_client.chat.completions.create(
model=OPENAI_MODEL,
messages=messages
).choices[0].message.content
return response
+17
View File
@@ -0,0 +1,17 @@
[project]
name = "database-memory-agent"
version = "0.1.0"
description = "Database Memory Agent with RAG capabilities using MongoDB Atlas Vector Search, Voyage AI, and OpenAI"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"pymongo",
"voyageai",
"openai",
"langchain",
"langchain-mongodb",
"langchain-community",
"python-dotenv",
"streamlit",
"pypdf",
]
+39
View File
@@ -0,0 +1,39 @@
from config import vector_collection
from ingest_data import get_embedding
def vector_search_tool(user_input: str) -> str:
query_embedding = get_embedding(user_input)
pipeline = [
{
"$vectorSearch": {
"index": "vector_index",
"queryVector": query_embedding,
"path": "embedding",
"exact": True,
"limit": 5,
}
},
{
"$project": {
"_id": 0,
"text": 1,
}
},
]
results = vector_collection.aggregate(pipeline)
array_of_results = []
for doc in results:
array_of_results.append(doc)
return array_of_results
def calculator_tool(user_input: str) -> str:
try:
result = eval(user_input)
return str(result)
except Exception as e:
return f"Error: {str(e)}"
+3203
View File
File diff suppressed because it is too large Load Diff