cevheri commited on
Commit
20d293c
·
1 Parent(s): 5f220d4

feat: inital gradio UI implementation

Browse files
Files changed (7) hide show
  1. Dockerfile +2 -3
  2. README.md +27 -11
  3. app/core/api_response.py +8 -8
  4. gradio.py +29 -0
  5. main.py +9 -0
  6. pyproject.toml +1 -0
  7. uv.lock +30 -0
Dockerfile CHANGED
@@ -26,7 +26,6 @@ ENV PORT=7860
26
 
27
  # Expose the port
28
  EXPOSE 7860
29
-
30
  # Start command for Hugging Face Space
31
- # CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
32
- CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
26
 
27
  # Expose the port
28
  EXPOSE 7860
29
+ EXPOSE 7861
30
  # Start command for Hugging Face Space
31
+ CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
README.md CHANGED
@@ -74,6 +74,27 @@ API Key: sk-template-token
74
  API Key saved to api_key.txt
75
  ```
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  ## Contributing
78
  When you make changes to the code, please run the following commands to ensure the code is running on your local machine and formatted and linted correctly.
79
 
@@ -147,22 +168,17 @@ gh pr create --base main --head feature/new-feature --title "Add new feature" --
147
 
148
 
149
 
150
- ### 🔑 API Key Authentication
151
 
152
- ```bash
153
- curl -X POST "http://localhost:8000/v1/chat/completions" \
154
- -H "Authorization: Bearer sk-template-token" \
155
- -H "Content-Type: application/json" \
156
- -d '{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hello!"}]}'
157
- ```
158
 
159
  ## Open Issue
 
 
 
 
 
160
  - [ ] POST chat/completions - create a new chat completion
161
  - [ ] GET chat/completions - list stored chat completions
162
  - [ ] GET chat/completions/{completion_id} - get a stored chat completion by id
163
  - [ ] GET chat/completions/{completion_id}/messages - get the messages in a stored chat completion by id
164
  - [ ] GET chat/completions/{completion_id}/messages/{message_id}/plots - get the plots/graph-data/figure-json in a stored chat completion by id and message id
165
- - [ ] Implement Mock response for all endpoints
166
- - [ ] Implement API-Key Authentication and validation in all endpoints
167
- - [ ]
168
- - [ ]
 
74
  API Key saved to api_key.txt
75
  ```
76
 
77
+ ### 🔑 API Key Authentication
78
+
79
+ ```bash
80
+ curl -X POST "http://localhost:8000/v1/chat/completions" \
81
+ -H "Authorization: Bearer sk-template-token" \
82
+ -H "Content-Type: application/json" \
83
+ -d '{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hello!"}]}'
84
+ ```
85
+
86
+ ## Mock Implementation
87
+ * `USE_MOCK` environment variable is set to `true`, the API will return mock responses for all endpoints. Default is `false`.
88
+ * `MOCK_DIR` environment variable is used to specify the directory of the mock responses, default is `resources/mock`.
89
+ * Mock file names should be same as the module and function name like below:
90
+ - app/api/chat_api.py - listChatCompletions - resources/mock/`chat_api_list_chat_completions.json` - mock response for GET `/v1/chat/completions`
91
+ - app/api/chat_api.py - createChatCompletion - resources/mock/`chat_api_create_chat_completion.json` - mock response for POST `/v1/chat/completions`
92
+ - app/api/chat_api.py - getChatCompletionMessages - resources/mock/`chat_api_list_messages.json` - mock response for GET `/v1/chat/completions/{completion_id}/messages`
93
+ - app/api/chat_api.py - getChatCompletion - resources/mock/`chat_api_retrieve_chat_completion.json` - mock response for GET `/v1/chat/completions/{completion_id}`
94
+ - app/api/chat_api.py - getChatPlotByMessage - resources/mock/`chat_api_retrieve_plot.json` - mock response for GET `/v1/chat/completions/{completion_id}/messages/{message_id}/plots`
95
+
96
+
97
+
98
  ## Contributing
99
  When you make changes to the code, please run the following commands to ensure the code is running on your local machine and formatted and linted correctly.
100
 
 
168
 
169
 
170
 
 
171
 
 
 
 
 
 
 
172
 
173
  ## Open Issue
174
+ Mock Implementation:
175
+ - [X] Implement Mock response for all endpoints
176
+ - [X] Implement API-Key Authentication and validation in all endpoints
177
+
178
+ Production Implementation:
179
  - [ ] POST chat/completions - create a new chat completion
180
  - [ ] GET chat/completions - list stored chat completions
181
  - [ ] GET chat/completions/{completion_id} - get a stored chat completion by id
182
  - [ ] GET chat/completions/{completion_id}/messages - get the messages in a stored chat completion by id
183
  - [ ] GET chat/completions/{completion_id}/messages/{message_id}/plots - get the plots/graph-data/figure-json in a stored chat completion by id and message id
184
+
 
 
 
