Skip to content

Commit 7d7894c

Browse files
committed
Fix serving ui from backend container
1 parent 49be146 commit 7d7894c

16 files changed

+78
-55
lines changed

Dockerfile

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,8 @@ COPY ./backend .
4444

4545
# Copy the frontend build
4646
COPY --from=builder /frontend/dist ./ui
47+
COPY --from=builder /frontend/dist/index.html ./ui/404.html
4748

48-
ENTRYPOINT [ "uvicorn", "app.server:app", "--host", "0.0.0.0" ]
49+
ENV PORT=8000
50+
51+
ENTRYPOINT uvicorn app.server:app --host 0.0.0.0 --port $PORT

backend/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ RUN poetry config virtualenvs.create false \
2727
# Copy the rest of application code
2828
COPY . .
2929

30-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --start-interval=1s --retries=3 CMD [ "curl", "-f", "http://localhost:8000/health" ]
30+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --start-interval=1s --retries=3 CMD [ "curl", "-f", "http://localhost:8000/ok" ]
3131

3232
ENTRYPOINT [ "uvicorn", "app.server:app", "--host", "0.0.0.0" ]

backend/app/api/__init__.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
1-
from fastapi import APIRouter
1+
import orjson
2+
from fastapi import APIRouter, Form, UploadFile, HTTPException
23

4+
import app.storage as storage
5+
from app.auth.handlers import AuthedUser
6+
from app.upload import convert_ingestion_input_to_blob, ingest_runnable
37
from app.api.assistants import router as assistants_router
48
from app.api.runs import router as runs_router
59
from app.api.threads import router as threads_router
610

711
router = APIRouter()
812

913

10-
@router.get("/ok")
11-
async def ok():
12-
return {"ok": True}
14+
@router.post("/ingest", description="Upload files to the given assistant.")
15+
async def ingest_files(
16+
files: list[UploadFile], user: AuthedUser, config: str = Form(...)
17+
) -> None:
18+
"""Ingest a list of files."""
19+
config = orjson.loads(config)
20+
21+
assistant_id = config["configurable"].get("assistant_id")
22+
if assistant_id is not None:
23+
assistant = await storage.get_assistant(user["user_id"], assistant_id)
24+
if assistant is None:
25+
raise HTTPException(status_code=404, detail="Assistant not found.")
26+
27+
thread_id = config["configurable"].get("thread_id")
28+
if thread_id is not None:
29+
thread = await storage.get_thread(user["user_id"], thread_id)
30+
if thread is None:
31+
raise HTTPException(status_code=404, detail="Thread not found.")
32+
33+
file_blobs = [convert_ingestion_input_to_blob(file) for file in files]
34+
return ingest_runnable.batch(file_blobs, config)
1335

1436

