From 3dc33817ba126668223db679998a7bbf69c56456 Mon Sep 17 00:00:00 2001 From: Vasilije <8619304+Vasilije1990@users.noreply.github.com> Date: Sun, 29 Oct 2023 19:11:20 +0100 Subject: [PATCH] Added docs functionality --- README.md | 131 +++++++++++++++----- level_3/.env.template | 8 +- level_3/Dockerfile | 1 + level_3/api.py | 98 +++++++++++++-- level_3/create_database.py | 80 ++++++++++++ level_3/database/database.py | 14 ++- level_3/docker-compose.yml | 11 ++ level_3/entrypoint.sh | 15 ++- level_3/models/docs.py | 3 +- level_3/models/operation.py | 2 + level_3/models/testoutput.py | 1 + level_3/poetry.lock | 8 +- level_3/rag_test_manager.py | 113 +++++++++-------- level_3/scripts/create_database.py | 1 + level_3/vectordb/basevectordb.py | 6 + level_3/vectordb/loaders/loaders.py | 72 +++++++---- level_3/vectordb/vectordb.py | 26 +++- level_3/wait-for-it.sh | 182 ++++++++++++++++++++++++++++ 18 files changed, 652 insertions(+), 120 deletions(-) create mode 100644 level_3/create_database.py create mode 100644 level_3/wait-for-it.sh diff --git a/README.md b/README.md index 2af3e4704..b7b1caaa4 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # PromethAI-Memory -Memory management and testing for the AI Applications and RAGs -Dynamic Graph Memory Manager + DB + Rag Test Manager +AI Applications and RAGs - Cognitive Architecture, Testability, Production Ready Apps -
+ +
-Open-source framework that manages memory for AI Agents and LLM apps
+Open-source framework for building and testing RAGs and Cognitive Architectures, designed for accuracy, transparency, and control.
-+
@@ -52,9 +52,9 @@ Dynamic Graph Memory Manager + DB + Rag Test Manager
[//]: # (
Share promethAI Repository
+Share promethAI Repository
-+
@@ -71,33 +71,40 @@ Dynamic Graph Memory Manager + DB + Rag Test Manager
-
+
+This repo is built to test and evolve RAG architecture, inspired by human cognitive processes, using Python. It's aims to be production ready, testable, but give great visibility in how we build RAG applications.
+
+This project is a part of the [PromethAI](https://prometh.ai/) ecosystem.
+
+It runs in iterations, with each iteration building on the previous one.
+
+_Keep Ithaka always in your mind.
+Arriving there is what you’re destined for.
+But don’t hurry the journey at all.
+Better if it lasts for years_
-## Production-ready modern data platform
+### Installation
+To get started with PromethAI Memory, start with the latest iteration, and follow the instructions in the README.md file
-Browsing the database of theresanaiforthat.com, we can observe around [7000 new, mostly semi-finished projects](https://theresanaiforthat.com/) in the field of applied AI.
-It seems it has never been easier to create a startup, build an app, and go to market… and fail.
+### Current Focus
-Decades of technological advancements have led to small teams being able to do in 2023 what in 2015 required a team of dozens.
-Yet, the AI apps currently being pushed out still mostly feel and perform like demos.
-The rise of this new profession is perhaps signaling the need for a solution that is not yet there — a solution that in its essence represents a Large Language Model (LLM) — [a powerful general problem solver](https://lilianweng.github.io/posts/2023-06-23-agent/?fbclid=IwAR1p0W-Mg_4WtjOCeE8E6s7pJZlTDCDLmcXqHYVIrEVisz_D_S8LfN6Vv20) — available in the palm of your hand 24/7/365.
+RAG test manager can be used via API or via the CLI
-To address this issue, [dlthub](https://dlthub.com/) and [prometh.ai](http://prometh.ai/) will collaborate on a productionizing a common use-case, progressing step by step. We will utilize the LLMs, frameworks, and services, refining the code until we attain a clearer understanding of what a modern LLM architecture stack might entail.
+
-## Read more on our blog post [prometh.ai](http://prometh.ai/promethai-memory-blog-post-on)
+### Project Structure
-
-## Project Structure
-
-### Level 1 - OpenAI functions + Pydantic + DLTHub
+#### Level 1 - OpenAI functions + Pydantic + DLTHub
Scope: Give PDFs to the model and get the output in a structured format
+Blog post: https://prometh.ai/promethai-memory-blog-post-one
We introduce the following concepts:
- Structured output with Pydantic
- CMD script to process custom PDFs
-### Level 2 - Memory Manager + Metadata management
+#### Level 2 - Memory Manager + Metadata management
Scope: Give PDFs to the model and consolidate with the previous user activity and more
+Blog post: https://prometh.ai/promethai-memory-blog-post-two
We introduce the following concepts:
- Long Term Memory -> store and format the data
@@ -106,8 +113,9 @@ We introduce the following concepts:
- Docker
- API
-### Level 3 - Dynamic Graph Memory Manager + DB + Rag Test Manager
+#### Level 3 - Dynamic Graph Memory Manager + DB + Rag Test Manager
Scope: Store the data in N-related stores and test the retrieval with the Rag Test Manager
+Blog post: https://prometh.ai/promethai-memory-blog-post-three
- Dynamic Memory Manager -> store the data in N hierarchical stores
- Auto-generation of tests
- Multiple file formats supported
@@ -116,26 +124,92 @@ Scope: Store the data in N-related stores and test the retrieval with the Rag Te
- API
-## Run the level 3
+### Run the level 3
Make sure you have Docker, Poetry, and Python 3.11 installed and postgres installed.
-Copy the .env.example to .env and fill the variables
+Copy the .env.example to .env and fill in the variables
-Start the docker:
+Two ways to run the level 3:
-```docker compose up promethai_mem ```
+#### Docker:
+
+Copy the .env.template to .env and fill in the variables
+Specify the environment variable in the .env file to "docker"
+
+
+Launch the docker image:
+
+```docker compose up promethai_mem ```
+
+Send the request to the API:
+
+```
+curl -X POST -H "Content-Type: application/json" -d '{
+ "payload": {
+ "user_id": "681",
+ "data": [".data/3ZCCCW.pdf"],
+ "test_set": "sample",
+ "params": ["chunk_size"],
+ "metadata": "sample",
+ "retriever_type": "single_document_context"
+ }
+}' http://0.0.0.0:8000/rag-test/rag_test_run
+
+```
+Params:
+
+data -> list of URLs or path to the file, located in the .data folder (pdf, docx, txt, html)
+test_set -> sample, manual (list of questions and answers)
+metadata -> sample, manual (json) or version (in progress)
+params -> chunk_size, chunk_overlap, search_type (hybrid, bm25), embeddings
+retriever_type -> llm_context, single_document_context, multi_document_context, cognitive_architecture(coming soon)
+
+Inspect the results in the DB:
+
+``` docker exec -it postgres psql -U bla ```
+
+``` \c bubu ```
+
+``` select * from test_outputs; ```
+
+Or set up the superset to visualize the results:
+
+
+
+#### Poetry environment:
+
+
+Copy the .env.template to .env and fill in the variables
+Specify the environment variable in the .env file to "local"
Use the poetry environment:
``` poetry shell ```
+Change the .env file Environment variable to "local"
+
+Launch the postgres DB
+
+``` docker compose up postgres ```
+
+Launch the superset
+
+``` docker compose up superset ```
+
+Open the superset in your browser
+
+``` http://localhost:8088 ```
+Add the Postgres datasource to the Superset with the following connection string:
+
+``` postgres://bla:bla@postgres:5432/bubu ```
+
Make sure to run to initialize DB tables
``` python scripts/create_database.py ```
-After that, you can run the RAG test manager.
+After that, you can run the RAG test manager from your command line.
```
@@ -149,3 +223,4 @@ After that, you can run the RAG test manager.
```
Examples of metadata structure and test set are in the folder "example_data"
+
diff --git a/level_3/.env.template b/level_3/.env.template
index bc9893356..8ca7daed0 100644
--- a/level_3/.env.template
+++ b/level_3/.env.template
@@ -1,3 +1,9 @@
OPENAI_API_KEY=sk
WEAVIATE_URL =
-WEAVIATE_API_KEY =
\ No newline at end of file
+WEAVIATE_API_KEY =
+ENVIRONMENT = docker
+POSTGRES_USER = bla
+POSTGRES_PASSWORD = bla
+POSTGRES_DB = bubu
+POSTGRES_HOST = localhost
+POSTGRES_HOST_DOCKER = postgres
\ No newline at end of file
diff --git a/level_3/Dockerfile b/level_3/Dockerfile
index f2b0a3efa..feb677fe3 100644
--- a/level_3/Dockerfile
+++ b/level_3/Dockerfile
@@ -43,6 +43,7 @@ RUN apt-get update -q && \
WORKDIR /app
COPY . /app
+COPY scripts/ /app
COPY entrypoint.sh /app/entrypoint.sh
COPY scripts/create_database.py /app/create_database.py
RUN chmod +x /app/entrypoint.sh
diff --git a/level_3/api.py b/level_3/api.py
index 953150d61..aa200d56a 100644
--- a/level_3/api.py
+++ b/level_3/api.py
@@ -1,9 +1,11 @@
+import json
import logging
import os
+from enum import Enum
from typing import Dict, Any
import uvicorn
-from fastapi import FastAPI
+from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import JSONResponse
from pydantic import BaseModel
@@ -11,6 +13,7 @@ from database.database import AsyncSessionLocal
from database.database_crud import session_scope
from vectorstore_manager import Memory
from dotenv import load_dotenv
+from rag_test_manager import start_test
# Set up logging
logging.basicConfig(
@@ -200,25 +203,100 @@ def memory_factory(memory_type):
memory_list = ["episodic", "buffer", "semantic"]
for memory_type in memory_list:
memory_factory(memory_type)
+class TestSetType(Enum):
+ SAMPLE = "sample"
+ MANUAL = "manual"
+def get_test_set(test_set_type, folder_path="example_data", payload=None):
+ if test_set_type == TestSetType.SAMPLE:
+ file_path = os.path.join(folder_path, "test_set.json")
+ if os.path.isfile(file_path):
+ with open(file_path, "r") as file:
+ return json.load(file)
+ elif test_set_type == TestSetType.MANUAL:
+ # Check if the manual test set is provided in the payload
+ if payload and "manual_test_set" in payload:
+ return payload["manual_test_set"]
+ else:
+ # Attempt to load the manual test set from a file
+ pass
+
+ return None
+
+
+class MetadataType(Enum):
+ SAMPLE = "sample"
+ MANUAL = "manual"
+
+def get_metadata(metadata_type, folder_path="example_data", payload=None):
+ if metadata_type == MetadataType.SAMPLE:
+ file_path = os.path.join(folder_path, "metadata.json")
+ if os.path.isfile(file_path):
+ with open(file_path, "r") as file:
+ return json.load(file)
+ elif metadata_type == MetadataType.MANUAL:
+ # Check if the manual metadata is provided in the payload
+ if payload and "manual_metadata" in payload:
+ return payload["manual_metadata"]
+ else:
+ pass
+
+ return None
@app.post("/rag-test/rag_test_run", response_model=dict)
async def rag_test_run(
payload: Payload,
- # files: List[UploadFile] = File(...),
+ background_tasks: BackgroundTasks,
):
try:
- from rag_test_manager import start_test
- logging.info(" Running RAG Test ")
+ logging.info("Starting RAG Test")
decoded_payload = payload.payload
- output = await start_test(data=decoded_payload['data'], test_set=decoded_payload['test_set'], user_id=decoded_payload['user_id'], params=decoded_payload['params'], metadata=decoded_payload['metadata'],
- retriever_type=decoded_payload['retriever_type'])
- return JSONResponse(content={"response": output}, status_code=200)
- except Exception as e:
- return JSONResponse(
- content={"response": {"error": str(e)}}, status_code=503
+ test_set_type = TestSetType(decoded_payload['test_set'])
+
+ metadata_type = MetadataType(decoded_payload['metadata'])
+
+ metadata = get_metadata(metadata_type, payload=decoded_payload)
+ if metadata is None:
+ return JSONResponse(content={"response": "Invalid metadata value"}, status_code=400)
+
+ test_set = get_test_set(test_set_type, payload=decoded_payload)
+ if test_set is None:
+ return JSONResponse(content={"response": "Invalid test_set value"}, status_code=400)
+
+ async def run_start_test(data, test_set, user_id, params, metadata, retriever_type):
+ result = await start_test(data = data, test_set = test_set, user_id =user_id, params =params, metadata =metadata, retriever_type=retriever_type)
+
+ logging.info("Retriever DATA type", type(decoded_payload['data']))
+
+ background_tasks.add_task(
+ run_start_test,
+ decoded_payload['data'],
+ test_set,
+ decoded_payload['user_id'],
+ decoded_payload['params'],
+ metadata,
+ decoded_payload['retriever_type']
)
+ logging.info("Retriever type", decoded_payload['retriever_type'])
+ return JSONResponse(content={"response": "Task has been started"}, status_code=200)
+
+ except Exception as e:
+ return JSONResponse(
+
+ content={"response": {"error": str(e)}}, status_code=503
+
+ )
+
+
+# @app.get("/rag-test/{task_id}")
+# async def check_task_status(task_id: int):
+# task_status = task_status_db.get(task_id, "not_found")
+#
+# if task_status == "not_found":
+# return {"status": "Task not found"}
+#
+# return {"status": task_status}
# @app.get("/available-buffer-actions", response_model=dict)
# async def available_buffer_actions(
diff --git a/level_3/create_database.py b/level_3/create_database.py
new file mode 100644
index 000000000..3d8ff426d
--- /dev/null
+++ b/level_3/create_database.py
@@ -0,0 +1,80 @@
+import sys
+import os
+
+# this is needed to import classes from other modules
+script_dir = os.path.dirname(os.path.abspath(__file__))
+# Get the parent directory of your script and add it to sys.path
+parent_dir = os.path.dirname(script_dir)
+sys.path.append(parent_dir)
+
+from database.database import Base, engine
+import models.memory
+import models.metadatas
+import models.operation
+import models.sessions
+import models.testoutput
+import models.testset
+import models.user
+import models.docs
+from sqlalchemy import create_engine, text
+import psycopg2
+from dotenv import load_dotenv
+load_dotenv()
+import os
+
+
+
+
+
+
+def create_admin_engine(username, password, host, database_name):
+ admin_url = f"postgresql://{username}:{password}@{host}:5432/{database_name}"
+ return create_engine(admin_url)
+
+
+def database_exists(username, password, host, db_name):
+ engine = create_admin_engine(username, password, host, db_name)
+ connection = engine.connect()
+ query = text(f"SELECT 1 FROM pg_database WHERE datname='{db_name}'")
+ result = connection.execute(query).fetchone()
+ connection.close()
+ engine.dispose()
+ return result is not None
+
+
+def create_database(username, password, host, db_name):
+ engine = create_admin_engine(username, password, host, db_name)
+ connection = engine.raw_connection()
+ connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
+ cursor = connection.cursor()
+ cursor.execute(f"CREATE DATABASE {db_name}")
+ cursor.close()
+ connection.close()
+ engine.dispose()
+
+
+def create_tables(engine):
+ Base.metadata.create_all(bind=engine)
+
+if __name__ == "__main__":
+ username = os.getenv('POSTGRES_USER')
+ password = os.getenv('POSTGRES_PASSWORD')
+ database_name = os.getenv('POSTGRES_DB')
+ environment = os.environ.get("ENVIRONMENT")
+
+ if environment == "local":
+ host = os.getenv('POSTGRES_HOST')
+
+ elif environment == "docker":
+ host = os.getenv('POSTGRES_HOST_DOCKER')
+ else:
+ host = os.getenv('POSTGRES_HOST_DOCKER')
+
+ engine = create_admin_engine(username, password, host, database_name)
+
+ if not database_exists(username, password, host, database_name):
+ print(f"Database {database_name} does not exist. Creating...")
+ create_database(username, password, host, database_name)
+ print(f"Database {database_name} created successfully.")
+
+ create_tables(engine)
\ No newline at end of file
diff --git a/level_3/database/database.py b/level_3/database/database.py
index 122efb64e..b100f5c8c 100644
--- a/level_3/database/database.py
+++ b/level_3/database/database.py
@@ -24,7 +24,19 @@ RETRY_DELAY = 5
username = os.getenv('POSTGRES_USER')
password = os.getenv('POSTGRES_PASSWORD')
database_name = os.getenv('POSTGRES_DB')
-host = os.getenv('POSTGRES_HOST')
+import os
+
+environment = os.environ.get("ENVIRONMENT")
+
+if environment == "local":
+ host= os.getenv('POSTGRES_HOST')
+
+elif environment == "docker":
+ host= os.getenv('POSTGRES_HOST_DOCKER')
+else:
+ host= os.getenv('POSTGRES_HOST_DOCKER')
+
+
# Use the asyncpg driver for async operation
SQLALCHEMY_DATABASE_URL = f"postgresql+asyncpg://{username}:{password}@{host}:5432/{database_name}"
diff --git a/level_3/docker-compose.yml b/level_3/docker-compose.yml
index 8e7e56698..97564a17e 100644
--- a/level_3/docker-compose.yml
+++ b/level_3/docker-compose.yml
@@ -18,14 +18,25 @@ services:
- promethai_mem_backend
build:
context: ./
+
volumes:
- "./:/app"
+ - ./.data:/app/.data
+
environment:
- HOST=0.0.0.0
profiles: ["exclude-from-up"]
ports:
- 8000:8000
- 443:443
+ depends_on:
+ - postgres
+ deploy:
+ resources:
+ limits:
+ cpus: "4.0"
+ memory: 8GB
+
postgres:
image: postgres
diff --git a/level_3/entrypoint.sh b/level_3/entrypoint.sh
index 5a87898d6..4cf551d01 100755
--- a/level_3/entrypoint.sh
+++ b/level_3/entrypoint.sh
@@ -1,7 +1,20 @@
#!/bin/bash
export ENVIRONMENT
+# Run Python scripts with error handling
+echo "Running fetch_secret.py"
python fetch_secret.py
+if [ $? -ne 0 ]; then
+ echo "Error: fetch_secret.py failed"
+ exit 1
+fi
+
+echo "Running create_database.py"
python create_database.py
+if [ $? -ne 0 ]; then
+ echo "Error: create_database.py failed"
+ exit 1
+fi
# Start Gunicorn
-gunicorn -w 2 -k uvicorn.workers.UvicornWorker -t 120 --bind=0.0.0.0:8000 --bind=0.0.0.0:443 --log-level debug api:app
\ No newline at end of file
+echo "Starting Gunicorn"
+gunicorn -w 3 -k uvicorn.workers.UvicornWorker -t 30000 --bind=0.0.0.0:8000 --bind=0.0.0.0:443 --log-level debug api:app
diff --git a/level_3/models/docs.py b/level_3/models/docs.py
index 95d98c485..38166956b 100644
--- a/level_3/models/docs.py
+++ b/level_3/models/docs.py
@@ -15,4 +15,5 @@ class DocsModel(Base):
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, onupdate=datetime.utcnow)
- operation = relationship("Operation", back_populates="docs")
+
+ operations = relationship("Operation", back_populates="docs")
\ No newline at end of file
diff --git a/level_3/models/operation.py b/level_3/models/operation.py
index 595745b15..1d06657d9 100644
--- a/level_3/models/operation.py
+++ b/level_3/models/operation.py
@@ -14,6 +14,7 @@ class Operation(Base):
id = Column(String, primary_key=True)
user_id = Column(String, ForeignKey('users.id'), index=True) # Link to User
operation_type = Column(String, nullable=True)
+ operation_status = Column(String, nullable=True)
operation_params = Column(String, nullable=True)
number_of_files = Column(Integer, nullable=True)
test_set_id = Column(String, ForeignKey('test_sets.id'), index=True)
@@ -24,6 +25,7 @@ class Operation(Base):
# Relationships
user = relationship("User", back_populates="operations")
test_set = relationship("TestSet", back_populates="operations")
+ docs = relationship("DocsModel", back_populates="operations")
def __repr__(self):
return f"