mirror of
https://github.com/aimingmed/aimingmed-ai.git
synced 2026-02-03 21:53:16 +08:00
commit
34d16712fe
142
.github/workflows/build.yml
vendored
Normal file
142
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
name: CI - build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/$(echo $GITHUB_REPOSITORY | tr '[A-Z]' '[a-z]')/aimingmed-ai-backend
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check disk space
|
||||
run: df -h
|
||||
- name: Cleanup Docker resources
|
||||
if: always()
|
||||
run: |
|
||||
docker system prune -a -f --volumes
|
||||
- name: Remove unnecessary files
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
- name: Check disk space
|
||||
run: df -h
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: develop
|
||||
- name: Log in to GitHub Packages
|
||||
run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Pull image
|
||||
run: |
|
||||
docker pull ${{ env.IMAGE }}:latest || true
|
||||
- name: Check disk space
|
||||
if: always()
|
||||
run: df -h
|
||||
- name: Build image
|
||||
run: |
|
||||
docker build \
|
||||
--cache-from ${{ env.IMAGE }}:latest \
|
||||
--tag ${{ env.IMAGE }}:latest \
|
||||
--file ./app/backend/Dockerfile.prod \
|
||||
"./app/backend"
|
||||
- name: Push image
|
||||
run: |
|
||||
docker push ${{ env.IMAGE }}:latest
|
||||
- name: Check disk space
|
||||
if: always()
|
||||
run: df -h
|
||||
- name: Cleanup Docker resources
|
||||
if: always()
|
||||
run: docker system prune -a -f --volumes
|
||||
- name: Check disk space
|
||||
if: always()
|
||||
run: df -h
|
||||
|
||||
test:
|
||||
name: Test Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check disk space
|
||||
run: df -h
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: develop
|
||||
- name: Log in to GitHub Packages
|
||||
run: echo ${GITHUB_TOKEN} | docker login -u ${GITHUB_ACTOR} --password-stdin ghcr.io
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Cleanup Docker resources
|
||||
if: always()
|
||||
run: docker system prune -a -f --volumes
|
||||
- name: Remove unnecessary files
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
- name: Pull image
|
||||
run: |
|
||||
docker pull ${{ env.IMAGE }}:latest || true
|
||||
- name: Check disk space
|
||||
if: always()
|
||||
run: df -h
|
||||
- name: Build image
|
||||
run: |
|
||||
docker build \
|
||||
--cache-from ${{ env.IMAGE }}:latest \
|
||||
--tag ${{ env.IMAGE }}:latest \
|
||||
--file ./app/backend/Dockerfile.prod \
|
||||
"./app/backend"
|
||||
- name: Check disk space
|
||||
if: always()
|
||||
run: df -h
|
||||
- name: Validate Docker image
|
||||
run: docker inspect ${{ env.IMAGE }}:latest
|
||||
- name: Run container
|
||||
run: |
|
||||
docker run \
|
||||
-d \
|
||||
-e DEEPSEEK_API_KEY=${{ secrets.DEEPSEEK_API_KEY }} \
|
||||
-e TAVILY_API_KEY=${{ secrets.TAVILY_API_KEY }} \
|
||||
-e ENVIRONMENT=dev \
|
||||
-e TESTING=0 \
|
||||
-e PORT=8765 \
|
||||
-e LOG_LEVEL=DEBUG \
|
||||
--name backend-backend \
|
||||
-p 8004:8765 \
|
||||
${{ env.IMAGE }}:latest
|
||||
- name: Monitor memory usage
|
||||
run: free -h
|
||||
- name: Get container logs
|
||||
if: failure()
|
||||
run: docker logs backend-backend
|
||||
- name: Pytest
|
||||
run: docker exec backend-backend pipenv run python -m pytest .
|
||||
# - name: Flake8
|
||||
# run: docker exec backend-backend pipenv run python -m flake8 .
|
||||
# - name: Black
|
||||
# run: docker exec backend-backend pipenv run python -m black . --check
|
||||
- name: isort
|
||||
if: always()
|
||||
run: docker exec backend-backend pipenv run python -m isort . --check-only
|
||||
- name: Cleanup container at end of job
|
||||
if: always()
|
||||
run: docker stop backend-backend || true && docker rm backend-backend || true
|
||||
11
Pipfile
Normal file
11
Pipfile
Normal file
@ -0,0 +1,11 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
@ -1,4 +1,4 @@
|
||||
[](https://github.com/aimingmed/aimingmed-ai/actions/workflows/app-testing.yml)
|
||||
[](https://github.com/aimingmed/aimingmed-ai/actions/workflows/build.yml)
|
||||
|
||||
## Important note:
|
||||
|
||||
|
||||
11
app/Pipfile
Normal file
11
app/Pipfile
Normal file
@ -0,0 +1,11 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
20
app/Pipfile.lock
generated
Normal file
20
app/Pipfile.lock
generated
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "ed6d5d614626ae28e274e453164affb26694755170ccab3aa5866f093d51d3e4"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.11"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {},
|
||||
"develop": {}
|
||||
}
|
||||
50
app/README.md
Normal file
50
app/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# How to work with this app repository
|
||||
|
||||
Build the images:
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
I
|
||||
|
||||
# Run the tests for backend:
|
||||
|
||||
```bash
|
||||
docker compose exec backend pipenv run python -m pytest --disable-warnings --cov="."
|
||||
```
|
||||
|
||||
Lint:
|
||||
|
||||
```bash
|
||||
docker compose exec backend pipenv run flake8 .
|
||||
```
|
||||
|
||||
Run Black and isort with check options:
|
||||
|
||||
```bash
|
||||
docker compose exec backend pipenv run black . --check
|
||||
docker compose exec backend pipenv run isort . --check-only
|
||||
```
|
||||
|
||||
Make code changes with Black and isort:
|
||||
|
||||
```bash
|
||||
docker compose exec backend pipenv run black .
|
||||
docker compose exec backend pipenv run isort .
|
||||
```
|
||||
|
||||
# Postgres
|
||||
|
||||
Want to access the database via psql?
|
||||
|
||||
```bash
|
||||
docker compose exec -it database psql -U postgres
|
||||
```
|
||||
|
||||
Then, you can connect to the database and run SQL queries. For example:
|
||||
|
||||
```sql
|
||||
# \c web_dev
|
||||
# \dt
|
||||
```
|
||||
0
app/backend/.dockerignore
Normal file
0
app/backend/.dockerignore
Normal file
23
app/backend/Dockerfile
Normal file
23
app/backend/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
# pull official base image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# set working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# install system dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential netcat-traditional gcc \
|
||||
&& apt-get clean
|
||||
|
||||
# install python dependencies
|
||||
RUN pip install --upgrade pip setuptools wheel -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
RUN pip install pipenv -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
COPY ./Pipfile .
|
||||
RUN pipenv install --deploy --dev
|
||||
|
||||
# add app
|
||||
COPY . .
|
||||
84
app/backend/Dockerfile.prod
Normal file
84
app/backend/Dockerfile.prod
Normal file
@ -0,0 +1,84 @@
|
||||
###########
|
||||
# BUILDER #
|
||||
###########
|
||||
|
||||
# pull official base image
|
||||
FROM python:3.11-slim-bookworm AS builder
|
||||
|
||||
|
||||
# set working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# install system dependencies
|
||||
# RUN apt-get update && apt-get -y install build-essential \
|
||||
# && apt-get clean \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# install python dependencies
|
||||
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pipenv && rm -rf ~/.cache/pip
|
||||
COPY ./Pipfile .
|
||||
RUN pipenv install --deploy --dev
|
||||
|
||||
# add app
|
||||
COPY . /usr/src/app
|
||||
# RUN pipenv run pip install black==23.12.1 flake8==7.0.0 isort==5.13.2
|
||||
# RUN pipenv run flake8 .
|
||||
# RUN pipenv run black --exclude=migrations . --check
|
||||
# RUN pipenv run isort . --check-only
|
||||
|
||||
#########
|
||||
# FINAL #
|
||||
#########
|
||||
|
||||
# pull official base image
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
# create directory for the app user
|
||||
RUN mkdir -p /home/app
|
||||
|
||||
# create the app user
|
||||
RUN addgroup --system app && adduser --system --group app
|
||||
|
||||
|
||||
# create the appropriate directories
|
||||
ENV HOME=/home/app
|
||||
ENV APP_HOME=/home/app/backend
|
||||
RUN mkdir $APP_HOME
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV ENVIRONMENT=prod
|
||||
ENV TESTING=0
|
||||
|
||||
# install system dependencies
|
||||
# RUN apt-get update \
|
||||
# && apt-get -y install build-essential \
|
||||
# && apt-get clean \
|
||||
#&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# install python dependencies
|
||||
COPY --from=builder /usr/src/app/Pipfile .
|
||||
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pipenv && rm -rf ~/.cache/pip
|
||||
RUN pipenv install --deploy --dev
|
||||
RUN pipenv run pip install "uvicorn[standard]==0.26.0"
|
||||
|
||||
# add app
|
||||
COPY . $APP_HOME
|
||||
|
||||
# chown all the files to the app user
|
||||
RUN chown -R app:app $APP_HOME
|
||||
|
||||
# change to the app user
|
||||
USER app
|
||||
|
||||
# expose the port the app runs on
|
||||
EXPOSE 8765
|
||||
|
||||
# run uvicorn
|
||||
CMD ["pipenv", "run", "uvicorn", "main:app", "--reload", "--workers", "1", "--host", "0.0.0.0", "--port", "8765"]
|
||||
30
app/backend/Pipfile
Normal file
30
app/backend/Pipfile
Normal file
@ -0,0 +1,30 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
fastapi = "==0.115.9"
|
||||
starlette = "==0.45.3"
|
||||
uvicorn = "==0.26.0"
|
||||
pydantic-settings = "==2.1.0"
|
||||
gunicorn = "==21.0.1"
|
||||
python-decouple = "==3.8"
|
||||
pyyaml = "==6.0.1"
|
||||
docker = "*"
|
||||
chromadb = "*"
|
||||
sentence-transformers = "*"
|
||||
langchain = "*"
|
||||
langchain-deepseek = "*"
|
||||
|
||||
[dev-packages]
|
||||
httpx = "==0.26.0"
|
||||
pytest = "==7.4.4"
|
||||
pytest-cov = "==4.1.0"
|
||||
pytest-mock = "==3.10.0"
|
||||
flake8 = "==7.0.0"
|
||||
black = "==23.12.1"
|
||||
isort = "==5.13.2"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
2896
app/backend/Pipfile.lock
generated
Normal file
2896
app/backend/Pipfile.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
app/backend/api/chatbot.py
Normal file
54
app/backend/api/chatbot.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from decouple import config
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
from langchain_deepseek import ChatDeepSeek
|
||||
|
||||
from models.adaptive_rag import grading, query, routing
|
||||
|
||||
from .utils import ConnectionManager
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Load environment variables
|
||||
os.environ["DEEPSEEK_API_KEY"] = config("DEEPSEEK_API_KEY", cast=str)
|
||||
os.environ["TAVILY_API_KEY"] = config("TAVILY_API_KEY", cast=str)
|
||||
|
||||
# Initialize the DeepSeek chat model
|
||||
llm_chat = ChatDeepSeek(
|
||||
model="deepseek-chat",
|
||||
temperature=0,
|
||||
max_tokens=None,
|
||||
timeout=None,
|
||||
max_retries=2,
|
||||
)
|
||||
|
||||
# Initialize the connection manager
|
||||
manager = ConnectionManager()
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
|
||||
try:
|
||||
data_json = json.loads(data)
|
||||
if isinstance(data_json, list) and len(data_json) > 0 and 'content' in data_json[0]:
|
||||
async for chunk in llm_chat.astream(data_json[0]['content']):
|
||||
await manager.send_personal_message(json.dumps({"type": "message", "payload": chunk.content}), websocket)
|
||||
else:
|
||||
await manager.send_personal_message("Invalid message format", websocket)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await manager.broadcast("Invalid JSON message")
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
await manager.broadcast("Client disconnected")
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
await manager.broadcast("Client disconnected")
|
||||
|
||||
|
||||
14
app/backend/api/ping.py
Normal file
14
app/backend/api/ping.py
Normal file
@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from config import Settings, get_settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/ping")
|
||||
async def pong(settings: Settings = Depends(get_settings)):
|
||||
return {
|
||||
"ping": "pong!",
|
||||
"environment": settings.environment,
|
||||
"testing": settings.testing,
|
||||
}
|
||||
25
app/backend/api/utils.py
Normal file
25
app/backend/api/utils.py
Normal file
@ -0,0 +1,25 @@
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
from fastapi import WebSocket
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||
await websocket.send_text(message)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
json_message = {"type": "message", "payload": message}
|
||||
for connection in self.active_connections:
|
||||
await connection.send_text(json.dumps(json_message))
|
||||
|
||||
17
app/backend/config.py
Normal file
17
app/backend/config.py
Normal file
@ -0,0 +1,17 @@
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
log = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
environment: str = "dev"
|
||||
testing: bool = 0
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> BaseSettings:
|
||||
log.info("Loading config settings from the environment...")
|
||||
return Settings()
|
||||
34
app/backend/main.py
Normal file
34
app/backend/main.py
Normal file
@ -0,0 +1,34 @@
|
||||
import logging
|
||||
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from api import chatbot, ping
|
||||
from config import Settings, get_settings
|
||||
|
||||
log = logging.getLogger("uvicorn")
|
||||
|
||||
origins = ["http://localhost:8004"]
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
application = FastAPI()
|
||||
application.include_router(ping.router, tags=["ping"])
|
||||
application.include_router(
|
||||
chatbot.router, tags=["chatbot"])
|
||||
return application
|
||||
|
||||
|
||||
app = create_application()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# uvicorn.run("main:app", host="0.0.0.0", port=8765, reload=True)
|
||||
23
app/backend/models/adaptive_rag/grading.py
Normal file
23
app/backend/models/adaptive_rag/grading.py
Normal file
@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GradeDocuments(BaseModel):
|
||||
"""Binary score for relevance check on retrieved documents."""
|
||||
|
||||
binary_score: str = Field(
|
||||
description="Documents are relevant to the question, 'yes' or 'no'"
|
||||
)
|
||||
|
||||
class GradeHallucinations(BaseModel):
|
||||
"""Binary score for hallucination present in generation answer."""
|
||||
|
||||
binary_score: str = Field(
|
||||
description="Answer is grounded in the facts, 'yes' or 'no'"
|
||||
)
|
||||
|
||||
class GradeAnswer(BaseModel):
|
||||
"""Binary score to assess answer addresses question."""
|
||||
|
||||
binary_score: str = Field(
|
||||
description="Answer addresses the question, 'yes' or 'no'"
|
||||
)
|
||||
9
app/backend/models/adaptive_rag/query.py
Normal file
9
app/backend/models/adaptive_rag/query.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class QueryRequest(BaseModel):
|
||||
query: str = Field(..., description="The question to ask the model")
|
||||
|
||||
class QueryResponse(BaseModel):
|
||||
response: str = Field(..., description="The model's response")
|
||||
|
||||
12
app/backend/models/adaptive_rag/routing.py
Normal file
12
app/backend/models/adaptive_rag/routing.py
Normal file
@ -0,0 +1,12 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RouteQuery(BaseModel):
|
||||
"""Route a user query to the most relevant datasource."""
|
||||
|
||||
datasource: Literal["vectorstore", "web_search"] = Field(
|
||||
...,
|
||||
description="Given a user question choose to route it to web search or a vectorstore.",
|
||||
)
|
||||
0
app/backend/tests/__init__.py
Normal file
0
app/backend/tests/__init__.py
Normal file
11
app/backend/tests/api/test_chatbot.py
Normal file
11
app/backend/tests/api/test_chatbot.py
Normal file
@ -0,0 +1,11 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
||||
|
||||
from api.chatbot import llm_chat, manager, websocket_endpoint
|
||||
45
app/backend/tests/api/test_utils.py
Normal file
45
app/backend/tests/api/test_utils.py
Normal file
@ -0,0 +1,45 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from fastapi import WebSocket
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
||||
|
||||
from api.utils import ConnectionManager
|
||||
|
||||
|
||||
class TestConnectionManager(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.manager = ConnectionManager()
|
||||
|
||||
async def test_connect(self):
|
||||
mock_websocket = AsyncMock(spec=WebSocket)
|
||||
await self.manager.connect(mock_websocket)
|
||||
self.assertIn(mock_websocket, self.manager.active_connections)
|
||||
mock_websocket.accept.assert_awaited_once()
|
||||
|
||||
async def test_disconnect(self):
|
||||
mock_websocket = MagicMock(spec=WebSocket)
|
||||
self.manager.active_connections.append(mock_websocket)
|
||||
self.manager.disconnect(mock_websocket)
|
||||
self.assertNotIn(mock_websocket, self.manager.active_connections)
|
||||
|
||||
async def test_send_personal_message(self):
|
||||
mock_websocket = AsyncMock(spec=WebSocket)
|
||||
message = "Test message"
|
||||
await self.manager.send_personal_message(message, mock_websocket)
|
||||
mock_websocket.send_text.assert_awaited_once_with(message)
|
||||
|
||||
async def test_broadcast(self):
|
||||
mock_websocket1 = AsyncMock(spec=WebSocket)
|
||||
mock_websocket2 = AsyncMock(spec=WebSocket)
|
||||
self.manager.active_connections = [mock_websocket1, mock_websocket2]
|
||||
message = "Broadcast message"
|
||||
await self.manager.broadcast(message)
|
||||
mock_websocket1.send_text.assert_awaited_once_with('{"type": "message", "payload": "Broadcast message"}')
|
||||
mock_websocket2.send_text.assert_awaited_once_with('{"type": "message", "payload": "Broadcast message"}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
21
app/backend/tests/conftest.py
Normal file
21
app/backend/tests/conftest.py
Normal file
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from config import Settings, get_settings
|
||||
from main import create_application
|
||||
|
||||
|
||||
def get_settings_override():
|
||||
return Settings(testing=1)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def test_app():
|
||||
# set up
|
||||
app = create_application()
|
||||
app.dependency_overrides[get_settings] = get_settings_override
|
||||
with TestClient(app) as test_client:
|
||||
# testing
|
||||
yield test_client
|
||||
|
||||
# tear down
|
||||
4
app/backend/tests/test_ping.py
Normal file
4
app/backend/tests/test_ping.py
Normal file
@ -0,0 +1,4 @@
|
||||
def test_ping(test_app):
|
||||
response = test_app.get("/ping")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"environment": "dev", "ping": "pong!", "testing": True}
|
||||
@ -1,10 +1,38 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
streamlit:
|
||||
build: ./streamlit
|
||||
# streamlit:
|
||||
# build: ./streamlit
|
||||
# platform: linux/amd64
|
||||
# ports:
|
||||
# - "8501:8501"
|
||||
# volumes:
|
||||
# - ./llmops/src/rag_cot_evaluation/chroma_db:/app/llmops/src/rag_cot_evaluation/chroma_db
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile.prod
|
||||
container_name: backend
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "8501:8501"
|
||||
# command: pipenv run uvicorn main:app --reload --workers 1 --host 0.0.0.0 --port 8765
|
||||
volumes:
|
||||
- ./llmops/src/rag_cot_evaluation/chroma_db:/app/llmops/src/rag_cot_evaluation/chroma_db
|
||||
- ./backend:/usr/src/app
|
||||
ports:
|
||||
- "8000:8765"
|
||||
environment:
|
||||
- ENVIRONMENT=dev
|
||||
- TESTING=0
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.local
|
||||
container_name: frontend
|
||||
volumes:
|
||||
- ./frontend:/usr/src/app
|
||||
- /usr/src/app/node_modules
|
||||
ports:
|
||||
- "3000:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
environment:
|
||||
LOG_LEVEL: "DEBUG"
|
||||
|
||||
1
app/frontend/.dockerignore
Normal file
1
app/frontend/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
1
app/frontend/.env.production
Normal file
1
app/frontend/.env.production
Normal file
@ -0,0 +1 @@
|
||||
REACT_APP_BASE_URL=https://backend.aimingmed.com/
|
||||
24
app/frontend/.gitignore
vendored
Normal file
24
app/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
54
app/frontend/README.md
Normal file
54
app/frontend/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
28
app/frontend/eslint.config.js
Normal file
28
app/frontend/eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
app/frontend/index.html
Normal file
13
app/frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5874
app/frontend/package-lock.json
generated
Normal file
5874
app/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
app/frontend/package.json
Normal file
40
app/frontend/package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"daisyui": "^5.0.17",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.2.0",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
6
app/frontend/postcss.config.js
Normal file
6
app/frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
22
app/frontend/src/App.test.tsx
Normal file
22
app/frontend/src/App.test.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import App from './App';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
it('renders initial state', () => {
|
||||
render(<App />);
|
||||
expect(screen.getByText('Simple Chatbot')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /send/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sends a message', () => {
|
||||
const mockSend = vi.fn();
|
||||
vi.spyOn(WebSocket.prototype, 'send').mockImplementation(mockSend);
|
||||
render(<App />);
|
||||
const inputElement = screen.getByRole('textbox');
|
||||
fireEvent.change(inputElement, { target: { value: 'Hello' } });
|
||||
const buttonElement = screen.getByRole('button', { name: /send/i });
|
||||
fireEvent.click(buttonElement);
|
||||
expect(mockSend).toHaveBeenCalledWith(JSON.stringify([{ role: 'user', content: 'Hello' }]));
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
});
|
||||
93
app/frontend/src/App.tsx
Normal file
93
app/frontend/src/App.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const BASE_DOMAIN_NAME = import.meta.env.REACT_APP_DOMAIN_NAME || 'localhost';
|
||||
|
||||
|
||||
interface Message {
|
||||
sender: 'user' | 'bot';
|
||||
text: string;
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [socket, setSocket] = useState<WebSocket | null>(null);
|
||||
const mounted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
const ws = new WebSocket(`ws://${BASE_DOMAIN_NAME}:8000/ws`);
|
||||
setSocket(ws);
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connection opened');
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'message' && data.payload && mounted.current) {
|
||||
setMessages((prevMessages) => {
|
||||
const lastMessage = prevMessages[prevMessages.length - 1];
|
||||
if (lastMessage && lastMessage.sender === 'bot') {
|
||||
return [...prevMessages.slice(0, -1), { ...lastMessage, text: lastMessage.text + data.payload }];
|
||||
} else {
|
||||
return [...prevMessages, { sender: 'bot', text: data.payload }];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Unexpected message format:', data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
ws.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sendMessage = () => {
|
||||
if (newMessage.trim() !== '') {
|
||||
const message = [{ role: 'user', content: newMessage }];
|
||||
setMessages((prevMessages) => [...prevMessages, { sender: 'user', text: newMessage }]);
|
||||
socket?.send(JSON.stringify(message));
|
||||
setNewMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-gray-100">
|
||||
<div className="p-4">
|
||||
<h1 className="text-3xl font-bold text-center text-gray-800">Simple Chatbot</h1>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto p-4">
|
||||
{messages.map((msg, index) => (
|
||||
<div key={index} className={`p-4 rounded-lg mb-2 ${msg.sender === 'user' ? 'bg-blue-100 text-blue-800' : 'bg-gray-200 text-gray-800'}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-300">
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
className="flex-grow p-2 border border-gray-300 rounded-lg mr-2"
|
||||
/>
|
||||
<button onClick={sendMessage} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
1
app/frontend/src/assets/react.svg
Normal file
1
app/frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
app/frontend/src/index.css
Normal file
3
app/frontend/src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
10
app/frontend/src/main.tsx
Normal file
10
app/frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
9
app/frontend/src/vite-env.d.ts
vendored
Normal file
9
app/frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
||||
|
||||
declare global {
|
||||
namespace jest {
|
||||
interface Matchers<R = void>
|
||||
extends TestingLibraryMatchers<typeof expect.stringContaining, R> {}
|
||||
}
|
||||
}
|
||||
11
app/frontend/tailwind.config.js
Normal file
11
app/frontend/tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
}
|
||||
|
||||
9
app/frontend/tests/setup.ts
Normal file
9
app/frontend/tests/setup.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { expect, afterEach } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import * as matchers from "@testing-library/jest-dom/matchers";
|
||||
|
||||
expect.extend(matchers);
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
26
app/frontend/tsconfig.app.json
Normal file
26
app/frontend/tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
app/frontend/tsconfig.json
Normal file
7
app/frontend/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
app/frontend/tsconfig.node.json
Normal file
24
app/frontend/tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
17
app/frontend/vite.config.ts
Normal file
17
app/frontend/vite.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: true,
|
||||
strictPort: true,
|
||||
port: 5173
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./tests/setup.ts",
|
||||
},
|
||||
});
|
||||
@ -15,7 +15,7 @@ rag:
|
||||
testing:
|
||||
query: "如何治疗乳腺癌?"
|
||||
evaluation:
|
||||
evaluation_dataset_csv_path: "../../../../data/qa_dataset_20250401b.csv"
|
||||
evaluation_dataset_csv_path: "../../../../data/qa_dataset_20250409_onlyBreast.csv"
|
||||
evaluation_dataset_column_question: question
|
||||
evaluation_dataset_column_answer: answer
|
||||
ls_chat_model_provider:
|
||||
|
||||
@ -6,8 +6,9 @@ Use the vectorstore for questions on these topics. Otherwise, use web-search.
|
||||
|
||||
system_retriever_grader = """You are a grader assessing relevance of a retrieved document to a user question. \n
|
||||
If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n
|
||||
It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \n
|
||||
Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question."""
|
||||
You must make sure to read carefully that the document contains a sentence or chunk of sentences that is exactly related but not closely related to the question subject (e.g. must be the exact disease or subject in question). \n
|
||||
The goal is to filter out erroneous retrievals. \n
|
||||
Must return a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question."""
|
||||
|
||||
system_hallucination_grader = """You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \n
|
||||
Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts."""
|
||||
@ -18,6 +19,25 @@ system_answer_grader = """You are a grader assessing whether an answer addresses
|
||||
system_question_rewriter = """You a question re-writer that converts an input question to a better version that is optimized \n
|
||||
for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning."""
|
||||
|
||||
# prompt for question answering based on retrieved documents
|
||||
qa_prompt_template = """You are an expert at answering questions based on the following retrieved context.\n
|
||||
Before answering the question, you must have your own thought process what are the general scopes to cover when answering this question, step-by-step. Do not include this thought process in the answer.\n
|
||||
Then, given your thought process, you must read the provided context carefully and extract the relevant information.\n
|
||||
|
||||
If the question is about medical question, you must answer the question in a medical way and assume that the audience is a junior doctor or a medical student: \n
|
||||
1. For cancer diseases, you must include comprehensive treatment advices that encompasses multidisciplinary treatment options that included but not limited to surgery, chemotherapy, radiology, internal medicine (drugs), nutritional ratio (protein), etc. You must layout out the treatment options like what are the first-line, second-line treatment etc.\n
|
||||
2. For cancer diseases, don't consider context that is not primary tumor/cancer related, unless the question specifically mention it is secondary tumor/cancer related.\n
|
||||
3. If the question didn't state the stage of the cancer disease, you must reply with treatment options for each stage of the cancer disease, if they are availalbe in the provided context. If they are not available in the provided context, give a general one.\n
|
||||
|
||||
You must not use any information that is not present in the provided context to answer the question. Make sure to remove those information not present in the provided context.\n
|
||||
If you don't know the answer, just say that you don't know.\n
|
||||
Provide the answer in a concise and organized manner. \n
|
||||
|
||||
Question: {question} \n
|
||||
Context: {context} \n
|
||||
Answer:
|
||||
"""
|
||||
|
||||
|
||||
# Evaluation
|
||||
CORRECTNESS_PROMPT = """You are an impartial judge. Evaluate Student Answer against Ground Truth for conceptual similarity and correctness.
|
||||
|
||||
@ -18,11 +18,14 @@ from typing_extensions import TypedDict
|
||||
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_community.tools.tavily_search import TavilySearchResults
|
||||
from langchain.prompts import PromptTemplate, HumanMessagePromptTemplate
|
||||
|
||||
from langchain.schema import Document
|
||||
from pprint import pprint
|
||||
from langgraph.graph import END, StateGraph, START
|
||||
from langsmith import Client
|
||||
|
||||
|
||||
from data_models import (
|
||||
RouteQuery,
|
||||
GradeDocuments,
|
||||
@ -34,7 +37,8 @@ from prompts_library import (
|
||||
system_retriever_grader,
|
||||
system_hallucination_grader,
|
||||
system_answer_grader,
|
||||
system_question_rewriter
|
||||
system_question_rewriter,
|
||||
qa_prompt_template
|
||||
)
|
||||
|
||||
from evaluators import (
|
||||
@ -141,18 +145,22 @@ def go(args):
|
||||
|
||||
##########################################
|
||||
### Generate
|
||||
from langchain import hub
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
|
||||
# Prompt
|
||||
prompt = hub.pull("rlm/rag-prompt")
|
||||
# Create a PromptTemplate with the given prompt
|
||||
new_prompt_template = PromptTemplate(
|
||||
input_variables=["context", "question"],
|
||||
template=qa_prompt_template,
|
||||
)
|
||||
|
||||
# Post-processing
|
||||
def format_docs(docs):
|
||||
return "\n\n".join(doc.page_content for doc in docs)
|
||||
# Create a new HumanMessagePromptTemplate with the new PromptTemplate
|
||||
new_human_message_prompt_template = HumanMessagePromptTemplate(
|
||||
prompt=new_prompt_template
|
||||
)
|
||||
prompt_qa = ChatPromptTemplate.from_messages([new_human_message_prompt_template])
|
||||
|
||||
# Chain
|
||||
rag_chain = prompt | llm | StrOutputParser()
|
||||
rag_chain = prompt_qa | llm | StrOutputParser()
|
||||
|
||||
|
||||
##########################################
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user