Skip to content

Commit

Permalink
add V3(official) ChatGPT API. Use unfied Chatbot gRPC api
Browse files Browse the repository at this point in the history
proto changed to:

cdfmlr/muvtuber-proto@183c5dd

solve #1
  • Loading branch information
cdfmlr committed Mar 9, 2023
1 parent 0750caa commit b3bc527
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 118 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
.idea

### https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon
Icon


# Thumbnails
._*
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,28 @@ options:
### 请求
```sh
$ grpcurl -d '{"access_token": "eyJ***99A", "initial_prompt": "..."}' -plaintext localhost:50052 muvtuber.chatbot.chatgpt_chatbot.v1.ChatGPTService.NewSession

$ grpcurl -d '{"config": "{\"version\": 3, \"api_key\": \"sk-xxxx\"}", "initial_prompt": "你好"}' -plaintext localhost:50052 muvtuber.chatbot.v2.ChatbotService.NewSession
{
"sessionId": "b7268187-ab7a-4e2d-9d4a-0161975369bd"
"sessionId": "2617613c-9f20-4d6c-b47e-1622392a134e",
"initialResponse": "你好!我很高兴能和你交流。有什么我可以帮助你的吗?"
}

$ grpcurl -d '{"session_id": "b7268187-ab7a-4e2d-9d4a-0161975369bd", "prompt": "hello!!"}' -plaintext localhost:50052 muvtuber.chatbot.chatgpt_chatbot.v1.ChatGPTService.Chat
$ grpcurl -d '{"session_id": "2617613c-9f20-4d6c-b47e-1622392a134e", "prompt": "hello!!"}' -plaintext localhost:50052 muvtuber.chatbot.v2.ChatbotService.Chat
{
"response": "Hello! How can I assist you today?"
}

$ grpcurl -d '{"session_id": "b7268187-ab7a-4e2d-9d4a-0161975369bd"}' -plaintext localhost:50052 muvtuber.chatbot.chatgpt_chatbot.v1.ChatGPTService.DeleteSession
$ grpcurl -d '{"session_id": "2617613c-9f20-4d6c-b47e-1622392a134e"}' -plaintext localhost:50052 muvtuber.chatbot.v2.ChatbotService.DeleteSession
{
"sessionId": "b7268187-ab7a-4e2d-9d4a-0161975369bd"
"sessionId": "2617613c-9f20-4d6c-b47e-1622392a134e"
}
```
### errors
```md
- NewSession
- INVALID_ARGUMENT: access_token is required
- INVALID_ARGUMENT: version & access_token|api_key is required
- RESOURCE_EXHAUSTED: TooManySessions (该系统内 MultiChatGPT 的最大会话数限制)
- UNAVAILABLE: ChatGPTError (向 ChatGPT 请求 initial_prompt 时出错)
- Chat
Expand Down
113 changes: 97 additions & 16 deletions chatgpt/chatbot.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from dataclasses import dataclass
import logging
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
import os
from enum import Enum
from typing import Dict
import uuid
from warnings import warn
from revChatGPT.V1 import Chatbot
from revChatGPT.V1 import Chatbot as ChatbotV1
from revChatGPT.V3 import Chatbot as ChatbotV3
import threading
from datetime import datetime

Expand All @@ -19,24 +22,39 @@
# Cooldown: a decorator to limit the frequency of function calls


class ChatGPT:
class ChatGPT(metaclass=ABCMeta):
@abstractmethod
def ask(self, session_id, prompt):
"""Ask ChatGPT with prompt, return response text
Raises:
ChatGPTError: ChatGPT error
"""
pass


# V1 Standard ChatGPT
# Update 2023/03/09 9:50AM - No longer functional
class ChatGPTv1(ChatGPT):
def __init__(self, config={'access_token': 'your access token', 'initial_prompt': 'your initial prompt'}):
"""ChatGPT with config: {access_token, initial_prompt}"""
self.chatbot = Chatbot(config=config)
self.chatbot = ChatbotV1(config=config)
self.lock = threading.Lock() # for self.chatbot

q = config.get('initial_prompt', None)
if q:
a = self.ask(q)
a = self.ask('', q)
print(f'{datetime.now()} ChatGPT initial ask: {q} -> {a}')
self.initial_response = a
else:
self.initial_response = None

@cooldown(os.getenv("CHATGPT_COOLDOWN", 75))
def ask(self, prompt) -> str: # raises Exception
def ask(self, session_id, prompt) -> str: # raises Exception
"""Ask ChatGPT with prompt, return response text
- session_id: unused
Raises:
ChatGPTError: ChatGPT error
"""
Expand All @@ -63,25 +81,80 @@ def renew(self, access_token: str):
warn("ChatGPT.renew is deprecated", DeprecationWarning)

with self.lock:
self.chatbot = Chatbot(config={"access_token": access_token})
self.chatbot = ChatbotV1(config={"access_token": access_token})


# V3 Official Chat API
# Paid
class ChatGPTv3(ChatGPT):
def __init__(self, config={'api_key': 'your api key', 'initial_prompt': 'your initial prompt'}):
self.chatbot = ChatbotV3(api_key=config.get('api_key', ''))
self.lock = threading.Lock() # for self.chatbot

q = config.get('initial_prompt', None)
if q:
a = self.ask('', q)
logging.info(f'ChatGPT initial ask: {q} -> {a}')
self.initial_response = a
else:
self.initial_response = None


# Rate Limits: 20 RPM / 40,000 TPM
# https://platform.openai.com/docs/guides/rate-limits/overview
# 主要是价格w
# gpt-3.5-turbo: $0.002 / 1K tokens
@cooldown(os.getenv("CHATGPT_V3_COOLDOWN", 30))
def ask(self, session_id, prompt) -> str: # raises Exception
"""Ask ChatGPT with prompt, return response text
- session_id: unused
Raises:
ChatGPTError: ChatGPT error
"""
response: str | None = None

try:
with self.lock:
response = self.chatbot.ask(prompt)
except Exception as e:
logging.warning(f"ChatGPT ask error: {e}")
raise ChatGPTError(str(e))

if not response:
raise ChatGPTError("ChatGPT response is None")

return response


class APIVersion(Enum):
V1 = 1
V3 = 3

def get_ChatGPT_class(self):
if self == self.V1:
return ChatGPTv1
elif self == self.V3:
return ChatGPTv3


# ChatGPTConfig: {access_token, initial_prompt}
@dataclass
class ChatGPTConfig:
version: APIVersion
access_token: str
initial_prompt: str


MAX_SESSIONS = 10


# MultiChatGPT: {session_id: ChatGPT}:
# - new(config) -> session_id
# - ask(session_id, prompt) -> response
# - delete(session_id)


class MultiChatGPT:
class MultiChatGPT(ChatGPT):
"""MultiChatGPT: {session_id: ChatGPT}"""

def __init__(self):
Expand All @@ -90,7 +163,7 @@ def __init__(self):
def new(self, config: ChatGPTConfig) -> str: # raises TooManySessions, ChatGPTError
"""Create new ChatGPT session, return session_id
session_id is a uuid4 string
session_id is an uuid4 string
Raises:
TooManySessions: Too many sessions
Expand All @@ -100,10 +173,18 @@ def new(self, config: ChatGPTConfig) -> str: # raises TooManySessions, ChatGPTE
raise TooManySessions(MAX_SESSIONS)

session_id = str(uuid.uuid4())
self.chatgpt[session_id] = ChatGPT(config={
"access_token": config.access_token,
"initial_prompt": config.initial_prompt
})

if config.version == APIVersion.V3:
self.chatgpt[session_id] = ChatGPTv3(config={
"api_key": config.access_token,
"initial_prompt": config.initial_prompt
})
else:
self.chatgpt[session_id] = ChatGPTv1(config={
"access_token": config.access_token,
"initial_prompt": config.initial_prompt
})

return session_id

def ask(self, session_id: str, prompt: str) -> str: # raises ChatGPTError
Expand All @@ -116,7 +197,7 @@ def ask(self, session_id: str, prompt: str) -> str: # raises ChatGPTError
if session_id not in self.chatgpt:
raise SessionNotFound(session_id)

resp = self.chatgpt[session_id].ask(prompt)
resp = self.chatgpt[session_id].ask(session_id, prompt)

return resp

Expand Down
40 changes: 21 additions & 19 deletions chatgpt/grpcapi.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from datetime import datetime
import json
import logging
import os
from chatbot import MultiChatGPT, ChatGPTConfig, ChatGPTError, TooManySessions, SessionNotFound
from chatbot import MultiChatGPT, ChatGPTConfig, ChatGPTError, TooManySessions, SessionNotFound, APIVersion
from cooldown import CooldownException
from protos import chatgpt_chatbot_pb2, chatgpt_chatbot_pb2_grpc
from protos import chatbot_pb2, chatbot_pb2_grpc

import grpc
from concurrent import futures


class ChatGPTgRPCServer(chatgpt_chatbot_pb2_grpc.ChatGPTServiceServicer):
class ChatGPTgRPCServer(chatbot_pb2_grpc.ChatbotServiceServicer):
def __init__(self):
self.multiChatGPT = MultiChatGPT()

Expand All @@ -18,16 +18,18 @@ def NewSession(self, request, context):
Input: access_token (string) and initial_prompt (string).
Output: session_id (string).
"""
if not request.access_token:
# raise ValueError('access_token is required')
try:
c = request.config
c = json.loads(c)
except Exception as e:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details('access_token is required')
logging.warn(
'ChatGPTgRPCServer.NewSession: access_token is required')
return chatgpt_chatbot_pb2.NewSessionResponse()
context.set_details(str(e))
logging.warning('ChatGPTgRPCServer.NewSession: bad config')
return chatbot_pb2.NewSessionResponse()

config = ChatGPTConfig(
access_token=request.access_token,
version=APIVersion(c.get('version', None)),
access_token=c.get('access_token', False) or c.get('api_key', False) or '',
initial_prompt=request.initial_prompt)

session_id = None
Expand All @@ -48,7 +50,7 @@ def NewSession(self, request, context):
f'ChatGPTgRPCServer.NewSession: (OK) session_id={session_id}')

# TODO: 这个 initial_response 太恶心了,还是逐层传比较好吧
return chatgpt_chatbot_pb2.NewSessionResponse(session_id=session_id, initial_response=self.multiChatGPT.chatgpt[session_id].initial_response)
return chatbot_pb2.NewSessionResponse(session_id=session_id, initial_response=self.multiChatGPT.chatgpt[session_id].initial_response)

def Chat(self, request, context):
"""Chat sends a prompt to ChatGPT and receives a response.
Expand All @@ -60,13 +62,13 @@ def Chat(self, request, context):
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details('session_id is required')
logging.warn('ChatGPTgRPCServer.Chat: session_id is required')
return chatgpt_chatbot_pb2.ChatResponse()
return chatbot_pb2.ChatResponse()
if not request.prompt:
# raise ValueError('prompt is required')
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details('prompt is required')
logging.warn('ChatGPTgRPCServer.Chat: prompt is required')
return chatgpt_chatbot_pb2.ChatResponse()
return chatbot_pb2.ChatResponse()

response = None
try:
Expand All @@ -89,7 +91,7 @@ def Chat(self, request, context):
logging.info(
f'ChatGPTgRPCServer.Chat: (OK) {response}')

return chatgpt_chatbot_pb2.ChatResponse(response=response)
return chatbot_pb2.ChatResponse(response=response)

def DeleteSession(self, request, context):
"""DeleteSession deletes a session with ChatGPT.
Expand All @@ -102,7 +104,7 @@ def DeleteSession(self, request, context):
context.set_details('session_id is required')
logging.warn(
'ChatGPTgRPCServer.DeleteSession: session_id is required')
return chatgpt_chatbot_pb2.DeleteSessionResponse()
return chatbot_pb2.DeleteSessionResponse()

try:
self.multiChatGPT.delete(request.session_id)
Expand All @@ -117,17 +119,17 @@ def DeleteSession(self, request, context):
logging.info(
f'ChatGPTgRPCServer.DeleteSession: (OK) {request.session_id}')

return chatgpt_chatbot_pb2.DeleteSessionResponse(session_id=request.session_id)
return chatbot_pb2.DeleteSessionResponse(session_id=request.session_id)


def serveGRPC(address: str = 'localhost:50052'):
"""Starts a gRPC server at the specified address 'host:port'."""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
chatgpt_chatbot_pb2_grpc.add_ChatGPTServiceServicer_to_server(
chatbot_pb2_grpc.add_ChatbotServiceServicer_to_server(
ChatGPTgRPCServer(), server)

SERVICE_NAMES = [
chatgpt_chatbot_pb2.DESCRIPTOR.services_by_name['ChatGPTService'].full_name]
chatbot_pb2.DESCRIPTOR.services_by_name['ChatbotService'].full_name]

# the reflection service
if os.getenv('GRPC_REFLECTION', False):
Expand Down
6 changes: 3 additions & 3 deletions chatgpt/httpapi.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from datetime import datetime
from chatbot import ChatGPT
from chatbot import ChatGPTv1
import aiohttp
from aiohttp.web import Request, Response


class ChatGPTHTTPServer:
def __init__(self, chatgpt: ChatGPT, host: str = "localhost", port: int = 9006):
def __init__(self, chatgpt: ChatGPTv1, host: str = "localhost", port: int = 9006):
self.chatgpt = chatgpt
self.host = host
self.port = port
Expand Down Expand Up @@ -68,7 +68,7 @@ def serveHTTP(address: str = "localhost:9006"):
except:
raise ValueError("address should be in format of host:port")

chatgpt = ChatGPT()
chatgpt = ChatGPTv1()
server = ChatGPTHTTPServer(chatgpt, host, port)
server.run()

Expand Down
Loading

0 comments on commit b3bc527

Please sign in to comment.