Skip to content

Commit

Permalink
first init
Browse files Browse the repository at this point in the history
  • Loading branch information
jamiesun committed Nov 15, 2023
0 parents commit 1167331
Show file tree
Hide file tree
Showing 17 changed files with 980 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .editotconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
root = true

# all files
[*.go]
indent_style = tab
indent_size = 4
insert_final_newline = true

[*.py]
indent_style = space
indent_size = 4

[Makefile]
indent_style = tab

[*.js]
charset = utf-8
indent_style = space
indent_size = 4

[*.json]
indent_style = space
indent_size = 2
45 changes: 45 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Build and Publish

on:
# run it on push to the default repository branch
push:
branches: [main]
# run it during pull request
pull_request:

jobs:
# define job to build and publish docker image
build-and-push-docker-image:
name: Build Docker image and push to repositories
# run only when code is compiling and tests are passing
runs-on: ubuntu-latest

# steps to perform in job
steps:
- name: Checkout code
uses: actions/checkout@v3

# setup Docker buld action
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2

- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build image and push to Docker Hub and GitHub Container Registry
uses: docker/build-push-action@v2
with:
# 指向带有 Dockerfile 的源代码所在位置的相对路径
context: ./
# Note: tags has to be all lower-case
tags: |
talkincode/gptservice:latest-amd64
# build on feature branches, push only on main branch
push: ${{ github.ref == 'refs/heads/main' }}

- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.idea
__pycache__
.vscode
/release/
release
Dockerfile.local
__debug_bin
.DS_Store
build
/rundata/
.env
/venv/
/playground/chroma_db/
/playground/local_qdrant/
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 使用官方的 Python 基础镜像
FROM python:3.12.0-alpine3.18

# 设置工作目录
WORKDIR /app

# 将项目文件复制到工作目录
COPY apps /app/libs
COPY ./main.py /app/main.py
COPY ./webapi.py /app/webapi.py
COPY ./requirements.txt /app/requirements.txt

# 安装项目依赖
RUN pip install --no-cache-dir -r requirements.txt

# 暴露端口
EXPOSE 8000

# 设置启动命令
CMD ["uvicorn","main:app"]
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fastpub:
docker buildx build --build-arg GoArch="amd64" --platform=linux/amd64 -t \
talkincode/gptservice:latest .
docker push talkincode/gptservice:latest

arm64:
docker buildx build --build-arg GoArch="arm64" --platform=linux/arm64 -t \
talkincode/gptservice:latest-arm64 .
docker push talkincode/gptservice:latest-arm64


.PHONY: clean build
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# GPTService

## Introduction of GPTService

GPTService is an advanced API project designed for custom GPT models. It follows the OpenAPI specification and provides efficient and flexible vector knowledge base retrieval and vector database management capabilities.

## Features

- Vector Knowledge Base Retrieval: Efficiently retrieve knowledge related to custom GPT models.
- Vector Database Management: Flexible database management tool for users to manage and update data.
- OpenAPI Compliance: Ensures compatibility with existing systems and tools.
- Easy Integration: Suitable for a wide range of programming environments and frameworks.

3 changes: 3 additions & 0 deletions apps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@


__all__ = ['apps', "common", "qdrant_index"]
125 changes: 125 additions & 0 deletions apps/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import logging
import os
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import APIKeyHeader
from pydantic import BaseModel, Field
from starlette import status
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response
from starlette.types import ASGIApp

from .common import num_tokens_from_string
from .qdrant_index import qdrant
from typing import Any

log = logging.getLogger(__name__)


class LimitUploadSize(BaseHTTPMiddleware):
"""限制上传文件大小"""

def __init__(self, app: ASGIApp, max_upload_size: int) -> None:
super().__init__(app)
self.max_upload_size = max_upload_size

async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
if request.method == 'POST':
if 'content-length' not in request.headers:
return Response(status_code=status.HTTP_411_LENGTH_REQUIRED)
content_length = int(request.headers['content-length'])
if content_length > self.max_upload_size:
return Response(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE)
return await call_next(request)


app = FastAPI(
title="GPTService API",
description="gptservice api",
version="1.0.0",
)
app.add_middleware(LimitUploadSize, max_upload_size=1024 * 1024 * 10)


class TokenData(BaseModel):
api_key: str


class IndexItem(BaseModel):
collection: str = Field("default", title="索引名称", description="索引名称")
text: str = Field(title="文本内容", description="要向量化存储的文本内容")
type: str = Field("text", title="文本类型", description="文本类型, text: 文本, webbase: 网页, webpdf: 网页PDF")
url: str = Field("", title="网页地址", description="网页地址, 当type为webbase或webpdf时, 此项必填")
separator: str = Field("\n\n", title="分隔符", description="分隔符, 处理文本时如何切分")
chunk_size: int = Field(2000, title="切分大小", description="切分大小, 处理文本时每段的大小")
chunk_overlap: int = Field(0, title="切分重叠", description="切分重叠, 处理文本时每段的重叠大小")


class IndexSearchItem(BaseModel):
collection: str = Field("default", title="索引名称", description="知识库索引存储名称")
query: str = Field(title="查询内容", description="查询文本内容")


class TokenItem(BaseModel):
text: str = Field(title="文本内容", description="要向量化存储的文本内容")
encoding: str = Field("cl100k_base", title="编码", description="编码, 默认为cl100k_base")


