From f355eec81ab7c258212a564a7e69bcc229f5c509 Mon Sep 17 00:00:00 2001 From: neal logan Date: Sat, 23 Nov 2024 20:02:05 -0500 Subject: [PATCH] Made changes to chat app to enable docker deployment --- MinuteMate/README.md | 19 ++ MinuteMate/back/.env.example | 6 + MinuteMate/back/Dockerfile | 13 +- MinuteMate/back/docker-compose.yml | 11 + MinuteMate/back/main.py | 391 +++++++++++++++------------- MinuteMate/back/main02.py | 242 ----------------- MinuteMate/back/main_fn.py | 199 ++++++++++++++ MinuteMate/back/methods/__init__.py | 0 MinuteMate/back/requirements.txt | 10 +- MinuteMate/front/Dockerfile | 11 +- MinuteMate/front/app.py | 36 +-- MinuteMate/front/docker-compose.yml | 7 + MinuteMate/front/requirements.txt | 8 +- 13 files changed, 484 insertions(+), 469 deletions(-) create mode 100644 MinuteMate/back/.env.example create mode 100644 MinuteMate/back/docker-compose.yml delete mode 100644 MinuteMate/back/main02.py create mode 100644 MinuteMate/back/main_fn.py delete mode 100644 MinuteMate/back/methods/__init__.py create mode 100644 MinuteMate/front/docker-compose.yml diff --git a/MinuteMate/README.md b/MinuteMate/README.md index d4c463c8..a888b313 100644 --- a/MinuteMate/README.md +++ b/MinuteMate/README.md @@ -15,6 +15,25 @@ - **Response Generation** - Generates a response, typically by sending a request to a generative model with the original user prompt and a system prompt including selected resources acquired from the vector database. - **Trust & Safety** - Two separate stages, each using an external generative model. The first stage examines only the incoming prompt. If an inappropriate prompt is detected at this stage, all ordinary steps are skipped and a response is sent declining the prompt. The second examines generated responses and vetoes those with inappropriate content. +##### Setup - Docker + + + +In MinuteMate/back: + +Set up secrets in a .env file according to .env.example + +```bash +docker compose --env-file .env up -d --build +``` + +In MinuteMate/front: + +```bash +docker compose up -d --build +``` + + ##### Setup - Repo **Front-End diff --git a/MinuteMate/back/.env.example b/MinuteMate/back/.env.example new file mode 100644 index 00000000..7484b918 --- /dev/null +++ b/MinuteMate/back/.env.example @@ -0,0 +1,6 @@ +#OPENAI API KEY +OPENAI_API_KEY = + +# Weaviate cloud deployment +WEAVIATE_URL = +WEAVIATE_API_KEY = \ No newline at end of file diff --git a/MinuteMate/back/Dockerfile b/MinuteMate/back/Dockerfile index ecc035d7..152c893e 100644 --- a/MinuteMate/back/Dockerfile +++ b/MinuteMate/back/Dockerfile @@ -1,6 +1,7 @@ -FROM python:3.11-slim -WORKDIR /App -COPY . /App -RUN python -m pip install -r requirements.txt -EXPOSE 8000 -CMD [" ", "start","--port","8002","--host","0.0.0.0"] +FROM python:3.11 +WORKDIR /app +COPY . /app +RUN pip install --upgrade pip +RUN pip install -r requirements.txt +EXPOSE 8001 +CMD ["uvicorn", "main:app","--host","0.0.0.0","--port","8001"] \ No newline at end of file diff --git a/MinuteMate/back/docker-compose.yml b/MinuteMate/back/docker-compose.yml new file mode 100644 index 00000000..0b7a5c28 --- /dev/null +++ b/MinuteMate/back/docker-compose.yml @@ -0,0 +1,11 @@ +services: + chat_backend: + build: + context: ./ + dockerfile: Dockerfile + ports: + - 8001:8001 + environment: + - WEAVIATE_ENDPOINT_URL=$WEAVIATE_ENDPOINT_URL + - WEAVIATE_API_KEY=$WEAVIATE_API_KEY + - OPENAI_API_KEY=$OPENAI_API_KEY \ No newline at end of file diff --git a/MinuteMate/back/main.py b/MinuteMate/back/main.py index 43bb144a..f87caef7 100644 --- a/MinuteMate/back/main.py +++ b/MinuteMate/back/main.py @@ -1,199 +1,242 @@ -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel - import os +import logging +from typing import Optional, List -from rake_nltk import Rake +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field import weaviate from weaviate.classes.init import Auth -from weaviate.classes.init import AdditionalConfig -from weaviate.classes.init import Timeout -from weaviate.classes.query import Rerank -from weaviate.classes.query import MetadataQuery +from weaviate.classes.query import Rerank, MetadataQuery import openai from openai import OpenAI +from rake_nltk import Rake +from dotenv import load_dotenv + +import nltk +import ssl + +try: + _create_unverified_https_context = ssl._create_unverified_context +except AttributeError: + pass +else: + ssl._create_default_https_context = _create_unverified_https_context + + +try: + nltk.download('punkt') + nltk.download('punkt_tab') + nltk.download('stopwords') +except Exception as e: + print(f"Error downloading NLTK resources: {e}") + + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +load_dotenv() + + # Initialize the FastAPI app app = FastAPI( - title="MinuteMate Propmpt & Response API", - description="A simple API that takes a text prompt uses ", - version="1.0.0" + title="MinuteMate Prompt & Response API", + description="An AI-powered API for processing meeting-related prompts", + version="1.0.0" +) +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers ) -# Define the request schema +# Define request and response models class PromptRequest(BaseModel): - user_prompt_text: str + user_prompt_text: str = Field(..., min_length=1, max_length=1000) + +class ContextSegment(BaseModel): + chunk_id: int + content: str + score: Optional[float] = None -# Define the response schema class PromptResponse(BaseModel): generated_response: str - error_code : int - -# Takes a prompt from the front end, processes the prompt -# using NLP tools, embedding services, and generative services -# and finally returns the prompt response -def process_prompt(prompt_request: PromptRequest) -> PromptResponse: - - ### 0 - ENVIRONMENT AND CONFIGURATION ### - - # Update environment variables - # not sure if this works - # TODO test and/or look for alternatives - import os - - # Set API keys, endpoint URLs, model versions, and configurations - # Embedding and Generative Models - OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') - # OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') - OPENAI_EMBEDDING_URL = os.getenv('OPENAI_EMBEDDING_URL') - OPENAI_GENERATION_URL = os.getenv('OPENAI_GENERATION_URL') - EMBEDDING_MODEL = 'text-embedding-3-small' - ENCODING_FORMAT = 'float' - RESPONDING_GENERATIVE_MODEL = 'gpt-4o' - # TRUSTSAFETY_GENERATIVE_MODEL = llama on modal, probably, but can't be too - - # API key, endpoint URL, and target collection(s) - # for Weaviate vector database - WEAVIATE_URL = os.environ['WEAVIATE_URL'] - WEAVIATE_API_KEY = os.environ['WEAVIATE_API_KEY'] - WEAVIATE_TARGET_COLLECTION = 'MeetingDocument' - # WEAVIATE_TARGET_COLLECTION = "VERBA_Embedding_text_embedding_3_small" - - - - ### 1- INITIAL TRUST AND SAFETY CHECK ### - # TODO add initial trust & safety check here - # If trust and safety check fails, return the error immediately - - - - ### 2- INFORMATION RETRIEVAL ### - - # Set RAG search type - SEARCH_TYPE = 'keyword' - # SEARCH_TYPE = 'vector' - # SEARCH_TYPE = 'hybrid' - - # Establish connection with Weaviate server - # https://weaviate.io/developers/weaviate - weaviate_client = weaviate.connect_to_weaviate_cloud( - cluster_url=WEAVIATE_URL, - auth_credentials=Auth.api_key(WEAVIATE_API_KEY), - ) - # Look up the appropriate Weviate database collection - db_collection = weaviate_client.collections.get(WEAVIATE_TARGET_COLLECTION) - db_response = None - - # Extract keywords and query database - # TODO - finish and test - if(SEARCH_TYPE == 'keyword'): + context_segments: List[ContextSegment] = [] + keywords: List[str] = [] + error_code: int = 0 + +class WeaviateConfig: + """Configuration for Weaviate connection and querying""" + SEARCH_TYPES = { + 'keyword': 'bm25', + 'vector': 'near_vector', + 'hybrid': 'hybrid' + } + + @classmethod + def get_weaviate_client(cls, url: str, api_key: str): + """Establish Weaviate connection""" + try: + return weaviate.connect_to_weaviate_cloud( + cluster_url=url, + auth_credentials=Auth.api_key(api_key), + additional_config=weaviate.classes.init.AdditionalConfig( + timeout=weaviate.classes.init.Timeout(init=10, query=30) + ) + ) + except Exception as e: + logger.error(f"Weaviate connection error: {e}") + raise + +class PromptProcessor: + """Main class for processing user prompts""" + def __init__(self): + # Load environment variables + self.load_env_vars() - rake = Rake() - rake.extract_keywords_from_text(prompt_request.user_prompt_text) - keywords = rake.get_ranked_phrases()[:3] - db_response = db_collection.query.bm25( - query=",".join(keywords), - limit=5, - # rerank=Rerank( - # prop="content", - # query="meeting" - # ), - # return_metadata=MetadataQuery(score=True) + # Initialize clients + self.weaviate_client = WeaviateConfig.get_weaviate_client( + self.WEAVIATE_URL, + self.WEAVIATE_API_KEY ) - - # Vectorize the prompt and query the database - # TODO - test - elif(SEARCH_TYPE == 'vector'): + self.openai_client = OpenAI(api_key=self.OPENAI_API_KEY) + + def load_env_vars(self): + """Load and validate environment variables""" + required_vars = [ + 'OPENAI_API_KEY', + 'WEAVIATE_URL', + 'WEAVIATE_API_KEY' + ] + for var in required_vars: + value = os.getenv(var) + print(f"Loading {var}: {value}") + if not value: + raise ValueError(f"Missing environment variable: {var}") + setattr(self, var, value) + + def extract_keywords(self, text: str) -> List[str]: + """Extract keywords using RAKE""" + try: - # Set API Key. Not necessary if you have an - # OPENAI_API_KEY variable in your environment - openai.api_key = OPENAI_API_KEY - embedding_client = OpenAI() - - # Vector-embed the prompt - embedding_response = embedding_client.embeddings.create( - model = EMBEDDING_MODEL, - input = prompt_request.user_prompt_text, - encoding_format = ENCODING_FORMAT - ) - - # Extract the vector embeddings list[float] from the embedding response - query_vector = embedding_response.data[0].embedding - - # Send vector query to database and get response - db_response = db_collection.query.near_vector( - near_vector=query_vector, - limit=10, - return_metadata=MetadataQuery(distance=True) - ) - - #TODO support this - #elif(SEARCH_TYPE == 'hybrid'): - - - else: - #No RAG search - db_response = None - - # Extract items from database response - # and aggregate into a single string - db_response_text = "" - for item in db_response.objects: - segment = '\n\n' - db_response_text += segment - db_response_text += item.properties.get('content') - - - ### 3 - RESPONSE GENERATION ### - - # Generate response to user with OpenAI generative model - # https://platform.openai.com/docs/api-reference/chat/create - openai.api_key = OPENAI_API_KEY - generation_client = OpenAI() - generated_response_text = generation_client.chat.completions.create( - model="gpt-4o", - messages=[ - { - "role": "system", - "content": f"You are a helpful assistant who uses this context if appropriate: {db_response_text}" - }, - { - "role": "user", - "content": prompt_request.user_prompt_text - } - ] - ) - - ### 4 - FINAL TRUST AND SAFETY CHECK ### - # TODO add final trust & safety check here - # If trust and safety check fails, return an error - - - ### 5 - BUILD & RETURN RESPONSE OBJECT ### - # Return chat response to API layer - # to be passed along to frontend - prompt_response = PromptResponse() - prompt_response.generated_response = generated_response_text - return prompt_request - - - - - -# API endpoint + rake = Rake() + rake.extract_keywords_from_text(text) + return rake.get_ranked_phrases()[:3] + except Exception as e: + logger.error(f"Keyword extraction error: {e}") + return [] + + def search_weaviate(self, query: str, search_type: str = 'keyword') -> List[ContextSegment]: + """Perform search in Weaviate database""" + try: + collection = self.weaviate_client.collections.get('MeetingDocument') + + if search_type == 'keyword': + keywords = self.extract_keywords(query) + results = collection.query.bm25( + query=",".join(keywords), + limit=5 + ) + print(keywords) + elif search_type == 'vector': + embedding = self.openai_client.embeddings.create( + model='text-embedding-3-small', + input=query + ).data[0].embedding + + results = collection.query.near_vector( + near_vector=embedding, + limit=5 + ) + else: + raise ValueError(f"Unsupported search type: {search_type}") + + context_segments = [ + ContextSegment( + chunk_id=int(item.properties.get('chunk_id', 0)), + content=item.properties.get('content', ''), + score=getattr(item.metadata, 'distance', None) + ) for item in results.objects + ] + return context_segments, keywords + except Exception as e: + logger.error(f"Weaviate search error: {e}") + return [] + + def generate_response(self, prompt: str, context_segments: List[ContextSegment]) -> str: + """Generate response using OpenAI""" + context_text = "\n".join([ + f"\n{seg.content}" + for seg in context_segments + ]) + + try: + response = self.openai_client.chat.completions.create( + model="gpt-4o", + messages=[ + { + "role": "system", + "content": f"Use this context if relevant: {context_text}" + }, + { + "role": "user", + "content": prompt + } + ] + ) + return response.choices[0].message.content + except Exception as e: + logger.error(f"OpenAI generation error: {e}") + return "I'm sorry, but I couldn't generate a response." + + def process_prompt(self, prompt_request: PromptRequest) -> PromptResponse: + """Main method to process user prompt""" + try: + # Search for relevant context + context_segments, keywords = self.search_weaviate(prompt_request.user_prompt_text) + + # Generate response + generated_response = self.generate_response( + prompt_request.user_prompt_text, + context_segments + ) + + return PromptResponse( + generated_response=generated_response, + context_segments=context_segments, + keywords = keywords, + error_code=0 + ) + + except Exception as e: + logger.error(f"Prompt processing error: {e}") + return PromptResponse( + generated_response="An error occurred while processing your request.", + error_code=500 + ) + +# Initialize processor +processor = PromptProcessor() + +# API Endpoint @app.post("/process-prompt", response_model=PromptResponse) async def process_prompt_endpoint(prompt_request: PromptRequest): - """ - Process the prompt and return the response - """ - try: - prompt_response = process_prompt(prompt_request) - return PromptResponse(result=prompt_response) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - \ No newline at end of file + """Process user prompt and return response""" + return processor.process_prompt(prompt_request) + + +# Cleanup on shutdown +@app.on_event("shutdown") +async def shutdown_event(): + """Close Weaviate connection on app shutdown""" + processor.weaviate_client.close() \ No newline at end of file diff --git a/MinuteMate/back/main02.py b/MinuteMate/back/main02.py deleted file mode 100644 index f87caef7..00000000 --- a/MinuteMate/back/main02.py +++ /dev/null @@ -1,242 +0,0 @@ -import os -import logging -from typing import Optional, List - -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, Field - -import weaviate -from weaviate.classes.init import Auth -from weaviate.classes.query import Rerank, MetadataQuery - -import openai -from openai import OpenAI - - -from rake_nltk import Rake -from dotenv import load_dotenv - -import nltk -import ssl - -try: - _create_unverified_https_context = ssl._create_unverified_context -except AttributeError: - pass -else: - ssl._create_default_https_context = _create_unverified_https_context - - -try: - nltk.download('punkt') - nltk.download('punkt_tab') - nltk.download('stopwords') -except Exception as e: - print(f"Error downloading NLTK resources: {e}") - - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -load_dotenv() - - -# Initialize the FastAPI app -app = FastAPI( - title="MinuteMate Prompt & Response API", - description="An AI-powered API for processing meeting-related prompts", - version="1.0.0" -) - -# Add CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Allows all origins - allow_credentials=True, - allow_methods=["*"], # Allows all methods - allow_headers=["*"], # Allows all headers -) - -# Define request and response models -class PromptRequest(BaseModel): - user_prompt_text: str = Field(..., min_length=1, max_length=1000) - -class ContextSegment(BaseModel): - chunk_id: int - content: str - score: Optional[float] = None - -class PromptResponse(BaseModel): - generated_response: str - context_segments: List[ContextSegment] = [] - keywords: List[str] = [] - error_code: int = 0 - -class WeaviateConfig: - """Configuration for Weaviate connection and querying""" - SEARCH_TYPES = { - 'keyword': 'bm25', - 'vector': 'near_vector', - 'hybrid': 'hybrid' - } - - @classmethod - def get_weaviate_client(cls, url: str, api_key: str): - """Establish Weaviate connection""" - try: - return weaviate.connect_to_weaviate_cloud( - cluster_url=url, - auth_credentials=Auth.api_key(api_key), - additional_config=weaviate.classes.init.AdditionalConfig( - timeout=weaviate.classes.init.Timeout(init=10, query=30) - ) - ) - except Exception as e: - logger.error(f"Weaviate connection error: {e}") - raise - -class PromptProcessor: - """Main class for processing user prompts""" - def __init__(self): - # Load environment variables - self.load_env_vars() - - # Initialize clients - self.weaviate_client = WeaviateConfig.get_weaviate_client( - self.WEAVIATE_URL, - self.WEAVIATE_API_KEY - ) - self.openai_client = OpenAI(api_key=self.OPENAI_API_KEY) - - def load_env_vars(self): - """Load and validate environment variables""" - required_vars = [ - 'OPENAI_API_KEY', - 'WEAVIATE_URL', - 'WEAVIATE_API_KEY' - ] - - for var in required_vars: - value = os.getenv(var) - print(f"Loading {var}: {value}") - if not value: - raise ValueError(f"Missing environment variable: {var}") - setattr(self, var, value) - - def extract_keywords(self, text: str) -> List[str]: - """Extract keywords using RAKE""" - try: - - rake = Rake() - rake.extract_keywords_from_text(text) - return rake.get_ranked_phrases()[:3] - except Exception as e: - logger.error(f"Keyword extraction error: {e}") - return [] - - def search_weaviate(self, query: str, search_type: str = 'keyword') -> List[ContextSegment]: - """Perform search in Weaviate database""" - try: - collection = self.weaviate_client.collections.get('MeetingDocument') - - if search_type == 'keyword': - keywords = self.extract_keywords(query) - results = collection.query.bm25( - query=",".join(keywords), - limit=5 - ) - print(keywords) - elif search_type == 'vector': - embedding = self.openai_client.embeddings.create( - model='text-embedding-3-small', - input=query - ).data[0].embedding - - results = collection.query.near_vector( - near_vector=embedding, - limit=5 - ) - else: - raise ValueError(f"Unsupported search type: {search_type}") - - context_segments = [ - ContextSegment( - chunk_id=int(item.properties.get('chunk_id', 0)), - content=item.properties.get('content', ''), - score=getattr(item.metadata, 'distance', None) - ) for item in results.objects - ] - return context_segments, keywords - except Exception as e: - logger.error(f"Weaviate search error: {e}") - return [] - - def generate_response(self, prompt: str, context_segments: List[ContextSegment]) -> str: - """Generate response using OpenAI""" - context_text = "\n".join([ - f"\n{seg.content}" - for seg in context_segments - ]) - - try: - response = self.openai_client.chat.completions.create( - model="gpt-4o", - messages=[ - { - "role": "system", - "content": f"Use this context if relevant: {context_text}" - }, - { - "role": "user", - "content": prompt - } - ] - ) - return response.choices[0].message.content - except Exception as e: - logger.error(f"OpenAI generation error: {e}") - return "I'm sorry, but I couldn't generate a response." - - def process_prompt(self, prompt_request: PromptRequest) -> PromptResponse: - """Main method to process user prompt""" - try: - # Search for relevant context - context_segments, keywords = self.search_weaviate(prompt_request.user_prompt_text) - - # Generate response - generated_response = self.generate_response( - prompt_request.user_prompt_text, - context_segments - ) - - return PromptResponse( - generated_response=generated_response, - context_segments=context_segments, - keywords = keywords, - error_code=0 - ) - - except Exception as e: - logger.error(f"Prompt processing error: {e}") - return PromptResponse( - generated_response="An error occurred while processing your request.", - error_code=500 - ) - -# Initialize processor -processor = PromptProcessor() - -# API Endpoint -@app.post("/process-prompt", response_model=PromptResponse) -async def process_prompt_endpoint(prompt_request: PromptRequest): - """Process user prompt and return response""" - return processor.process_prompt(prompt_request) - - -# Cleanup on shutdown -@app.on_event("shutdown") -async def shutdown_event(): - """Close Weaviate connection on app shutdown""" - processor.weaviate_client.close() \ No newline at end of file diff --git a/MinuteMate/back/main_fn.py b/MinuteMate/back/main_fn.py new file mode 100644 index 00000000..43bb144a --- /dev/null +++ b/MinuteMate/back/main_fn.py @@ -0,0 +1,199 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +import os + +from rake_nltk import Rake + +import weaviate +from weaviate.classes.init import Auth +from weaviate.classes.init import AdditionalConfig +from weaviate.classes.init import Timeout +from weaviate.classes.query import Rerank +from weaviate.classes.query import MetadataQuery + +import openai +from openai import OpenAI + + +# Initialize the FastAPI app +app = FastAPI( + title="MinuteMate Propmpt & Response API", + description="A simple API that takes a text prompt uses ", + version="1.0.0" + +) + +# Define the request schema +class PromptRequest(BaseModel): + user_prompt_text: str + +# Define the response schema +class PromptResponse(BaseModel): + generated_response: str + error_code : int + +# Takes a prompt from the front end, processes the prompt +# using NLP tools, embedding services, and generative services +# and finally returns the prompt response +def process_prompt(prompt_request: PromptRequest) -> PromptResponse: + + ### 0 - ENVIRONMENT AND CONFIGURATION ### + + # Update environment variables + # not sure if this works + # TODO test and/or look for alternatives + import os + + # Set API keys, endpoint URLs, model versions, and configurations + # Embedding and Generative Models + OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') + # OPENAI_BASE_URL = os.getenv('OPENAI_BASE_URL') + OPENAI_EMBEDDING_URL = os.getenv('OPENAI_EMBEDDING_URL') + OPENAI_GENERATION_URL = os.getenv('OPENAI_GENERATION_URL') + EMBEDDING_MODEL = 'text-embedding-3-small' + ENCODING_FORMAT = 'float' + RESPONDING_GENERATIVE_MODEL = 'gpt-4o' + # TRUSTSAFETY_GENERATIVE_MODEL = llama on modal, probably, but can't be too + + # API key, endpoint URL, and target collection(s) + # for Weaviate vector database + WEAVIATE_URL = os.environ['WEAVIATE_URL'] + WEAVIATE_API_KEY = os.environ['WEAVIATE_API_KEY'] + WEAVIATE_TARGET_COLLECTION = 'MeetingDocument' + # WEAVIATE_TARGET_COLLECTION = "VERBA_Embedding_text_embedding_3_small" + + + + ### 1- INITIAL TRUST AND SAFETY CHECK ### + # TODO add initial trust & safety check here + # If trust and safety check fails, return the error immediately + + + + ### 2- INFORMATION RETRIEVAL ### + + # Set RAG search type + SEARCH_TYPE = 'keyword' + # SEARCH_TYPE = 'vector' + # SEARCH_TYPE = 'hybrid' + + # Establish connection with Weaviate server + # https://weaviate.io/developers/weaviate + weaviate_client = weaviate.connect_to_weaviate_cloud( + cluster_url=WEAVIATE_URL, + auth_credentials=Auth.api_key(WEAVIATE_API_KEY), + ) + # Look up the appropriate Weviate database collection + db_collection = weaviate_client.collections.get(WEAVIATE_TARGET_COLLECTION) + db_response = None + + # Extract keywords and query database + # TODO - finish and test + if(SEARCH_TYPE == 'keyword'): + + rake = Rake() + rake.extract_keywords_from_text(prompt_request.user_prompt_text) + keywords = rake.get_ranked_phrases()[:3] + db_response = db_collection.query.bm25( + query=",".join(keywords), + limit=5, + # rerank=Rerank( + # prop="content", + # query="meeting" + # ), + # return_metadata=MetadataQuery(score=True) + ) + + # Vectorize the prompt and query the database + # TODO - test + elif(SEARCH_TYPE == 'vector'): + + + # Set API Key. Not necessary if you have an + # OPENAI_API_KEY variable in your environment + openai.api_key = OPENAI_API_KEY + embedding_client = OpenAI() + + # Vector-embed the prompt + embedding_response = embedding_client.embeddings.create( + model = EMBEDDING_MODEL, + input = prompt_request.user_prompt_text, + encoding_format = ENCODING_FORMAT + ) + + # Extract the vector embeddings list[float] from the embedding response + query_vector = embedding_response.data[0].embedding + + # Send vector query to database and get response + db_response = db_collection.query.near_vector( + near_vector=query_vector, + limit=10, + return_metadata=MetadataQuery(distance=True) + ) + + #TODO support this + #elif(SEARCH_TYPE == 'hybrid'): + + + else: + #No RAG search + db_response = None + + # Extract items from database response + # and aggregate into a single string + db_response_text = "" + for item in db_response.objects: + segment = '\n\n' + db_response_text += segment + db_response_text += item.properties.get('content') + + + ### 3 - RESPONSE GENERATION ### + + # Generate response to user with OpenAI generative model + # https://platform.openai.com/docs/api-reference/chat/create + openai.api_key = OPENAI_API_KEY + generation_client = OpenAI() + generated_response_text = generation_client.chat.completions.create( + model="gpt-4o", + messages=[ + { + "role": "system", + "content": f"You are a helpful assistant who uses this context if appropriate: {db_response_text}" + }, + { + "role": "user", + "content": prompt_request.user_prompt_text + } + ] + ) + + ### 4 - FINAL TRUST AND SAFETY CHECK ### + # TODO add final trust & safety check here + # If trust and safety check fails, return an error + + + ### 5 - BUILD & RETURN RESPONSE OBJECT ### + # Return chat response to API layer + # to be passed along to frontend + prompt_response = PromptResponse() + prompt_response.generated_response = generated_response_text + return prompt_request + + + + + +# API endpoint +@app.post("/process-prompt", response_model=PromptResponse) +async def process_prompt_endpoint(prompt_request: PromptRequest): + """ + Process the prompt and return the response + """ + try: + prompt_response = process_prompt(prompt_request) + return PromptResponse(result=prompt_response) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + \ No newline at end of file diff --git a/MinuteMate/back/methods/__init__.py b/MinuteMate/back/methods/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/MinuteMate/back/requirements.txt b/MinuteMate/back/requirements.txt index 1cfda1c3..7f5fa680 100644 --- a/MinuteMate/back/requirements.txt +++ b/MinuteMate/back/requirements.txt @@ -1,12 +1,16 @@ +# NLP stuff +rake_nltk==1.0.6 +nltk==3.9.1 + # Necessary for the API -fastapi[standard] +fastapi[standard]==0.115.5 # Necessary for the API # Handles data validation, parsing, error handling, conversions, type hints -pydantic +pydantic==2.10.1 # Necessary for web stuff -uvicorn +uvicorn[standard]==0.32.1 # vector database weaviate-client==4.7.1 diff --git a/MinuteMate/front/Dockerfile b/MinuteMate/front/Dockerfile index ecc035d7..e37eccaf 100644 --- a/MinuteMate/front/Dockerfile +++ b/MinuteMate/front/Dockerfile @@ -1,6 +1,7 @@ -FROM python:3.11-slim -WORKDIR /App -COPY . /App +FROM python:3.11 +WORKDIR /app +COPY . /app +RUN pip install --upgrade pip RUN python -m pip install -r requirements.txt -EXPOSE 8000 -CMD [" ", "start","--port","8002","--host","0.0.0.0"] +EXPOSE 8501 +CMD ["streamlit", "run","app.py"] diff --git a/MinuteMate/front/app.py b/MinuteMate/front/app.py index 4e89685b..62006a63 100644 --- a/MinuteMate/front/app.py +++ b/MinuteMate/front/app.py @@ -1,10 +1,6 @@ -from st_weaviate_connection import WeaviateConnection import streamlit as st -import time -import os import requests -from dotenv import load_dotenv # st.set_page_config(layout="wide") @@ -20,22 +16,8 @@ # ) -load_dotenv() - -ENV_VARS = ["WEAVIATE_URL", "WEAVIATE_API_KEY", "OPENAI_KEY"] NUM_IMAGES_PER_ROW = 3 -def get_env_vars(env_vars: list) -> dict: - """Retrieve environment variables.""" - env_vars_dict = {} - for var in ENV_VARS: - value = os.environ.get(var, "") - if value == "": - st.error(f"{var} not set", icon="🚨") - st.stop() - env_vars_dict[var] = value - - return env_vars_dict def display_chat_messages() -> None: @@ -50,24 +32,13 @@ def display_chat_messages() -> None: if i + j < len(message["images"]): cols[j].image(message["images"][i + j], width=200) -env_vars = get_env_vars(ENV_VARS) -url = env_vars["WEAVIATE_URL"] -api_key = env_vars["WEAVIATE_API_KEY"] -openai_key = env_vars["OPENAI_KEY"] st.title("📝 Minute Mate") -conn = st.connection( - "weaviate", - type=WeaviateConnection, - url=url, - api_key=api_key, - additional_headers={"X-OpenAI-Api-Key": openai_key}, -) - with st.sidebar: - st.sidebar.image("./../../assets/Fun_Logo.jpg", width=150) + # TODO add image to app assets to deploy + # st.sidebar.image("./../../assets/Fun_Logo.jpg", width=150) st.subheader("Speeding up Municipal Communication") st.header("Settings") @@ -144,7 +115,6 @@ def display_chat_messages() -> None: button_pressed = example_prompts[5] - if prompt := (st.chat_input("Type your prompt") or button_pressed): with st.chat_message("user"): st.markdown(prompt) @@ -153,7 +123,7 @@ def display_chat_messages() -> None: try: # Make API call to backend response = requests.post( - "http://localhost:8000/process-prompt", # Adjust URL as needed + "http://host.docker.internal:8001/process-prompt", # Adjust URL as needed json={"user_prompt_text": prompt} ) diff --git a/MinuteMate/front/docker-compose.yml b/MinuteMate/front/docker-compose.yml new file mode 100644 index 00000000..f3bfca4e --- /dev/null +++ b/MinuteMate/front/docker-compose.yml @@ -0,0 +1,7 @@ +services: + chat_frontend: + build: + context: ./ + dockerfile: Dockerfile + ports: + - 8501:8501 \ No newline at end of file diff --git a/MinuteMate/front/requirements.txt b/MinuteMate/front/requirements.txt index 1cb8da3b..8d905b7f 100644 --- a/MinuteMate/front/requirements.txt +++ b/MinuteMate/front/requirements.txt @@ -1,8 +1,4 @@ -# Environmental variables -python-dotenv==1.0.0 -# Embedding and generation -openai==1.54.3 +streamlit -# Vector database -weaviate-client==4.7.1 \ No newline at end of file +pandas \ No newline at end of file