app/core/api_response.py CHANGED
@@ -22,7 +22,7 @@ def url_to_filename(url: str, method: str) -> str:
22
  - Input: GET "/v1/chat/completions/{completion_id}" -> "chat_completions_id_GET.json"
23
  - Input: GET "/v1/chat/completions/123/messages" -> "chat_completions_id_messages_GET.json"
24
  """
25
- logger.debug(f"BEGIN: url: {url} method: {method}")
26
  # Remove version prefix and leading/trailing slashes
27
  path = url.strip("/")
28
  if path.startswith("v1/"):
@@ -32,15 +32,15 @@ def url_to_filename(url: str, method: str) -> str:
32
  path = path.replace("{completion_id}", "id")
33
  path = path.replace("{message_id}", "id")
34
 
35
- logger.debug(f"replaced path: {path}")
36
 
37
  # Convert to filename format
38
  filename = path.replace("/", "_")
39
- logger.debug(f"filename: {filename}")
40
 
41
  # convert conversations_1 to conversations with dynamic id
42
  final_filename= re.sub(r'[_][^/]+$', '_id', filename)
43
- logger.debug(f"final_filename: {final_filename}")
44
 
45
  # Add method suffix
46
  result = f"{final_filename}_{method}"
@@ -92,9 +92,9 @@ def api_response():
92
  @wraps(func)
93
  async def wrapper(request: Request, *args, **kwargs):
94
  logger.trace(f"BEGIN: wrapper: {request}")
95
- logger.debug(f"func: {func.__name__}")
96
- logger.debug(f"args: {args}")
97
- logger.debug(f"kwargs: {kwargs}")
98
  if not request:
99
  logger.error("Request object not found in args or kwargs")
100
  raise HTTPException(
@@ -110,7 +110,7 @@ def api_response():
110
  if USE_MOCK:
111
  python_method_name = func.__name__
112
  python_module_name = func.__module__
113
- logger.warning(f"Using mock response for {request.url.path} [{request.method}]")
114
  try:
115
  result = get_mock_response(request.url.path, python_module_name, python_method_name)
116
  logger.trace(f"Mock response: {result}")
 
22
  - Input: GET "/v1/chat/completions/{completion_id}" -> "chat_completions_id_GET.json"
23
  - Input: GET "/v1/chat/completions/123/messages" -> "chat_completions_id_messages_GET.json"
24
  """
25
+ logger.trace(f"BEGIN: url: {url} method: {method}")
26
  # Remove version prefix and leading/trailing slashes
27
  path = url.strip("/")
28
  if path.startswith("v1/"):
 
32
  path = path.replace("{completion_id}", "id")
33
  path = path.replace("{message_id}", "id")
34
 
35
+ logger.trace(f"replaced path: {path}")
36
 
37
  # Convert to filename format
38
  filename = path.replace("/", "_")
39
+ logger.trace(f"filename: {filename}")
40
 
41
  # convert conversations_1 to conversations with dynamic id
42
  final_filename= re.sub(r'[_][^/]+$', '_id', filename)
43
+ logger.trace(f"final_filename: {final_filename}")
44
 
45
  # Add method suffix
46
  result = f"{final_filename}_{method}"
 
92
  @wraps(func)
93
  async def wrapper(request: Request, *args, **kwargs):
94
  logger.trace(f"BEGIN: wrapper: {request}")
95
+ logger.trace(f"func: {func.__name__}")
96
+ logger.trace(f"args: {args}")
97
+ logger.trace(f"kwargs: {kwargs}")
98
  if not request:
99
  logger.error("Request object not found in args or kwargs")
