MingDoan commited on
Commit
eabc9cd
·
1 Parent(s): aabae8d

feat: Chats & FD services

Browse files
.gitignore CHANGED
@@ -2,6 +2,7 @@ env/
2
  __pycache__/
3
  .env
4
  temp/*
 
5
 
6
  # Keep the .gitkeep file in the temp/ directory
7
  !.gitkeep
 
2
  __pycache__/
3
  .env
4
  temp/*
5
+ data/*
6
 
7
  # Keep the .gitkeep file in the temp/ directory
8
  !.gitkeep
README.md CHANGED
@@ -41,6 +41,14 @@ curl --location 'https://{domain}/service/fw/' --request 'POST' --data '{"url":"
41
  curl --location 'https://{domain}/service/fw/' --request 'DELETE' --data '{"index":0}'
42
  ```
43
 
 
 
 
 
 
 
 
 
44
  ### Children Server Endpoint Request & Response
45
 
46
  - `/api/rembg/`
@@ -59,6 +67,64 @@ curl --location 'https://{domain}/service/fw/' --request 'DELETE' --data '{"inde
59
  }
60
  ```
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  ## 😊 Contributors
63
 
64
  - GDSC-FPTU [[gdsc-fptu](https://github.com/gdsc-fptu)]
 
41
  curl --location 'https://{domain}/service/fw/' --request 'DELETE' --data '{"index":0}'
42
  ```
43
 
44
+ ### Server Health Checker
45
+
46
+ ```bash
47
+ curl --location '{children-base-domain}/health'
48
+ ```
49
+
50
+ This route checks the health status of children server. The children is considered as good health when it return anything with status 200.
51
+
52
  ### Children Server Endpoint Request & Response
53
 
54
  - `/api/rembg/`
 
67
  }
68
  ```
69
 
70
+ - `/api/fd/`
71
+
72
+ ```json
73
+ // Request. Form-data
74
+ {
75
+ "image": "<image-data"
76
+ }
77
+
78
+ // Response. JSON
79
+ {
80
+ "data": {
81
+ "faces": []
82
+ }
83
+ }
84
+ ```
85
+
86
+ - `/api/chats/`
87
+
88
+ ```json
89
+ // Request. JSON
90
+ {
91
+ "prompt": "A Prompt from user"
92
+ }
93
+
94
+ // Response. JSON
95
+ {
96
+ "data": {
97
+ "message": "A Message from AI"
98
+ }
99
+ }
100
+ ```
101
+
102
+ - `/api/chats/:id`
103
+
104
+ ```json
105
+ // Request. JSON
106
+ {
107
+ "prompt": "A Prompt from user",
108
+ "conversation": [
109
+ {
110
+ "role": "user",
111
+ "content": "Message A"
112
+ },
113
+ {
114
+ "role": "assistant",
115
+ "content": "Message B"
116
+ }
117
+ ]
118
+ }
119
+
120
+ // Response. JSON
121
+ {
122
+ "data": {
123
+ "message": "A Message from AI"
124
+ }
125
+ }
126
+ ```
127
+
128
  ## 😊 Contributors
129
 
130
  - GDSC-FPTU [[gdsc-fptu](https://github.com/gdsc-fptu)]
apps/apis/chats/__init__.py CHANGED
@@ -1,6 +1,10 @@
1
- from fastapi import APIRouter
 
2
  from fastapi.responses import JSONResponse
3
- from fastapi.requests import Request
 
 
 
4
 
5
  router = APIRouter(prefix='/chats')
6
  router_base_configs = {
@@ -10,24 +14,63 @@ router_base_configs = {
10
 
11
 
12
  # Message
13
- @router.post("/:conversation_id", **router_base_configs)
14
- def message():
15
- return {"message": "Hello World"}
 
 
 
 
 
 
16
 
 
 
17
 
18
- # Get conversations
19
- @router.get("/get", **router_base_configs)
20
- def get_conversations():
21
- return {"conversations": "Hello World"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
 
24
  # Create conversation
25
- @router.post("/create", **router_base_configs)
26
  def create_conversation():
27
- return {"conversation_id": "Hello World"}
 
28
 
29
 
30
  # Delete conversation
31
- @router.delete("/delete", **router_base_configs)
32
- def delete_conversation():
33
- return {"conversation": "Hello World"}
 
 
1
+ import os
2
+ from fastapi import APIRouter, HTTPException, Depends
3
  from fastapi.responses import JSONResponse
4
+ from .models.chats_model import ChatsModel
5
+ from .controllers.conversation_storage import is_conversation_storage_exist, get_conversation_storage, create_conversation_storage, delete_conversation_storage, update_conversation_storage
6
+ from .controllers.openai_controller import send_message, send_message_conversation
7
+ from apps.services.foward.fw_middleware import forward_request, forward_middleware
8
 
9
  router = APIRouter(prefix='/chats')
10
  router_base_configs = {
 
14
 
15
 
16
  # Message
17
+ @router.post("/", **router_base_configs)
18
+ def message(data: ChatsModel, fw_index=Depends(forward_middleware)):
19
+ # Forward request
20
+ fw_data = {
21
+ "data": {"prompt": data.prompt}
22
+ }
23
+ fw_response = forward_request(fw_index, fw_data, '/api/chats/')
24
+ if fw_response is not None:
25
+ return {"message": fw_response["data"]["message"]}
26
 
27
+ response = send_message(data.prompt)
28
+ return {"message": response}
29
 
30
+
31
+ # Message to conversation
32
+ @router.post("/{cid}", **router_base_configs)
33
+ def conversation(cid: str, data: ChatsModel, fw_index=Depends(forward_middleware)):
34
+ is_exist = is_conversation_storage_exist(cid)
35
+ if not is_exist:
36
+ raise HTTPException(status_code=404, detail="Conversation not found")
37
+
38
+ # Forward request
39
+ fw_data = {
40
+ "data": {"prompt": data.prompt, "conversation": get_conversation_storage(cid)}
41
+ }
42
+ fw_response = forward_request(fw_index, fw_data, f'/api/chats/{cid}')
43
+ if fw_response is not None:
44
+ # Update conversation
45
+ update_conversation_storage(cid, "user", data.prompt)
46
+ update_conversation_storage(
47
+ cid, "assistant", fw_response["data"]["message"])
48
+ return {"message": fw_response["data"]["message"]}
49
+
50
+ response = send_message_conversation(cid, data.prompt)
51
+ return {"message": response}
52
+
53
+
54
+ # Get conversation by id
55
+ @router.get("/{cid}", **router_base_configs)
56
+ def get_conversation(cid: str):
57
+ is_exist = is_conversation_storage_exist(cid)
58
+ if not is_exist:
59
+ raise HTTPException(status_code=404, detail="Conversation not found")
60
+
61
+ conversation = get_conversation_storage(cid)
62
+ return {"conversation": conversation}
63
 
64
 
65
  # Create conversation
66
+ @router.get("/create", **router_base_configs)
67
  def create_conversation():
68
+ cid = create_conversation_storage()
69
+ return {"conversation_id": cid}
70
 
71
 
72
  # Delete conversation
73
+ @router.delete("/delete/{cid}", **router_base_configs)
74
+ def delete_conversation(cid: str):
75
+ delete_conversation_storage(cid)
76
+ return {"conversation_id": cid}
apps/apis/chats/controllers/conversation_storage.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from utils.local_storage import read_from_local, save_to_local, remove_from_local
3
+ from utils.constants import DATA_DIR
4
+
5
+
6
+ def is_conversation_storage_exist(id: str) -> bool:
7
+ # Check if conversation exist
8
+ return read_from_local(f"{id}.json", DATA_DIR) is not None
9
+
10
+
11
+ def get_conversation_storage(id: str) -> list:
12
+ # Get conversation
13
+ conversation = read_from_local(f"{id}.json", DATA_DIR)
14
+ # Return conversation
15
+ return conversation
16
+
17
+
18
+ def create_conversation_storage() -> str:
19
+ # Generate conversation id
20
+ conversation_id = str(uuid.uuid4())
21
+ # Save to local
22
+ save_to_local([], f"{conversation_id}.json", False, DATA_DIR)
23
+ # Return conversation id
24
+ return conversation_id
25
+
26
+
27
+ def update_conversation_storage(id: str, role: str, message: str) -> list:
28
+ # Get conversation
29
+ conversation = get_conversation_storage(id)
30
+ # Append new message
31
+ conversation.append({
32
+ "role": role,
33
+ "content": message
34
+ })
35
+ # Save to local
36
+ save_to_local(conversation, f"{id}.json", False, DATA_DIR)
37
+ # Return conversation
38
+ return conversation
39
+
40
+
41
+ def delete_conversation_storage(id: str):
42
+ # Delete conversation
43
+ remove_from_local(f"{id}.json", DATA_DIR)
apps/apis/chats/controllers/openai_controller.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from .conversation_storage import get_conversation_storage, update_conversation_storage
4
+
5
+
6
+ BASE_ENHANCE_PROMPT = {
7
+ "role": "system", "content": "You are a helpful assistant. You are helping a customer to solve a problem."}
8
+
9
+
10
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
11
+
12
+
13
+ def send_message(prompt: str):
14
+ return client.chat.completions.create(
15
+ model="gpt-3.5-turbo",
16
+ messages=[
17
+ BASE_ENHANCE_PROMPT,
18
+ {"role": "user", "content": prompt}
19
+ ]
20
+ ).choices[0].message.content
21
+
22
+
23
+ def send_message_conversation(conversation_id: str, prompt: str):
24
+ # Update conversation
25
+ update_conversation_storage(conversation_id, "user", prompt)
26
+ # Generate response
27
+ response = client.chat.completions.create(
28
+ model="gpt-3.5-turbo",
29
+ messages=[
30
+ BASE_ENHANCE_PROMPT,
31
+ *get_conversation_storage(conversation_id),
32
+ {"role": "user", "content": prompt}
33
+ ]
34
+ ).choices[0].message.content
35
+ # Update conversation
36
+ update_conversation_storage(conversation_id, "assistant", response)
37
+ return response
apps/apis/chats/models/chats_model.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class ChatsModel(BaseModel):
5
+ prompt: str = Field(..., example="Hello, I'm a chatbot")
apps/apis/fd/__init__.py CHANGED
@@ -1,5 +1,9 @@
1
- from fastapi import APIRouter
2
  from fastapi.responses import JSONResponse
 
 
 
 
3
 
4
  router = APIRouter(prefix='/fd')
5
  router_base_configs = {
@@ -10,5 +14,28 @@ router_base_configs = {
10
 
11
  # Face detection
12
  @router.post("/", **router_base_configs)
13
- def faces_detection():
14
- return {"faces": "Hello World"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
  from fastapi.responses import JSONResponse
3
+ from apps.services.foward.fw_middleware import forward_request, forward_middleware
4
+ from .models.fd_model import FaceDetectionModel
5
+ from .controllers.fd_controller import faces_detection_controller
6
+
7
 
8
  router = APIRouter(prefix='/fd')
9
  router_base_configs = {
 
14
 
15
  # Face detection
16
  @router.post("/", **router_base_configs)
17
+ async def faces_detection(
18
+ image: FaceDetectionModel.image = FaceDetectionModel.image_default,
19
+ fw_index: FaceDetectionModel.fw_index = Depends(forward_middleware)
20
+ ):
21
+ # Forward request
22
+ fw_data = {
23
+ "files": {"image": image.file}
24
+ }
25
+ fw_response = forward_request(fw_index, fw_data, '/api/fd/')
26
+ if fw_response is not None:
27
+ return JSONResponse({
28
+ "faces": fw_response["data"]["faces"]
29
+ })
30
+
31
+ # Check if image is None
32
+ if image is None:
33
+ raise HTTPException(status_code=400, detail="Image is required")
34
+ # Check if image is empty
35
+ if image.filename == '':
36
+ raise HTTPException(status_code=400, detail="Image is empty")
37
+ # Read image
38
+ image_bytes = await image.read()
39
+ # Process image
40
+ detected_faces = faces_detection_controller(image_bytes)
41
+ return JSONResponse({"faces": detected_faces})
apps/apis/fd/controllers/fd_controller.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2 as cv
3
+ import mediapipe as mp
4
+ from mediapipe.tasks import python
5
+ from mediapipe.tasks.python import vision
6
+
7
+ base_options = python.BaseOptions(model_asset_path='resources/fd_model.tflite')
8
+ options = vision.FaceDetectorOptions(base_options=base_options)
9
+ detector = vision.FaceDetector.create_from_options(options)
10
+
11
+
12
+ def format_detections(detection_result):
13
+ faces = []
14
+ for detection in detection_result.detections:
15
+ face = {
16
+ "bbox": {
17
+ "x": detection.bounding_box.origin_x,
18
+ "y": detection.bounding_box.origin_y,
19
+ "width": detection.bounding_box.width,
20
+ "height": detection.bounding_box.height,
21
+ },
22
+ }
23
+ faces.append(face)
24
+ return faces
25
+
26
+
27
+ def faces_detection_controller(image: bytes):
28
+ cv_image = cv.imdecode(np.frombuffer(image, np.uint8), cv.IMREAD_COLOR)
29
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=cv_image)
30
+ # Process image
31
+ detection_result = detector.detect(mp_image)
32
+ return format_detections(detection_result)
apps/apis/fd/models/fd_model.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from fastapi import File, UploadFile
2
+
3
+
4
+ class FaceDetectionModel:
5
+ image = UploadFile | None
6
+ image_default = File(
7
+ None, description="Image for face detection", example="image.jpg")
8
+ fw_index = int | None
apps/services/foward/fw_middleware.py CHANGED
@@ -18,7 +18,7 @@ async def verify_fw_resources(url: str) -> bool:
18
  async def forward_middleware(fw: str = None):
19
  # Verify forward destination index
20
  try:
21
- if fw != 'auto':
22
  fw = int(fw)
23
  except Exception:
24
  raise HTTPException(
@@ -46,12 +46,16 @@ async def forward_middleware(fw: str = None):
46
  fw = None
47
  # If forward destination is not auto, update forward destination tasks
48
  else:
49
- update_fw_destination(fw, fw_destinations[fw]['tasks'] + 1)
 
50
 
51
  return fw
52
 
53
 
54
  def forward_request(fw_index: int, data: Any, endpoint: str = '', method: Literal['GET', 'POST', 'PUT', 'DELETE'] = 'POST'):
 
 
 
55
  # Get forward destination url
56
  fw_destinations = get_fw_destinations()
57
  fw_url = fw_destinations[fw_index]['url'] + endpoint
 
18
  async def forward_middleware(fw: str = None):
19
  # Verify forward destination index
20
  try:
21
+ if fw != 'auto' and fw is not None:
22
  fw = int(fw)
23
  except Exception:
24
  raise HTTPException(
 
46
  fw = None
47
  # If forward destination is not auto, update forward destination tasks
48
  else:
49
+ if fw is not None:
50
+ update_fw_destination(fw, fw_destinations[fw]['tasks'] + 1)
51
 
52
  return fw
53
 
54
 
55
  def forward_request(fw_index: int, data: Any, endpoint: str = '', method: Literal['GET', 'POST', 'PUT', 'DELETE'] = 'POST'):
56
+ # Return None if fw_index is None
57
+ if fw_index is None:
58
+ return None
59
  # Get forward destination url
60
  fw_destinations = get_fw_destinations()
61
  fw_url = fw_destinations[fw_index]['url'] + endpoint
apps/services/foward/fw_storage.py CHANGED
@@ -1,11 +1,12 @@
1
  from utils.local_storage import read_from_local, save_to_local
 
2
 
3
 
4
  FOWARD_JSON_FILE = 'forward.json'
5
 
6
 
7
  def get_fw_destinations() -> list:
8
- destinations = read_from_local(FOWARD_JSON_FILE)
9
  if destinations is None:
10
  return []
11
  return destinations
@@ -23,7 +24,7 @@ def append_fw_destination(url: str):
23
  "url": url
24
  })
25
  # Save to local
26
- save_to_local(forward_destinations, FOWARD_JSON_FILE, False)
27
  # Return index
28
  return len(forward_destinations) - 1
29
 
@@ -34,7 +35,7 @@ def update_fw_destination(index: int, tasks: int):
34
  # Update destination
35
  forward_destinations[index]['tasks'] = tasks
36
  # Save to local
37
- save_to_local(forward_destinations, FOWARD_JSON_FILE, False)
38
 
39
 
40
  def delete_fw_destination(index: int):
@@ -43,4 +44,4 @@ def delete_fw_destination(index: int):
43
  # Delete destination
44
  del forward_destinations[index]
45
  # Save to local
46
- save_to_local(forward_destinations, FOWARD_JSON_FILE, False)
 
1
  from utils.local_storage import read_from_local, save_to_local
2
+ from utils.constants import DATA_DIR
3
 
4
 
5
  FOWARD_JSON_FILE = 'forward.json'
6
 
7
 
8
  def get_fw_destinations() -> list:
9
+ destinations = read_from_local(FOWARD_JSON_FILE, DATA_DIR)
10
  if destinations is None:
11
  return []
12
  return destinations
 
24
  "url": url
25
  })
26
  # Save to local
27
+ save_to_local(forward_destinations, FOWARD_JSON_FILE, False, DATA_DIR)
28
  # Return index
29
  return len(forward_destinations) - 1
30
 
 
35
  # Update destination
36
  forward_destinations[index]['tasks'] = tasks
37
  # Save to local
38
+ save_to_local(forward_destinations, FOWARD_JSON_FILE, False, DATA_DIR)
39
 
40
 
41
  def delete_fw_destination(index: int):
 
44
  # Delete destination
45
  del forward_destinations[index]
46
  # Save to local
47
+ save_to_local(forward_destinations, FOWARD_JSON_FILE, False, DATA_DIR)
data/.gitkeep ADDED
File without changes
main.py CHANGED
@@ -2,7 +2,6 @@
2
  import os
3
  import uvicorn
4
  from apps.create_app import create_app
5
- from apps.services.foward.fw_middleware import forward_middleware
6
  # Import routers
7
  from apps.apis import router as api_router
8
  from apps.services import router as service_router
 
2
  import os
3
  import uvicorn
4
  from apps.create_app import create_app
 
5
  # Import routers
6
  from apps.apis import router as api_router
7
  from apps.services import router as service_router
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
resources/.gitkeep ADDED
File without changes
resources/fd_model.tflite ADDED
Binary file (230 kB). View file
 
utils/constants.py CHANGED
@@ -4,6 +4,8 @@ APP_DOMAIN = "http://localhost:8000"
4
 
5
  TEMP_DIR = os.path.join(os.getcwd(), 'temp')
6
 
 
 
7
  DOC_TEMPLATE = f'''
8
  <!DOCTYPE html>
9
  <html>
 
4
 
5
  TEMP_DIR = os.path.join(os.getcwd(), 'temp')
6
 
7
+ DATA_DIR = os.path.join(os.getcwd(), 'data')
8
+
9
  DOC_TEMPLATE = f'''
10
  <!DOCTYPE html>
11
  <html>
utils/local_storage.py CHANGED
@@ -31,7 +31,7 @@ def parse_filename(filename: str):
31
  return filename
32
 
33
 
34
- def save_to_local(file: bytes | Any, filename: str, is_parse_filename: bool = True):
35
  # Parse filename
36
  if is_parse_filename:
37
  filename = parse_filename(filename)
@@ -43,7 +43,7 @@ def save_to_local(file: bytes | Any, filename: str, is_parse_filename: bool = Tr
43
  else:
44
  mode = 'wb'
45
  # Save file
46
- with open(os.path.join(TEMP_DIR, filename), mode) as f:
47
  if file_extension == 'json':
48
  json.dump(file, f)
49
  else:
@@ -52,7 +52,7 @@ def save_to_local(file: bytes | Any, filename: str, is_parse_filename: bool = Tr
52
  return f"{APP_DOMAIN}/static/{filename}"
53
 
54
 
55
- def read_from_local(filename: str):
56
  # Get type of file
57
  file_extension = filename.split('.')[-1]
58
  # Get read mode
@@ -61,21 +61,21 @@ def read_from_local(filename: str):
61
  else:
62
  mode = 'rb'
63
  # If file is exist, return file
64
- if os.path.isfile(os.path.join(TEMP_DIR, filename)):
65
- with open(os.path.join(TEMP_DIR, filename), mode) as f:
66
  if file_extension == 'json':
67
  return json.load(f)
68
  return f.read()
69
 
70
 
71
- def remove_from_local(filename: str):
72
  # If file is exist, add number to filename
73
- if os.path.isfile(os.path.join(TEMP_DIR, filename)):
74
- os.remove(os.path.join(TEMP_DIR, filename))
75
 
76
 
77
- def remove_from_local_with_expire(filename: str, expire: int):
78
  # Remove file after expire time
79
  if expire is not None and expire > 0:
80
- t = Timer(expire, remove_from_local, args=[filename])
81
  t.start()
 
31
  return filename
32
 
33
 
34
+ def save_to_local(file: bytes | Any, filename: str, is_parse_filename: bool = True, directory: str = TEMP_DIR):
35
  # Parse filename
36
  if is_parse_filename:
37
  filename = parse_filename(filename)
 
43
  else:
44
  mode = 'wb'
45
  # Save file
46
+ with open(os.path.join(directory, filename), mode) as f:
47
  if file_extension == 'json':
48
  json.dump(file, f)
49
  else:
 
52
  return f"{APP_DOMAIN}/static/{filename}"
53
 
54
 
55
+ def read_from_local(filename: str, directory: str = TEMP_DIR):
56
  # Get type of file
57
  file_extension = filename.split('.')[-1]
58
  # Get read mode
 
61
  else:
62
  mode = 'rb'
63
  # If file is exist, return file
64
+ if os.path.isfile(os.path.join(directory, filename)):
65
+ with open(os.path.join(directory, filename), mode) as f:
66
  if file_extension == 'json':
67
  return json.load(f)
68
  return f.read()
69
 
70
 
71
+ def remove_from_local(filename: str, directory: str = TEMP_DIR):
72
  # If file is exist, add number to filename
73
+ if os.path.isfile(os.path.join(directory, filename)):
74
+ os.remove(os.path.join(directory, filename))
75
 
76
 
77
+ def remove_from_local_with_expire(filename: str, expire: int, directory: str = TEMP_DIR):
78
  # Remove file after expire time
79
  if expire is not None and expire > 0:
80
+ t = Timer(expire, remove_from_local, args=[filename, directory])
81
  t.start()