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

ArangoDB: Chathistory #9

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions comps/chathistory/arango/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (C) 2024 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

FROM python:3.11-slim

ENV LANG=C.UTF-8

# Install system dependencies
RUN apt-get update -y && apt-get install -y --no-install-recommends --fix-missing \
build-essential \
libjemalloc-dev \
libgl1-mesa-glx

# Install Python packages globally


# Create user
RUN useradd -m -s /bin/bash user && \
mkdir -p /home/user/comps/chathistory/arango && \
chown -R user /home/user

USER user

COPY comps /home/user/comps
COPY requirements.txt /home/user/

RUN pip install --no-cache-dir --upgrade pip setuptools && \
pip install --no-cache-dir -r /home/user/comps/chathistory/arango/requirements.txt && \
pip install --no-cache-dir -r /home/user/requirements.txt

ENV PYTHONPATH=/home/user/

WORKDIR /home/user/comps/chathistory/arango
RUN pip install -r requirements.txt

ENTRYPOINT ["python", "chathistory_arango.py"]
109 changes: 109 additions & 0 deletions comps/chathistory/arango/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 📝 Chat History Microservice with MongoDB

This README provides setup guides and all the necessary information about the Chat History microservice with MongoDB database.

---

## Setup Environment Variables

```bash
export http_proxy=${your_http_proxy}
export https_proxy=${your_http_proxy}
export MONGO_HOST=${MONGO_HOST}
export MONGO_PORT=27017
export DB_NAME=${DB_NAME}
export COLLECTION_NAME=${COLLECTION_NAME}
```

---

## 🚀Start Microservice with Docker

### Build Docker Image

```bash
cd ../../../../
docker build -t opea/chathistory-mongo-server:latest --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy -f comps/chathistory/mongo/Dockerfile .
```

### Run Docker with CLI

- Run MongoDB image container

```bash
docker run -d -p 27017:27017 --name=mongo mongo:latest
```

- Run the Chat History microservice

```bash
docker run -d --name="chathistory-mongo-server" -p 6012:6012 -e http_proxy=$http_proxy -e https_proxy=$https_proxy -e no_proxy=$no_proxy -e MONGO_HOST=${MONGO_HOST} -e MONGO_PORT=${MONGO_PORT} -e DB_NAME=${DB_NAME} -e COLLECTION_NAME=$ {COLLECTION_NAME} opea/chathistory-mongo-server:latest
```

---

## ✅ Invoke Microservice

The Chat History microservice exposes the following API endpoints:

- Create new chat conversation

```bash
curl -X 'POST' \
http://${host_ip}:6012/v1/chathistory/create \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"messages": "test Messages", "user": "test"
}
}'
```

- Get all the Conversations for a user

```bash
curl -X 'POST' \
http://${host_ip}:6012/v1/chathistory/get \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user": "test"}'
```

- Get a specific conversation by id.

```bash
curl -X 'POST' \
http://localhost:6012/v1/chathistory/get \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user": "test", "id":"673255bad3e51a6fdef12b5e"}'
```

- Update the conversation by id.

```bash
curl -X 'POST' \
http://localhost:6012/v1/chathistory/create \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"data": {
"messages": "test Messages Update", "user": "test"
},
"id":"673255bad3e51a6fdef12b5e"
}'
```

- Delete a stored conversation.

```bash
curl -X 'POST' \
http://${host_ip}:6012/v1/chathistory/delete \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"user": "test", "id":"668620173180b591e1e0cd74"}'
```
45 changes: 45 additions & 0 deletions comps/chathistory/arango/arango_conn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (C) 2024 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

# import motor.motor_asyncio as motor
from arango import ArangoClient as PythonArangoClient
from config import DB_NAME, ARANGODB_HOST, ARANGODB_PORT, ARANGODB_PASSWORD, ARANGODB_USERNAME

class ArangoClient:
conn_url = f"http://{ARANGODB_HOST}:{ARANGODB_PORT}/"

@staticmethod
def get_db_client():
try:
# Create client
print(f"Connecting to database: {ArangoClient.conn_url}, username: {ARANGODB_USERNAME}, password: {ARANGODB_PASSWORD}, db: {DB_NAME}")
client = PythonArangoClient(hosts=ArangoClient.conn_url)

# First connect to _system database
sys_db = client.db(
'_system',
username=ARANGODB_USERNAME,
password=ARANGODB_PASSWORD,
verify=True
)
print("Connected to _system database")

# Create target database if it doesn't exist
if not sys_db.has_database(DB_NAME):
sys_db.create_database(DB_NAME)
print(f"Created database {DB_NAME}")

# Now connect to the target database
db = client.db(
DB_NAME,
username=ARANGODB_USERNAME,
password=ARANGODB_PASSWORD,
verify=True
)
print(f"Connected to database {DB_NAME}")