1537
router.include_router(

backend/app/server.py

+19-32
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import logging
22
import os
33
from pathlib import Path
4+
from typing import Any, MutableMapping
45

5-
from fastapi.exception_handlers import http_exception_handler
66
import httpx
7-
import orjson
8-
from fastapi import FastAPI, Form, UploadFile
7+
from fastapi import FastAPI
8+
from fastapi.exception_handlers import http_exception_handler
99
from fastapi.exceptions import HTTPException
10+
from fastapi.responses import FileResponse
1011
from fastapi.staticfiles import StaticFiles
12+
from starlette.responses import Response
1113

12-
import app.storage as storage
1314
from app.api import router as api_router
14-
from app.auth.handlers import AuthedUser
1515
from app.lifespan import lifespan
16-
from app.upload import convert_ingestion_input_to_blob, ingest_runnable
1716

1817
logger = logging.getLogger(__name__)
1918

@@ -31,41 +30,29 @@ async def httpx_http_status_error_handler(request, exc: httpx.HTTPStatusError):
3130
ROOT = Path(__file__).parent.parent
3231

3332

34-
app.include_router(api_router)
35-
33+
@app.get("/ok")
34+
async def ok():
35+
return {"ok": True}
3636

37-
@app.post("/ingest", description="Upload files to the given assistant.")
38-
async def ingest_files(
39-
files: list[UploadFile], user: AuthedUser, config: str = Form(...)
40-
) -> None:
41-
"""Ingest a list of files."""
42-
config = orjson.loads(config)
4337

44-
assistant_id = config["configurable"].get("assistant_id")
45-
if assistant_id is not None:
46-
assistant = await storage.get_assistant(user["user_id"], assistant_id)
47-
if assistant is None:
48-
raise HTTPException(status_code=404, detail="Assistant not found.")
38+
app.include_router(api_router, prefix="/api")
4939

50-
thread_id = config["configurable"].get("thread_id")
51-
if thread_id is not None:
52-
thread = await storage.get_thread(user["user_id"], thread_id)
53-
if thread is None:
54-
raise HTTPException(status_code=404, detail="Thread not found.")
55-
56-
file_blobs = [convert_ingestion_input_to_blob(file) for file in files]
57-
return ingest_runnable.batch(file_blobs, config)
5840

41+
ui_dir = str(ROOT / "ui")
5942

60-
@app.get("/health")
61-
async def health() -> dict:
62-
return {"status": "ok"}
6343

44+
class StaticFilesSpa(StaticFiles):
45+
async def get_response(
46+
self, path: str, scope: MutableMapping[str, Any]
47+
) -> Response:
48+
res = await super().get_response(path, scope)
49+
if isinstance(res, FileResponse) and res.status_code == 404:
50+
res.status_code = 200
51+
return res
6452

65-
ui_dir = str(ROOT / "ui")
6653

6754
if os.path.exists(ui_dir):
68-
app.mount("", StaticFiles(directory=ui_dir, html=True), name="ui")
55+
app.mount("", StaticFilesSpa(directory=ui_dir, html=True), name="ui")
6956
else:
7057
logger.warn("No UI directory found, serving API only.")
7158

backend/app/storage.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,14 @@ async def get_or_create_user(sub: str) -> tuple[User, bool]:
160160
async with get_pg_pool().acquire() as conn:
161161
if user := await conn.fetchrow('SELECT * FROM "user" WHERE sub = $1', sub):
162162
return user, False
163-
user = await conn.fetchrow(
163+
if user := await conn.fetchrow(
164164
'INSERT INTO "user" (sub) VALUES ($1) ON CONFLICT (sub) DO NOTHING RETURNING *',
165165
sub,
166-
)
167-
return user, True
166+
):
167+
return user, True
168+
if user := await conn.fetchrow('SELECT * FROM "user" WHERE sub = $1', sub):
169+
return user, False
170+
raise RuntimeError("User creation failed.")
168171

169172

170173
async def delete_thread(user_id: str, thread_id: str):

compose.override.yml

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ services:
4545
- --reload
4646
frontend:
4747
container_name: opengpts-frontend
48+
pull_policy: build
4849
build:
4950
context: frontend
5051
depends_on:

deploy.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
langgraph build -t langchain/opengpts-langgraph:0.1.5 --platform linux/amd64,linux/arm64
2+
docker push langchain/opengpts-langgraph:0.1.5
3+
gcloud beta run deploy opengpts-demo-langgraph --image langchain/opengpts-langgraph:0.1.5 --region us-central1 --project langchain-dev --env-vars-file .env.gcp.yaml
4+
5+
docker build -t langchain/opengpts-backend:0.1.1 --platform linux/amd64,linux/arm64 .
6+
docker push langchain/opengpts-backend:0.1.1
7+
gcloud beta run deploy opengpts-demo-backend --image langchain/opengpts-backend:0.1.1 --region us-central1 --project langchain-dev --env-vars-file .env.gcp.yaml

frontend/src/api/assistants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export async function getAssistant(
44
assistantId: string,
55
): Promise<Config | null> {
66
try {
7-
const response = await fetch(`/assistants/${assistantId}`);
7+
const response = await fetch(`/api/assistants/${assistantId}`);
88
if (!response.ok) {
99
return null;
1010
}

frontend/src/api/threads.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Chat } from "../types";
22

33
export async function getThread(threadId: string): Promise<Chat | null> {
44
try {
5-
const response = await fetch(`/threads/${threadId}`);
5+
const response = await fetch(`/api/threads/${threadId}`);
66
if (!response.ok) {
77
return null;
88
}

frontend/src/hooks/useChatList.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function useChatList(): ChatListProps {
3232

3333
useEffect(() => {
3434
async function fetchChats() {
35-
const chats = await fetch("/threads/", {
35+
const chats = await fetch("/api/threads/", {
3636
headers: {
3737
Accept: "application/json",
3838
},
@@ -44,7 +44,7 @@ export function useChatList(): ChatListProps {
4444
}, []);
4545

4646
const createChat = useCallback(async (name: string, assistant_id: string) => {
47-
const response = await fetch(`/threads`, {
47+
const response = await fetch(`/api/threads`, {
4848
method: "POST",
4949
body: JSON.stringify({ assistant_id, name }),
5050
headers: {
@@ -59,7 +59,7 @@ export function useChatList(): ChatListProps {
5959

6060
const deleteChat = useCallback(
6161
async (thread_id: string) => {
62-
await fetch(`/threads/${thread_id}`, {
62+
await fetch(`/api/threads/${thread_id}`, {
6363
method: "DELETE",
6464
headers: {
6565
Accept: "application/json",

frontend/src/hooks/useChatMessages.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Message } from "../types";
33
import { StreamState, mergeMessagesById } from "./useStreamState";
44

55
async function getState(threadId: string) {
6-
const { values, next } = await fetch(`/threads/${threadId}/state`, {
6+
const { values, next } = await fetch(`/api/threads/${threadId}/state`, {
77
headers: {
88
Accept: "application/json",
99
},

frontend/src/hooks/useConfigList.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function useConfigList(): ConfigListProps {
5151

5252
useEffect(() => {
5353
async function fetchConfigs() {
54-
const myConfigs = await fetch("/assistants/", {
54+
const myConfigs = await fetch("/api/assistants/", {
5555
headers: {
5656
Accept: "application/json",
5757
},
@@ -73,7 +73,7 @@ export function useConfigList(): ConfigListProps {
7373
assistantId?: string,
7474
): Promise<string> => {
7575
const confResponse = await fetch(
76-
assistantId ? `/assistants/${assistantId}` : "/assistants",
76+
assistantId ? `/api/assistants/${assistantId}` : "/api/assistants",
7777
{
7878
method: assistantId ? "PUT" : "POST",
7979
body: JSON.stringify({ name, config, public: isPublic }),
@@ -94,7 +94,7 @@ export function useConfigList(): ConfigListProps {
9494
"config",
9595
JSON.stringify({ configurable: { assistant_id } }),
9696
);
97-
await fetch(`/ingest`, {
97+
await fetch(`/api/ingest`, {
9898
method: "POST",
9999
body: formData,
100100
});

frontend/src/hooks/useMessageEditing.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function useMessageEditing(
1313
}, []);
1414
const commitEdits = useCallback(async () => {
1515
if (!threadId) return;
16-
fetch(`/threads/${threadId}/state`, {
16+
fetch(`/api/threads/${threadId}/state`, {
1717
method: "POST",
1818
headers: { "Content-Type": "application/json" },
1919
body: JSON.stringify({ values: Object.values(editing) }),

frontend/src/hooks/useSchemas.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function useSchemas() {
3636

3737
useEffect(() => {
3838
async function save() {
39-
const configSchema = await fetch("/runs/config_schema")
39+
const configSchema = await fetch("/api/runs/config_schema")
4040
.then((r) => r.json())
4141
.then(simplifySchema);
4242
setSchemas({

frontend/src/hooks/useStreamState.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function useStreamState(): StreamStateProps {
3333
setController(controller);
3434
setCurrent({ status: "inflight", messages: input || [] });
3535

36-
await fetchEventSource("/runs/stream", {
36+
await fetchEventSource("/api/runs/stream", {
3737
signal: controller.signal,
3838
method: "POST",
3939
headers: { "Content-Type": "application/json" },

frontend/vite.config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ export default defineConfig({
66
plugins: [react()],
77
server: {
88
watch: {
9-
usePolling: true
9+
usePolling: true,
1010
},
1111
proxy: {
12-
"^/(assistants|threads|ingest|runs)": {
12+
"/api": {
1313
target: process.env.VITE_BACKEND_URL || "http://127.0.0.1:8100",
1414
changeOrigin: true,
1515
rewrite: (path) => path.replace("/____LANGSERVE_BASE_URL", ""),

0 commit comments

Comments
 (0)