Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try on kisski #84

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KISSKI_API_KEY='b6c89c0b03ba5bd30170933cc6861886'
Copy link
Member

@haesleinhuepf haesleinhuepf Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Yixin,

never store API keys on Github so that others can read them.

I recommed you to revoke the huggingface and the Kisski key. Otherwise others can use the platforms on your behalf.

https://docs.github.com/en/rest/authentication/keeping-your-api-credentials-secure?apiVersion=2022-11-28#store-your-authentication-credentials-securely

Best,
Robert

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Robert,

Thanks for your advice. I got a message last night, and in my latest version that I replaced it as "KISSKI_API_KEY=your_api_key" and uploaded it yesterday, but I don't know why you can still read it.

Best,
Yixin

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the git history contains it. Again, I recommend to revoke all keys you put on the internet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know how to delete them? I tried using the "delete file" function, but it does not seem to work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean to delete the history

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should revoke the key instead of deleting the history.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. as shown here: https://huggingface.co/docs/hub/en/security-tokens#how-to-manage-user-access-tokens or by sending an email to the KISSKI folks

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, ok thank you, I will write an email to them to recreate a new one , thank you again

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They have already created a new one and the HF token has already been deleted.

18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ services:
volumes:
- ./search_engine/search/backend/wordcloud/static:/app/static

chatbot_backend:
build:
context: ./search_engine/chatbot
dockerfile: Dockerfile
container_name: chatbot_backend
environment:
- ELASTICSEARCH_HOST=elasticsearch
- ELASTICSEARCH_PORT=9200
- KISSKI_API_KEY=${KISSKI_API_KEY} # Pass the KISSKI API Key
- USE_GPU=True # optional; for logging or future local usage
- MODEL_NAME=meta-llama-3.1-70b-instruct
depends_on:
elasticsearch:
condition: service_healthy
ports:
- "5002:5000"

frontend:
build:
context: ./search_engine/search/frontend # Path to the frontend code
Expand All @@ -63,6 +80,7 @@ services:
depends_on:
- appsubmitter_backend
- search_backend
- chatbot_backend
environment:
- REACT_APP_BACKEND_URL=http://localhost:5001

Expand Down
13 changes: 12 additions & 1 deletion folder structure.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@
├── images
│ ├── search_results.png
│ └── submit_materials.png
├── nfdi_search_engine_design
├── search_engine
│ ├── Elasticsearch
│ ├── appsubmitter_backend
│ │ ├── Dockerfile
│ │ ├── requirements_submitter.txt
│ │ └── submitter.py
│ ├── chatbot/
│ │ ├── chatbot.py
│ │ ├── llm_utilities.py
│ │ ├── requirements_chatbot.txt
│ │ └── Dockerfile
│ ├── search
│ │ ├── backend
│ │ │ ├── wordcloud
│ │ │ │ ├── generate_wordcloud.py
│ │ │ │ ├── requirements_wordcloud.txt
│ │ │ │ └── Dockerfile.txt
│ │ │ ├── data.json
│ │ │ ├── index_data.py
│ │ │ ├── Dockerfile
│ │ │ └── requirements_index.txt
│ │ └── frontend
│ │ ├── Dockerfile
Expand Down Expand Up @@ -77,5 +87,6 @@
│ └── elasticsearch setup
├── LICENSE
├── README
├── .env
├── docker-compose
└── folder structure
24 changes: 24 additions & 0 deletions search_engine/chatbot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Use the official Python image as a base
FROM python:3.12-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Set the working directory inside the container
WORKDIR /app

# Copy the requirements file into the container
COPY requirements_chatbot.txt .

# Install the dependencies
RUN pip install --upgrade pip && pip install -r requirements_chatbot.txt

# Copy the rest of the application code into the container
COPY . .

# Expose the port that the Flask app runs on
EXPOSE 5000

# Run the application
CMD ["python", "chatbot.py"]
155 changes: 155 additions & 0 deletions search_engine/chatbot/chatbot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
from elasticsearch import Elasticsearch, ConnectionError
from llm_utilities import LLMUtilities
import logging
import platform
import time
import os

# Flask app setup
app = Flask(__name__)
CORS(app)