return db

except Exception as e:
print(f"Failed to connect to database: {str(e)}, url: {ArangoClient.conn_url}, username: {ARANGODB_USERNAME}, password: {ARANGODB_PASSWORD}, db: {DB_NAME}")
raise e
161 changes: 161 additions & 0 deletions comps/chathistory/arango/arango_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# # Copyright (C) 2024 Intel Corporation
# # SPDX-License-Identifier: Apache-2.0

from config import COLLECTION_NAME
from arango_conn import ArangoClient
from pydantic import BaseModel

class DocumentStore:

def __init__(
self,
user: str,
):
self.user = user

def initialize_storage(self) -> None:
try:
self.db_client = ArangoClient.get_db_client()
# Create collection if it doesn't exist
print(COLLECTION_NAME)
if not self.db_client.has_collection(COLLECTION_NAME):
print("Creating collection")
self.collection = self.db_client.create_collection(COLLECTION_NAME)
else:
print("Collection already exists")
self.collection = self.db_client.collection(COLLECTION_NAME)

print(f"Successfully initialized storage with collection: {COLLECTION_NAME}")

except Exception as e:
print(f"Failed to initialize storage: {e}, url: {ArangoClient.conn_url}, collection: {COLLECTION_NAME}")
raise Exception(f"Storage initialization failed: {e}, url: {ArangoClient.conn_url}, collection: {COLLECTION_NAME}")

def save_document(self, document: BaseModel) -> str:
"""Stores a new document into the storage.

Args:
document: The document to be stored. It should be a Pydantic model.

Returns:
str: The ID of the inserted document.

Raises:
Exception: If an error occurs while storing the document.
"""
try:
inserted_conv = self.collection.insert(
document.model_dump(by_alias=True, mode="json", exclude={"id"})
)
document_id = str(inserted_conv["_key"])
return document_id

except Exception as e:
print(e)
raise Exception(e)

def update_document(self, document_id, updated_data, first_query) -> str:
"""Updates a document in the collection with the given document_id.

Args:
document_id (str): The ID of the document to update.
updated_data (object): The updated data to be set in the document.
first_query (object): The first query to be set in the document.

Returns:
bool: True if the document was successfully updated, False otherwise.

Raises:
KeyError: If an invalid document_id is provided.
Exception: If an error occurs during the update process.
"""
try:
cursor = self.db_client.aql.execute(f"""
FOR doc IN @@collection
FILTER doc._key == @document_id AND doc.data.user == @user
LIMIT 1
UPDATE doc WITH @body IN @@collection
OPTIONS {{ keepNull: @keep_none, mergeObjects: @merge }}
""",
bind_vars={"@collection": self.collection.name, "document_id": document_id, "user": self.user, "body": {"data": updated_data.model_dump(by_alias=True, mode="json"), "first_query": first_query}, "keep_none": True, "merge": True}
)
return "Updated document : {}".format(document_id)

except Exception as e:
print(e)
raise Exception(e)

def get_all_documents_of_user(self) -> list[dict]:
"""Retrieves all documents of a specific user from the collection.

Returns:
A list of dictionaries representing the conversation documents.
Raises:
Exception: If there is an error while retrieving the documents.
"""
conversation_list: list = []
try:
cursor = self.db_client.aql.execute("""
FOR doc IN @@collection
FILTER doc.data.user == @user
RETURN doc
""",
bind_vars={"@collection": self.collection.name, "user": self.user}
)
for document in cursor:
document["id"] = document["_key"]
del document["_key"]
del document["_id"]
del document["_rev"]
del document["data"]
conversation_list.append(document)
return conversation_list

except Exception as e:
print(e)
raise Exception(e)

def get_user_documents_by_id(self, document_id) -> dict | None:
"""Retrieves a user document from the collection based on the given document ID.

Args:
document_id (str): The ID of the document to retrieve.

Returns:
dict | None: The user document if found, None otherwise.
"""
try:
response = self.collection.get(document_id)
if response and response['data']["user"] == self.user:
response.pop("_id", None)
return response
return None

except Exception as e:
print(e)
raise Exception(e)

def delete_document(self, document_id) -> str:
"""Deletes a document from the collection based on the provided document ID.

Args:
document_id (str): The ID of the document to be deleted.

Returns:
bool: True if the document is successfully deleted, False otherwise.

Raises:
KeyError: If the provided document ID is invalid.
Exception: If an error occurs during the deletion process.
"""
try:
doc = self.collection.get(document_id)
if doc and doc['data']["user"] == self.user:
self.collection.delete(document_id)
return "Deleted document : {}".format(document_id)
else:
raise Exception("Not able to delete the Document")

except Exception as e:
print(e)
raise Exception(e)
Loading