dexter2389 commited on
Commit
7712aa8
·
1 Parent(s): 558414b

Update and restructure the main app and dockerfile to be work with uv

Browse files
Dockerfile CHANGED
@@ -1,19 +1,44 @@
1
- FROM python:3.13-slim-bookworm
 
 
2
 
3
  RUN apt-get update \
4
- && apt-get install -y --no-install-recommends gcc g++ cmake git \
5
  && apt-get clean \
6
  && rm -rf /var/lib/apt/lists/*
7
 
8
- RUN useradd -m -u 1000 user
9
- USER user
10
- ENV PATH="/home/user/.local/bin:$PATH"
11
 
12
- WORKDIR /app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- COPY --chown=user ./requirements.txt requirements.txt
15
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
16
 
17
- COPY --chown=user ./app.py app.py
18
- COPY --chown=user ./frontend.html frontend.html
19
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ FROM python:3.13-slim-bookworm AS builder
2
+
3
+ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy CMAKE_ARGS="-DGGML_BLAS=ON -DGGML_BLAS_VENDOR=OpenBLAS -DGGML_NATIVE=OFF"
4
 
5
  RUN apt-get update \
6
+ && apt-get install -y --no-install-recommends gcc g++ cmake ninja-build git pkg-config libopenblas-dev \
7
  && apt-get clean \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
 
 
11
 
12
+ ENV PATH="/root/.local/bin/:$PATH"
13
+
14
+ RUN --mount=type=cache,target=/root/.cache/uv \
15
+ --mount=type=bind,source=uv.lock,target=uv.lock \
16
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
17
+ uv sync --frozen --no-install-project --no-group dev --no-group test
18
+
19
+ FROM python:3.13-slim-bookworm AS runtime
20
+
21
+ ENV \
22
+ PYTHONUNBUFFERED=1 \
23
+ PYTHONFAULTHANDLER=1 \
24
+ DEBIAN_FRONTEND=noninteractive
25
+
26
+ RUN apt-get update \
27
+ && apt-get install -y libgomp1 libopenblas-dev \
28
+ && apt-get clean \
29
+ && rm -rf /var/lib/apt/lists/*
30
+
31
+ RUN useradd -m -u 1000 arcana
32
+ USER arcana
33
+
34
+ COPY --chown=arcana --from=builder /.venv /.venv
35
 
36
+ COPY --chown=arcana ./app /app
37
+
38
+ ENV PATH="/.venv/bin:$PATH"
39
+ ENV PYTHONPATH="/app:/.venv/lib/python3.13/dist-packages"
40
+
41
+ WORKDIR /app
42
 
43
+ EXPOSE 7860
44
+ CMD ["uvicorn", "app.main:app", "--workers", "2", "--host", "0.0.0.0", "--port", "7860"]
 
app.py → app/app/main.py RENAMED
@@ -2,18 +2,25 @@ import logging
2
  import os
3
  import time
4
  from contextlib import asynccontextmanager
 
5
  from enum import StrEnum
 
6
 
7
  from arcana_codex import (
8
  AdUnitsFetchModel,
9
  AdUnitsIntegrateModel,
10
  ArcanaCodexClient,
11
  )
 
 
 
12
  from llama_cpp import Llama
13
- from fastapi import FastAPI, Request
14
- from pydantic import BaseModel
15
  from starlette.responses import FileResponse
16
 
 
 
17
 
18
  class SupportedModelPipes(StrEnum):
19
  Gemma3 = "gemma3"
@@ -22,9 +29,14 @@ class SupportedModelPipes(StrEnum):
22
  SmolLLM2Reasoning = "smollm2-reasoning"
23
 
24
 
 
 
 
 
 
25
  smollm2_pipeline = Llama.from_pretrained(
26
- repo_id="tensorblock/SmolLM2-135M-Instruct-GGUF",
27
- filename="SmolLM2-135M-Instruct-Q8_0.gguf",
28
  verbose=False,
29
  )
30
 
@@ -56,16 +68,67 @@ class ChatResponse(BaseModel):
56
  response: str
57
 
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  # Configure logging
60
  logging.basicConfig(level=logging.INFO)
61
  logger = logging.getLogger(__name__)
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  @asynccontextmanager
65
  async def lifespan(app: FastAPI): # noqa: ARG001
66
  # Set API key in FastAPI app
67
  app.ARCANA_API_KEY = os.environ.get("ARCANA_API_KEY", "")
68
 
 
 
 
 
69
  logging.info("Application started")
70
 
71
  yield
@@ -79,11 +142,43 @@ async def lifespan(app: FastAPI): # noqa: ARG001
79
  app = FastAPI(lifespan=lifespan)
80
 
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  @app.post("/chat", response_model=ChatResponse)
83
- def chat(payload: ChatRequest, request: Request):
 
 
 
 
84
  logger.info(f"Received message: {payload.message}")
85
 
86
- client = ArcanaCodexClient(request.app.ARCANA_API_KEY)
 
 
87
  fetch_payload = AdUnitsFetchModel(query=payload.message)
88
  ad_fetch_response = client.fetch_ad_units(fetch_payload)
89
 
@@ -122,9 +217,22 @@ def chat(payload: ChatRequest, request: Request):
122
  "integrated_content"
123
  )
124
 
 
 
 
 
 
 
 
 
125
  return ChatResponse(response=integrated_content)
126
 
127
 
128
  @app.get("/")
129
- def frontend():
130
- return FileResponse("frontend.html")
 
 
 
 
 
 
2
  import os
3
  import time
4
  from contextlib import asynccontextmanager
5
+ from datetime import UTC, datetime
6
  from enum import StrEnum
7
+ from typing import Annotated
8
 
9
  from arcana_codex import (
10
  AdUnitsFetchModel,
11
  AdUnitsIntegrateModel,
12
  ArcanaCodexClient,
13
  )
14
+ from bson.objectid import ObjectId
15
+ from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
16
+ from fastapi.responses import JSONResponse
17
  from llama_cpp import Llama
18
+ from pydantic import BaseModel, EmailStr
19
+ from pymongo.mongo_client import MongoClient
20
  from starlette.responses import FileResponse
21
 
22
+ __version__ = "0.0.0"
23
+
24
 
25
  class SupportedModelPipes(StrEnum):
26
  Gemma3 = "gemma3"
 
29
  SmolLLM2Reasoning = "smollm2-reasoning"
30
 
31
 
32
+ class LogEvent(StrEnum):
33
+ CHAT_INTERACTION = "chat_interaction"
34
+ LOGIN = "login"
35
+
36
+
37
  smollm2_pipeline = Llama.from_pretrained(
38
+ repo_id="HuggingFaceTB/SmolLM2-360M-Instruct-GGUF",
39
+ filename="smollm2-360m-instruct-q8_0.gguf",
40
  verbose=False,
41
  )
42
 
 
68
  response: str
69
 
70
 
71
+ class LoginRequest(BaseModel):
72
+ email_id: EmailStr
73
+ access_key: str
74
+
75
+
76
+ class LoginResponse(BaseModel):
77
+ verified_id: str
78
+
79
+
80
+ class User(BaseModel):
81
+ _id: ObjectId
82
+ email_id: EmailStr
83
+
84
+
85
  # Configure logging
86
  logging.basicConfig(level=logging.INFO)
87
  logger = logging.getLogger(__name__)
88
 
89
 
90
+ def verify_authorization_header(
91
+ request: Request, authorization: str | None = Header(None)
92
+ ) -> User:
93
+ if not authorization:
94
+ raise HTTPException(
95
+ status_code=status.HTTP_401_UNAUTHORIZED,
96
+ detail="Authorization header is missing",
97
+ )
98
+
99
+ try:
100
+ scheme, token = authorization.split()
101
+ if scheme.lower() != "bearer":
102
+ raise HTTPException(
103
+ status_code=status.HTTP_401_UNAUTHORIZED,
104
+ detail="Invalid authentication scheme. Bearer required.",
105
+ )
106
+
107
+ user = request.app.mongo_db["users"].find_one({"_id": ObjectId(token)})
108
+ if not user:
109
+ raise HTTPException(
110
+ status_code=status.HTTP_401_UNAUTHORIZED,
111
+ detail="Invalid verified_id",
112
+ )
113
+
114
+ return User(**user)
115
+
116
+ except ValueError:
117
+ raise HTTPException(
118
+ status_code=status.HTTP_401_UNAUTHORIZED,
119
+ detail="Invalid authorization header format",
120
+ )
121
+
122
+
123
  @asynccontextmanager
124
  async def lifespan(app: FastAPI): # noqa: ARG001
125
  # Set API key in FastAPI app
126
  app.ARCANA_API_KEY = os.environ.get("ARCANA_API_KEY", "")
127
 
128
+ app.mongo_db = MongoClient(
129
+ os.environ.get("MONGO_URI", "mongodb+srv://localhost:27017/")
130
+ )["arcana_hf_demo"]
131
+
132
  logging.info("Application started")
133
 
134
  yield
 
142
  app = FastAPI(lifespan=lifespan)
143
 
144
 
145
+ @app.get("/health")
146
+ async def health_check():
147
+ return JSONResponse({"health_check": "pass"})
148
+
149
+
150
+ @app.post("/login", response_model=LoginResponse)
151
+ def login(payload: LoginRequest, request: Request):
152
+ user = request.app.mongo_db["users"].find_one(
153
+ {"email_id": payload.email_id, "access_key": payload.access_key}
154
+ )
155
+ if user:
156
+ request.app.mongo_db["logs"].insert_one(
157
+ {
158
+ "email_id": user["email_id"],
159
+ "timestamp": datetime.now(UTC),
160
+ "event": LogEvent.LOGIN,
161
+ }
162
+ )
163
+ verified_id = user["_id"]
164
+ return LoginResponse(verified_id=str(verified_id))
165
+ else:
166
+ raise HTTPException(
167
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
168
+ )
169
+
170
+
171
  @app.post("/chat", response_model=ChatResponse)
172
+ def chat(
173
+ payload: ChatRequest,
174
+ request: Request,
175
+ user: Annotated[User, Depends(verify_authorization_header)],
176
+ ):
177
  logger.info(f"Received message: {payload.message}")
178
 
179
+ client = ArcanaCodexClient(
180
+ api_key=request.app.ARCANA_API_KEY, base_url="http://gateway-backend/api/public"
181
+ )
182
  fetch_payload = AdUnitsFetchModel(query=payload.message)
183
  ad_fetch_response = client.fetch_ad_units(fetch_payload)
184
 
 
217
  "integrated_content"
218
  )
219
 
220
+ request.app.mongo_db["logs"].insert_one(
221
+ {
222
+ "email_id": user.email_id,
223
+ "timestamp": datetime.now(UTC),
224
+ "event": LogEvent.CHAT_INTERACTION,
225
+ }
226
+ )
227
+
228
  return ChatResponse(response=integrated_content)
229
 
230
 
231
  @app.get("/")
232
+ async def read_index():
233
+ return FileResponse("./static/index.html")
234
+
235
+
236
+ @app.get("/login")
237
+ async def read_login():
238
+ return FileResponse("./static/login.html")
frontend.html → app/static/index.html RENAMED
@@ -41,6 +41,7 @@
41
 
42
  <nav class="flex items-center space-x-6">
43
  <a href="https://arcana.ad/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
 
44
  </nav>
45
  </div>
46
  </header>
@@ -297,6 +298,20 @@
297
 
298
  <script>
299
  document.addEventListener('DOMContentLoaded', () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  const chatForm = document.getElementById('chat-form');
301
  const userInput = document.getElementById('user-input');
302
  const chatContainer = document.getElementById('chat-container');
@@ -481,10 +496,12 @@
481
 
482
  try {
483
  // Send message to API
 
484
  const response = await fetch('/chat', {
485
  method: 'POST',
486
  headers: {
487
  'Content-Type': 'application/json',
 
488
  },
489
  body: JSON.stringify({
490
  message: message,
@@ -493,6 +510,12 @@
493
  });
494
 
495
  if (!response.ok) {
 
 
 
 
 
 
496
  throw new Error('API request failed');
497
  }
498
 
 
41
 
42
  <nav class="flex items-center space-x-6">
43
  <a href="https://arcana.ad/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
44
+ <button id="logout-button" class="text-gray-600 hover:text-gray-900 font-medium">Logout</button>
45
  </nav>
46
  </div>
47
  </header>
 
298
 
299
  <script>
300
  document.addEventListener('DOMContentLoaded', () => {
301
+ // Check if user is logged in
302
+ const verifiedId = sessionStorage.getItem('verifiedId');
303
+ if (!verifiedId) {
304
+ window.location.href = '/login';
305
+ return;
306
+ }
307
+
308
+ // Logout functionality
309
+ const logoutButton = document.getElementById('logout-button');
310
+ logoutButton.addEventListener('click', () => {
311
+ sessionStorage.removeItem('verifiedId');
312
+ window.location.href = '/login';
313
+ });
314
+
315
  const chatForm = document.getElementById('chat-form');
316
  const userInput = document.getElementById('user-input');
317
  const chatContainer = document.getElementById('chat-container');
 
496
 
497
  try {
498
  // Send message to API
499
+ const verifiedId = sessionStorage.getItem('verifiedId');
500
  const response = await fetch('/chat', {
501
  method: 'POST',
502
  headers: {
503
  'Content-Type': 'application/json',
504
+ 'Authorization': `Bearer ${verifiedId}`
505
  },
506
  body: JSON.stringify({
507
  message: message,
 
510
  });
511
 
512
  if (!response.ok) {
513
+ // If unauthorized, redirect to login
514
+ if (response.status === 401) {
515
+ sessionStorage.removeItem('verifiedId');
516
+ window.location.href = '/login';
517
+ return;
518
+ }
519
  throw new Error('API request failed');
520
  }
521
 
app/static/login.html ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Login - AI Chat Assistant</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ primary: {
15
+ DEFAULT: '#3b82f6',
16
+ foreground: '#ffffff',
17
+ },
18
+ secondary: {
19
+ DEFAULT: '#f3f4f6',
20
+ foreground: '#1f2937',
21
+ },
22
+ }
23
+ }
24
+ }
25
+ }
26
+ </script>
27
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
28
+ </head>
29
+
30
+ <body class="bg-gray-50 text-gray-800 font-['Inter'] min-h-screen flex flex-col">
31
+ <!-- Header -->
32
+ <header class="border-b border-gray-200 p-4">
33
+ <div class="max-w-screen-xl mx-auto flex items-center justify-between">
34
+ <div class="flex items-center">
35
+ <h1 class="text-xl font-medium">OpenGPT with <span class="font-semibold"
36
+ style="color: #067ff3;">Ar</span><span class="font-semibold"
37
+ style="color: #f7cd1b;">ca</span><span class="font-semibold" style="color: #07b682;">na</span>
38
+ </h1>
39
+ </div>
40
+
41
+ <nav class="flex items-center space-x-6">
42
+ <a href="https://arcana.ad/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
43
+ </nav>
44
+ </div>
45
+ </header>
46
+
47
+ <main class="flex-1 flex items-center justify-center p-4">
48
+ <div class="w-full max-w-md">
49
+ <div class="bg-white rounded-lg shadow-md p-5 sm:p-8">
50
+ <div class="text-center mb-6">
51
+ <div
52
+ class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center text-2xl font-semibold mb-4 mx-auto">
53
+ AR
54
+ </div>
55
+ <h2 class="text-xl sm:text-2xl font-semibold">Welcome</h2>
56
+ <p class="text-gray-500 text-sm sm:text-base">Sign in to access the <span
57
+ class="font-medium">OpenGPT with </span><span class="font-semibold"
58
+ style="color: #067ff3;">Ar</span><span class="font-semibold"
59
+ style="color: #f7cd1b;">ca</span><span class="font-semibold"
60
+ style="color: #07b682;">na</span>
61
+ </p>
62
+ </div>
63
+
64
+ <form id="login-form" class="space-y-4">
65
+ <div id="error-message" class="hidden bg-red-50 text-red-700 p-3 rounded-md text-sm"></div>
66
+
67
+ <div>
68
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email ID</label>
69
+ <input type="email" id="email" name="email_id"
70
+ class="w-full px-3 py-2 sm:px-4 sm:py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-base"
71
+ placeholder="Enter your email" required>
72
+ </div>
73
+
74
+ <div>
75
+ <label for="access-key" class="block text-sm font-medium text-gray-700 mb-1">Access Key</label>
76
+ <input type="password" id="access-key" name="access_key"
77
+ class="w-full px-3 py-2 sm:px-4 sm:py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-base"
78
+ placeholder="Enter your access key" required>
79
+ </div>
80
+
81
+ <button type="submit" id="login-button"
82
+ class="w-full bg-blue-600 text-white py-2.5 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-base font-medium mt-2">
83
+ Sign In
84
+ </button>
85
+ </form>
86
+
87
+ <div class="mt-8 pt-6 border-t border-gray-200">
88
+ <p class="text-gray-600 mb-3 text-center text-sm sm:text-base">Don't have an access key?</p>
89
+ <a href="https://forms.google.com/request-access" id="request-access-button"
90
+ class="flex items-center justify-center w-full bg-gradient-to-r from-indigo-50 to-blue-50 text-blue-600 py-3 px-4 rounded-md hover:from-indigo-100 hover:to-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 border border-blue-200 shadow-sm text-sm sm:text-base font-medium"
91
+ target="_blank">
92
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
93
+ stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
94
+ class="mr-2">
95
+ <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
96
+ <circle cx="9" cy="7" r="4"></circle>
97
+ <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
98
+ <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
99
+ </svg>
100
+ Request Access Key
101
+ </a>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </main>
106
+
107
+ <footer class="text-center p-4 text-gray-500 text-sm">
108
+ <p>© 2025 Arcana Inc. All rights reserved.</p>
109
+ </footer>
110
+
111
+ <script>
112
+ document.addEventListener('DOMContentLoaded', () => {
113
+ // Check if user is already logged in
114
+ const verifiedId = sessionStorage.getItem('verifiedId');
115
+ if (verifiedId) {
116
+ window.location.href = '/';
117
+ return;
118
+ }
119
+
120
+ const loginForm = document.getElementById('login-form');
121
+ const loginButton = document.getElementById('login-button');
122
+ const errorMessage = document.getElementById('error-message');
123
+
124
+ loginForm.addEventListener('submit', async (e) => {
125
+ e.preventDefault();
126
+
127
+ // Get form data
128
+ const email = document.getElementById('email').value.trim();
129
+ const accessKey = document.getElementById('access-key').value.trim();
130
+
131
+ if (!email || !accessKey) {
132
+ showError('Please enter both email and access key');
133
+ return;
134
+ }
135
+
136
+ // Disable button and show loading state
137
+ loginButton.disabled = true;
138
+ loginButton.innerHTML = 'Signing in...';
139
+ errorMessage.classList.add('hidden');
140
+
141
+ try {
142
+ // Send login request
143
+ const response = await fetch('/login', {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ body: JSON.stringify({
149
+ email_id: email,
150
+ access_key: accessKey
151
+ }),
152
+ });
153
+
154
+ const data = await response.json();
155
+
156
+ if (!response.ok) {
157
+ throw new Error(data.message || 'Login failed. Please check your credentials.');
158
+ }
159
+
160
+ // Store auth token or user info
161
+ sessionStorage.setItem('verifiedId', data.verified_id);
162
+
163
+ // Redirect to chat page
164
+ window.location.href = '/';
165
+
166
+ } catch (error) {
167
+ console.error('Login error:', error);
168
+ showError(error.message || 'Login failed. Please try again.');
169
+
170
+ // Reset button state
171
+ loginButton.disabled = false;
172
+ loginButton.innerHTML = 'Sign In';
173
+ }
174
+ });
175
+
176
+ function showError(message) {
177
+ errorMessage.textContent = message;
178
+ errorMessage.classList.remove('hidden');
179
+ }
180
+ });
181
+ </script>
182
+ </body>
183
+
184
+ </html>