class RestResult(BaseModel):
code: int = Field(0, title="返回码", description="返回码, 0为成功, 其他为失败")
msg: str = Field("ok", title="返回消息", description="返回消息, 成功为ok, 失败为具体错误信息")
result: dict = Field({}, title="返回数据(可选)", description="返回数据(可选), 文本内容或是结构化数据")


API_KEY = os.environ.get("API_KEY")
api_key_header = APIKeyHeader(name="Authorization", auto_error=False)


def verify_api_key(api_key: str = Depends(api_key_header)):
"""验证API Key"""
if api_key is None or api_key == "" or len(api_key) < 8 or api_key[7:] != API_KEY:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key")
return TokenData(api_key=api_key)


@app.get("/")
async def root():
return "ok"


@app.post("/token/stat")
async def token_stat(item: TokenItem, td: TokenData = Depends(verify_api_key)):
"""统计文本的token数量"""
try:
return dict(code=0, msg="ok", data=dict(
encoding=item.encoding,
length=num_tokens_from_string(item.text, item.encoding)
))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@app.post("/knowledge/create")
async def create_index(item: IndexItem, td: TokenData = Depends(verify_api_key)):
"""创建知识库内容索引"""
try:
if item.type == "text":
await qdrant.index_text(item.collection, item.text, item.chunk_size, item.chunk_overlap)
elif item.type == "webbase":
await qdrant.index_text_from_url(item.collection, item.url, item.chunk_size, item.chunk_overlap)
elif item.type == "webpdf":
await qdrant.index_pdf_from_path(item.collection, item.url, item.chunk_size, item.chunk_overlap)
except Exception as e:
log.error(f"create_index error: {e}")
raise HTTPException(status_code=500, detail=str(e))
return RestResult(code=0, msg="success")


@app.post("/knowledge/search", summary="搜索知识库", description="搜索知识库, 获取相关内容")
async def search_index(item: IndexSearchItem, td: TokenData = Depends(verify_api_key)):
"""搜索知识库,返回相关内容"""
try:
result = await qdrant.search(item.collection, item.query)
return RestResult(code=0, msg="ok", result=dict(data=result))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
13 changes: 13 additions & 0 deletions apps/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import tiktoken


def num_tokens_from_string(string: str, encoding_name: str) -> int:
"""Returns the number of tokens in a text string."""
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
return num_tokens


def document_spliter_len(string: str) -> int:
encoding = tiktoken.get_encoding("cl100k_base")
return len(encoding.encode(string))
81 changes: 81 additions & 0 deletions apps/qdrant_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import os

import tiktoken
from langchain.document_loaders import WebBaseLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Qdrant
from langchain.document_loaders import PyMuPDFLoader
from .common import document_spliter_len
from qdrant_client import qdrant_client


class QdrantIndex(object):

def __init__(self):
self.qdrant_url = os.environ.get("QDRANT_URL")
self.qdrant_grpc = os.environ.get("QDRANT_GRPC") in ["1", "true", "True", "TRUE"]

async def search(self, collection, text, topk=3):
client = qdrant_client.QdrantClient(
url=self.qdrant_url, prefer_grpc=self.qdrant_grpc
)
embeddings = OpenAIEmbeddings()
q = Qdrant(
client=client, collection_name=collection,
embeddings=embeddings,
)
result = await q.asimilarity_search_with_score(text, k=topk)
data = []
if result:
for doc, score in result:
data.append(dict(content=doc.page_content, metadata=doc.metadata, score=score))
return data

async def index_text_from_url(self, collection, url, chunk_size=100, chunk_overlap=0):
loader = WebBaseLoader(url)
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=document_spliter_len)
docs = text_splitter.split_documents(documents)
embeddings = OpenAIEmbeddings()
await Qdrant.afrom_documents(
docs, embeddings,
url=self.qdrant_url,
prefer_grpc=self.qdrant_grpc,
collection_name=collection,
)

async def index_pdf_from_path(self, collection, pdffile, chunk_size=1000, chunk_overlap=0):
loader = PyMuPDFLoader(pdffile)
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=document_spliter_len)
docs = text_splitter.split_documents(documents)
embeddings = OpenAIEmbeddings()
await Qdrant.afrom_documents(
docs, embeddings,
url=self.qdrant_url,
prefer_grpc=self.qdrant_grpc,
collection_name=collection,
)

async def index_text(self, collection, text, chunk_size=1000, chunk_overlap=0):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=document_spliter_len
)
docs = text_splitter.create_documents([text])
embeddings = OpenAIEmbeddings()
await Qdrant.afrom_documents(
docs, embeddings,
url=self.qdrant_url,
prefer_grpc=self.qdrant_grpc,
collection_name=collection,
)


qdrant = QdrantIndex()
31 changes: 31 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: "3"
services:
gptservice:
container_name: "gptservice"
image: talkincode/gptservice:latest
logging:
driver: "json-file"
options:
max-size: "50m"
environment:
- API_KEY=${API_KEY}
- OPENAI_API_TYPE=${OPENAI_API_TYPE}
- AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION}
- AZURE_OPENAI_API_BASE=${AZURE_OPENAI_API_BASE}
- AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- QDRANT_URL=${QDRANT_URL}
- DATA_DIR=/data
volumes:
- gptservice-volume:/data
ports:
- "8888:8700"
command: ["uvicorn", "--host","0.0.0.0","main:apps"]
networks:
gptservice_network:

networks:
gptservice_network:

volumes:
gptservice-volume:
Loading

0 comments on commit 1167331

Please sign in to comment.