100
  raise HTTPException(
 
110
  if USE_MOCK:
111
  python_method_name = func.__name__
112
  python_module_name = func.__module__
113
+ logger.warning(f"Using mock response for {request.url.path} [{request.method}] > {python_module_name}.{python_method_name}")
114
  try:
115
  result = get_mock_response(request.url.path, python_module_name, python_method_name)
116
  logger.trace(f"Mock response: {result}")
gradio.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gradio.py
2
+ import gradio as gr
3
+ import asyncio
4
+ import httpx
5
+ import environs
6
+
7
+ env = environs.Env()
8
+ env.read_env()
9
+
10
+ BASE_URL = env.str("BASE_URL", "http://localhost:7860")
11
+ API_KEY = env.str("API_KEY", "sk-test-xxx")
12
+
13
+ CHAT_API_ENDPOINT = f"{BASE_URL}/v1/chat/completions"
14
+
15
+ async def call_chat_api(user_input):
16
+ headers = {"Authorization": f"sk-{API_KEY}"}
17
+ payload = {
18
+ "messages": [{"role": "user", "content": user_input}]
19
+ }
20
+ async with httpx.AsyncClient() as client:
21
+ response = await client.post(CHAT_API_ENDPOINT, json=payload, headers=headers)
22
+ return response.json().get("choices", [{}])[0].get("message", {}).get("content", "No response")
23
+
24
+ def chatbot_fn(user_input):
25
+ return asyncio.run(call_chat_api(user_input))
26
+
27
+ def launch_gradio():
28
+ iface = gr.Interface(fn=chatbot_fn, inputs="text", outputs="text", title="Chat with your Data")
29
+ iface.launch(server_name="0.0.0.0", server_port=7861, show_error=True)
main.py CHANGED
@@ -6,6 +6,8 @@ from loguru import logger
6
  from environs import Env
7
  from contextlib import asynccontextmanager
8
  from app.db.client import mongodb
 
 
9
 
10
  env = Env()
11
  env.read_env()
@@ -16,6 +18,8 @@ STORAGE_TYPE = env.str("STORAGE_TYPE", "mongodb")
16
 
17
  print(log_config.get_log_level())
18
 
 
 
19
  @asynccontextmanager
20
  async def lifespan(app: FastAPI):
21
  """
@@ -26,6 +30,11 @@ async def lifespan(app: FastAPI):
26
  logger.info("Starting up application...")
27
  if STORAGE_TYPE == "mongodb":
28
  await mongodb.connect()
 
 
 
 
 
29
  yield
30
 
31
  # Shutdown
 
6
  from environs import Env
7
  from contextlib import asynccontextmanager
8
  from app.db.client import mongodb
9
+ from gradio import launch_gradio
10
+ import threading
11
 
12
  env = Env()
13
  env.read_env()
 
18
 
19
  print(log_config.get_log_level())
20
 
21
+
22
+
23
  @asynccontextmanager
24
  async def lifespan(app: FastAPI):
25
  """
 
30
  logger.info("Starting up application...")
31
  if STORAGE_TYPE == "mongodb":
32
  await mongodb.connect()
33
+
34
+ # Launch Gradio in a separate thread
35
+ thread = threading.Thread(target=launch_gradio)
36
+ thread.daemon = True
37
+ thread.start()
38
  yield
39
 
40
  # Shutdown
pyproject.toml CHANGED
@@ -7,6 +7,7 @@ requires-python = ">=3.12"
7
  dependencies = [
8
  "environs>=14.1.1",
9
  "fastapi>=0.115.12",
 
10
  "loguru>=0.7.3",
11
  "motor>=3.7.1",
12
  "pydantic>=2.11.4",
 
7
  dependencies = [
8
  "environs>=14.1.1",
9
  "fastapi>=0.115.12",
10
+ "httpx>=0.28.1",
11
  "loguru>=0.7.3",
12
  "motor>=3.7.1",
13
  "pydantic>=2.11.4",
uv.lock CHANGED
@@ -135,6 +135,34 @@ wheels = [
135
  { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
136
  ]
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  [[package]]
139
  name = "idna"
140
  version = "3.10"
@@ -185,6 +213,7 @@ source = { virtual = "." }
185
  dependencies = [
186
  { name = "environs" },
187
  { name = "fastapi" },
 
188
  { name = "loguru" },
189
  { name = "motor" },
190
  { name = "pydantic" },
@@ -198,6 +227,7 @@ dependencies = [
198
  requires-dist = [
199
  { name = "environs", specifier = ">=14.1.1" },
200
  { name = "fastapi", specifier = ">=0.115.12" },
 
201
  { name = "loguru", specifier = ">=0.7.3" },
202
  { name = "motor", specifier = ">=3.7.1" },
203
  { name = "pydantic", specifier = ">=2.11.4" },
 
135
  { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
136
  ]
137
 
138
+ [[package]]
139
+ name = "httpcore"
140
+ version = "1.0.9"
141
+ source = { registry = "https://pypi.org/simple" }
142
+ dependencies = [
143
+ { name = "certifi" },
144
+ { name = "h11" },
145
+ ]
146
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
147
+ wheels = [
148
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
149
+ ]
150
+
151
+ [[package]]
152
+ name = "httpx"
153
+ version = "0.28.1"
154
+ source = { registry = "https://pypi.org/simple" }
155
+ dependencies = [
156
+ { name = "anyio" },
157
+ { name = "certifi" },
158
+ { name = "httpcore" },
159
+ { name = "idna" },
160
+ ]
161
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
162
+ wheels = [
163
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
164
+ ]
165
+
166
  [[package]]
167
  name = "idna"
168
  version = "3.10"
 
213
  dependencies = [
214
  { name = "environs" },
215
  { name = "fastapi" },
216
+ { name = "httpx" },
217
  { name = "loguru" },
218
  { name = "motor" },
219
  { name = "pydantic" },
 
227
  requires-dist = [
228
  { name = "environs", specifier = ">=14.1.1" },
229
  { name = "fastapi", specifier = ">=0.115.12" },
230
+ { name = "httpx", specifier = ">=0.28.1" },
231
  { name = "loguru", specifier = ">=0.7.3" },
232
  { name = "motor", specifier = ">=3.7.1" },
233
  { name = "pydantic", specifier = ">=2.11.4" },