Skip to content

Commit ec9128a

Browse files
authored
Add github actions to deploy docker imager to hetzner (#39)
* feat(docker): add Dockerfile * add mcp endpoint * docker: publish docker to hetzner
1 parent 87225d5 commit ec9128a

File tree

9 files changed

+130
-66
lines changed

9 files changed

+130
-66
lines changed

.gitignore

Lines changed: 0 additions & 51 deletions
This file was deleted.

apps/meshjs-rag/.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.env
2+
.vscode/
3+
.venv/
4+
**/__pycache__/
5+
docs/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Publish to Hetzner
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
publish_images:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: checkout
13+
uses: actions/checkout@v4
14+
15+
- name: build image
16+
run: docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/meshai-backend:latest .
17+
18+
- name: push image to the docker hub
19+
run: |
20+
echo ${{ secrets.DOCKER_HUB_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin
21+
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/meshai-backend:latest
22+
23+
- name: Deploy to Hetzner via SSH
24+
uses: appleboy/[email protected]
25+
with:
26+
host: ${{ secrets.HETZNER_HOST }}
27+
username: ${{ secrets.HETZNER_USERNAME }}
28+
key: ${{ secrets.SSH_PRIVATE_KEY }}
29+
script: |
30+
docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/meshai-backend:latest
31+
docker stop meshai-backend || true
32+
docker rm meshai-backend || true
33+
docker run -d --name meshai-backend --env-file ./.env --restart always -p 127.0.0.1:8000:8000 ${{ secrets.DOCKER_HUB_USERNAME }}/meshai-backend:latest

apps/meshjs-rag/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM python:3.13.5-slim
2+
3+
WORKDIR /code
4+
5+
COPY ./requirements.txt /code/requirements.txt
6+
7+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8+
9+
COPY ./main.py /code/main.py
10+
11+
COPY ./app /code/app
12+
13+
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "main:app", "-b", "0.0.0.0:8000", "--workers", "4"]
14+

apps/meshjs-rag/app/api/v1/ask_mesh_ai.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
from typing import Literal, List, Optional
2-
from fastapi import APIRouter, Depends, HTTPException, status
2+
from fastapi import APIRouter, Depends, HTTPException, status, Header
33
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
44
from fastapi.responses import StreamingResponse
55
from pydantic import BaseModel
66
from supabase import AsyncClient
77
import openai
8+
from dotenv import load_dotenv
9+
load_dotenv()
810
import os
911

1012
from app.services.openai import OpenAIService
1113
from app.utils.get_context import get_context
1214
from app.db.client import get_db_client
1315

16+
1417
router = APIRouter()
15-
openai_service = OpenAIService()
1618
security = HTTPBearer()
1719

1820
###########################################################################################################
@@ -27,6 +29,10 @@ class ChatCompletionRequest(BaseModel):
2729
messages: List[ChatMessage]
2830
stream: Optional[bool] = False
2931

32+
class MCPRequestBody(BaseModel):
33+
query: str
34+
model: str
35+
3036
###########################################################################################################
3137
# ENDPOINTS
3238
###########################################################################################################
@@ -40,6 +46,12 @@ async def ask_mesh_ai(body: ChatCompletionRequest, credentials: HTTPAuthorizatio
4046
detail="You are not authorized"
4147
)
4248

49+
openai_api_key = os.getenv("OPENAI_KEY") or None
50+
if openai_api_key is None:
51+
raise ValueError("OpenAI api key is missing")
52+
53+
openai_service = OpenAIService(openai_api_key)
54+
4355
try:
4456
question = body.messages[-1].content
4557

@@ -54,6 +66,43 @@ async def ask_mesh_ai(body: ChatCompletionRequest, credentials: HTTPAuthorizatio
5466
detail=f"An OpenAI API error occurred: {e}"
5567
)
5668
except Exception as e:
69+
raise HTTPException(
70+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
71+
detail=f"An unexpected error occurred: {e}"
72+
)
73+
74+
75+
###########################################################################################################
76+
@router.post("/mcp")
77+
async def ask_mesh_ai(body: MCPRequestBody, authorization: str = Header(None), supabase: AsyncClient = Depends(get_db_client)):
78+
79+
if not authorization or not authorization.startswith("Bearer"):
80+
print("error")
81+
raise HTTPException(
82+
status_code=status.HTTP_401_UNAUTHORIZED,
83+
detail="You are not authorized"
84+
)
85+
86+
try:
87+
OPENAI_KEY = authorization.split(" ")[-1]
88+
openai_service = OpenAIService(OPENAI_KEY)
89+
90+
question = body.query
91+
model = body.model
92+
93+
embedded_query = await openai_service.embed_query(question)
94+
context = await get_context(embedded_query, supabase)
95+
response = await openai_service.get_mcp_answer(question=question, context=context, model=model)
96+
return response
97+
98+
except (openai.APIError, openai.AuthenticationError, openai.RateLimitError) as e:
99+
print(e)
100+
raise HTTPException(
101+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
102+
detail=f"An OpenAI API error occurred: {e}"
103+
)
104+
except Exception as e:
105+
print(e)
57106
raise HTTPException(
58107
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
59108
detail=f"An unexpected error occurred: {e}"

apps/meshjs-rag/app/api/v1/ingest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
from app.utils.checksum import calculate_checksum
1818
from app.utils.extract_title import extract_chunk_title
1919

20+
openai_api_key = os.getenv("OPENAI_KEY") or None
21+
if openai_api_key is None:
22+
raise ValueError("OpenAI api key is missing")
23+
2024
router = APIRouter()
21-
openai_service = OpenAIService()
25+
openai_service = OpenAIService(openai_api_key=openai_api_key)
2226
security = HTTPBearer()
2327

2428
###########################################################################################################

apps/meshjs-rag/app/services/openai.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
from openai import AsyncOpenAI
2-
from dotenv import load_dotenv
3-
load_dotenv()
4-
import os
52
from typing import List
63
from tenacity import retry, wait_random_exponential, stop_after_attempt
74
import json
85

9-
openai_api_key = os.getenv("OPENAI_KEY") or None
10-
if openai_api_key is None:
11-
raise ValueError("OpenAI api key is missing")
12-
136
DOCUMENT_CONTEXT_PROMPT = """
147
<document>
158
{doc_content}
@@ -27,7 +20,7 @@
2720
"""
2821

2922
class OpenAIService:
30-
def __init__(self):
23+
def __init__(self, openai_api_key):
3124
self.client = AsyncOpenAI(api_key=openai_api_key)
3225

3326
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
@@ -82,7 +75,7 @@ async def embed_query(self, text: str) -> List[float]:
8275
return response.data[0].embedding
8376

8477
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6), reraise=True)
85-
async def get_answer(self, question: str, context: str):
78+
async def get_answer(self, question: str, context: str, model="gpt-4o-mini"):
8679
messages = [
8780
{
8881
"role": "system",
@@ -94,9 +87,25 @@ async def get_answer(self, question: str, context: str):
9487
}
9588
]
9689

97-
stream = await self._chat(messages=messages, stream=True)
90+
stream = await self._chat(messages=messages, stream=True, model=model)
9891

9992
async for chunk in stream:
10093
yield f"data: {json.dumps(chunk.model_dump())}\n\n"
10194

102-
yield "data: [DONE]\n\n"
95+
yield "data: [DONE]\n\n"
96+
97+
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6), reraise=True)
98+
async def get_mcp_answer(self, question: str, context: str, model="gpt-4o-mini"):
99+
messages = [
100+
{
101+
"role": "system",
102+
"content": "You are a MeshJS expert assistant. Help developers with MeshJS questions using the provided context.\n\nUse the documentation context to answer questions about MeshJS and Cardano development. Provide accurate code examples and explanations based on the context provided.\n\nWhen answering:\n- Give direct, helpful answers based on the context\n- Include relevant code examples when available\n- Explain concepts clearly for developers\n- Include any links present in the context for additional resources\n- If the context doesn't cover something, say so\n- Don't make up APIs or methods not in the documentation\n\nBe concise but thorough. Focus on practical, actionable guidance for MeshJS development."
103+
},
104+
{
105+
"role": "user",
106+
"content": f"""Context: {context}\n\nQuestion: {question}""",
107+
}
108+
]
109+
110+
response = await self._chat(messages=messages, model=model)
111+
return response.choices[0].message.content

apps/meshjs-rag/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from app.api.v1.api import api_router
33
import uvicorn
44

5-
app = FastAPI()
5+
app = FastAPI(title="MeshAI Backend")
66
app.include_router(api_router, prefix="/api/v1", tags=["api"])
77

88
@app.get("/")

apps/meshjs-rag/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ deprecation==2.1.0
88
distro==1.9.0
99
fastapi==0.116.1
1010
gotrue==2.12.3
11+
gunicorn==23.0.0
1112
h11==0.16.0
1213
h2==4.2.0
1314
hpack==4.1.0

0 commit comments

Comments
 (0)