# Logging setup
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Hardware information (informational only; actual GPU inference is on KISSKI's side)
SYSTEM_INFO = {
"Machine": platform.node(),
"Processor": platform.processor(),
"LocalGPU": "NVIDIA RTX 500 Ada Generation Laptop GPU (3.9GB dedicated / 37GB shared)"
}
logger.info(f"System Info: {SYSTEM_INFO}")

# Function to connect to Elasticsearch with retry logic
def connect_elasticsearch():
"""
Connects to Elasticsearch with retry logic.
Returns:
Elasticsearch instance if connection is successful, otherwise raises an exception.
"""
es = None
max_attempts = 120 # up to 20 minutes
es_host = os.getenv("ELASTICSEARCH_HOST", "elasticsearch")
es_port = os.getenv("ELASTICSEARCH_PORT", "9200")

# Convert es_port to integer
try:
es_port = int(es_port)
except ValueError:
logger.error(f"ELASTICSEARCH_PORT is not a valid integer: {es_port}")
raise

for attempt in range(max_attempts):
try:
es = Elasticsearch(
[{"host": es_host, "port": es_port, "scheme": "http"}],
request_timeout=30
)
if es.ping():
logger.info("Connected to Elasticsearch")
return es
else:
logger.error("Elasticsearch ping failed")
except ConnectionError:
logger.warning(
f"Elasticsearch not ready, attempt {attempt + 1}/{max_attempts}, retrying in 15 seconds..."
)
time.sleep(15)
except Exception as e:
logger.error(f"Unexpected error while connecting to Elasticsearch: {e}")
time.sleep(15)
raise Exception("Could not connect to Elasticsearch after several attempts")

# Connect to Elasticsearch
es = connect_elasticsearch()

# Determine if GPU usage is set (informational only in this remote KISSKI scenario)
use_gpu_env = os.getenv("USE_GPU", "False").lower() == "true"

# Model name to use on KISSKI; defaults to a 70B Llama model
model_name = os.getenv("MODEL_NAME", "meta-llama-3.1-70b-instruct")

# Initialize the LLM utility for KISSKI
llm_util = LLMUtilities(model_name=model_name, use_gpu=use_gpu_env)

def retrieve_documents(query, top_k=3):
"""
Retrieves relevant documents from Elasticsearch based on a user query.
Args:
query (str): The search query.
top_k (int): Number of top documents to retrieve.
Returns:
list: A list of retrieved documents.
"""
try:
response = es.search(
index="bioimage-training",
body={
"query": {
"multi_match": {
"query": query,
"fields": ["name^3", "description", "tags", "authors", "type", "license"],
"type": "best_fields",
}
}
},
size=top_k,
)
documents = [
{
"name": hit["_source"].get("name", "Unnamed"),
"description": hit["_source"].get("description", "No description available"),
"url": hit["_source"].get("url", ""),
}
for hit in response["hits"]["hits"]
]
return documents
except Exception as e:
logger.error(f"Error retrieving documents from Elasticsearch: {e}")
return []

def generate_response(query, documents):
"""
Generates a context-aware response from the KISSKI LLM using the provided query and document context.
"""
context = "\n".join(
[f"- {doc['name']}: {doc['description']} (URL: {doc['url']})" for doc in documents]
)
prompt = f"""
Based on the following documents, answer the user's question concisely and include relevant links.

## Documents
{context}

## Question
{query}
"""
return llm_util.generate_response(prompt)

# Chatbot API endpoint
@app.route("/api/chat", methods=["POST"])
def chat():
"""
Chat endpoint to process user queries and generate responses via the KISSKI LLM service.
"""
user_query = request.json.get("query", "")
if not user_query:
return jsonify({"error": "Query cannot be empty"}), 400

# Retrieve relevant documents from Elasticsearch
documents = retrieve_documents(user_query)
if not documents:
return jsonify({"response": "No relevant documents found.", "sources": []})

# Generate the chatbot response using the KISSKI LLM
reply = generate_response(user_query, documents)

return jsonify({"response": reply, "sources": documents})

# Main entry point
if __name__ == "__main__":
logger.info(f"Starting chatbot. GPU usage requested = {use_gpu_env}, model = {model_name}")
app.run(host="0.0.0.0", port=5000, debug=True)
56 changes: 56 additions & 0 deletions search_engine/chatbot/llm_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import logging
import openai

logger = logging.getLogger(__name__)

class LLMUtilities:
"""
A utility class for generating responses using the KISSKI LLM endpoint (OpenAI-compatible).
"""
def __init__(self, model_name="meta-llama-3.1-70b-instruct", use_gpu=True):
"""
Initialize the LLM utility with the specified model and GPU preference.
Args:
model_name (str): The KISSKI model name to load (e.g., "meta-llama-3.1-70b-instruct").
use_gpu (bool): Whether GPU usage is requested. Actual GPU usage depends on KISSKI's service.
"""
self.model_name = model_name
self.use_gpu = use_gpu

# Use the KISSKI-provided API key from environment
openai.api_key = os.environ.get("KISSKI_API_KEY")
if not openai.api_key:
logger.error("Missing KISSKI_API_KEY environment variable.")
raise EnvironmentError("Please set KISSKI_API_KEY for KISSKI LLM access.")

# Point OpenAI client to the KISSKI Chat AI endpoint
openai.api_base = "https://chat-ai.academiccloud.de/v1"

logger.info(
f"KISSKI LLM configured with model '{self.model_name}'. GPU usage = {self.use_gpu}."
)

def generate_response(self, prompt, max_new_tokens=150, num_return_sequences=1):
"""
Generate a response from the KISSKI LLM service using the new openai>=1.0.0 Chat interface.
Args:
prompt (str): The input prompt for the model.
max_new_tokens (int): Maximum tokens to generate in the reply.
num_return_sequences (int): How many responses to return.
Returns:
str: The generated response text from the LLM.
"""
try:
messages = [{"role": "user", "content": prompt}]
response = openai.chat.completions.create(
model=self.model_name,
messages=messages,
max_tokens=max_new_tokens,
n=num_return_sequences,
temperature=0.7
)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"Error during response generation via KISSKI LLM: {e}")
return f"Sorry, I couldn't generate a response. Error: {e}"
6 changes: 6 additions & 0 deletions search_engine/chatbot/requirements_chatbot.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
elasticsearch
flask
flask-cors
pyyaml
requests
openai==1.57.4