Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

512 lines
15KB

  1. """
  2. A simple wrapper for the official ChatGPT API
  3. """
  4. import argparse
  5. import json
  6. import os
  7. import sys
  8. from datetime import date
  9. import openai
  10. import tiktoken
  11. from bot.bot import Bot
  12. from config import conf
  13. ENGINE = os.environ.get("GPT_ENGINE") or "text-chat-davinci-002-20221122"
  14. ENCODER = tiktoken.get_encoding("gpt2")
  15. def get_max_tokens(prompt: str) -> int:
  16. """
  17. Get the max tokens for a prompt
  18. """
  19. return 4000 - len(ENCODER.encode(prompt))
  20. # ['text-chat-davinci-002-20221122']
  21. class Chatbot:
  22. """
  23. Official ChatGPT API
  24. """
  25. def __init__(self, api_key: str, buffer: int = None) -> None:
  26. """
  27. Initialize Chatbot with API key (from https://platform.openai.com/account/api-keys)
  28. """
  29. openai.api_key = api_key or os.environ.get("OPENAI_API_KEY")
  30. self.conversations = Conversation()
  31. self.prompt = Prompt(buffer=buffer)
  32. def _get_completion(
  33. self,
  34. prompt: str,
  35. temperature: float = 0.5,
  36. stream: bool = False,
  37. ):
  38. """
  39. Get the completion function
  40. """
  41. return openai.Completion.create(
  42. engine=ENGINE,
  43. prompt=prompt,
  44. temperature=temperature,
  45. max_tokens=get_max_tokens(prompt),
  46. stop=["\n\n\n"],
  47. stream=stream,
  48. )
  49. def _process_completion(
  50. self,
  51. user_request: str,
  52. completion: dict,
  53. conversation_id: str = None,
  54. user: str = "User",
  55. ) -> dict:
  56. if completion.get("choices") is None:
  57. raise Exception("ChatGPT API returned no choices")
  58. if len(completion["choices"]) == 0:
  59. raise Exception("ChatGPT API returned no choices")
  60. if completion["choices"][0].get("text") is None:
  61. raise Exception("ChatGPT API returned no text")
  62. completion["choices"][0]["text"] = completion["choices"][0]["text"].rstrip(
  63. "<|im_end|>",
  64. )
  65. # Add to chat history
  66. self.prompt.add_to_history(
  67. user_request,
  68. completion["choices"][0]["text"],
  69. user=user,
  70. )
  71. if conversation_id is not None:
  72. self.save_conversation(conversation_id)
  73. return completion
  74. def _process_completion_stream(
  75. self,
  76. user_request: str,
  77. completion: dict,
  78. conversation_id: str = None,
  79. user: str = "User",
  80. ) -> str:
  81. full_response = ""
  82. for response in completion:
  83. if response.get("choices") is None:
  84. raise Exception("ChatGPT API returned no choices")
  85. if len(response["choices"]) == 0:
  86. raise Exception("ChatGPT API returned no choices")
  87. if response["choices"][0].get("finish_details") is not None:
  88. break
  89. if response["choices"][0].get("text") is None:
  90. raise Exception("ChatGPT API returned no text")
  91. if response["choices"][0]["text"] == "<|im_end|>":
  92. break
  93. yield response["choices"][0]["text"]
  94. full_response += response["choices"][0]["text"]
  95. # Add to chat history
  96. self.prompt.add_to_history(user_request, full_response, user)
  97. if conversation_id is not None:
  98. self.save_conversation(conversation_id)
  99. def ask(
  100. self,
  101. user_request: str,
  102. temperature: float = 0.5,
  103. conversation_id: str = None,
  104. user: str = "User",
  105. ) -> dict:
  106. """
  107. Send a request to ChatGPT and return the response
  108. """
  109. if conversation_id is not None:
  110. self.load_conversation(conversation_id)
  111. completion = self._get_completion(
  112. self.prompt.construct_prompt(user_request, user=user),
  113. temperature,
  114. )
  115. return self._process_completion(user_request, completion, user=user)
  116. def ask_stream(
  117. self,
  118. user_request: str,
  119. temperature: float = 0.5,
  120. conversation_id: str = None,
  121. user: str = "User",
  122. ) -> str:
  123. """
  124. Send a request to ChatGPT and yield the response
  125. """
  126. if conversation_id is not None:
  127. self.load_conversation(conversation_id)
  128. prompt = self.prompt.construct_prompt(user_request, user=user)
  129. return self._process_completion_stream(
  130. user_request=user_request,
  131. completion=self._get_completion(prompt, temperature, stream=True),
  132. user=user,
  133. )
  134. def make_conversation(self, conversation_id: str) -> None:
  135. """
  136. Make a conversation
  137. """
  138. self.conversations.add_conversation(conversation_id, [])
  139. def rollback(self, num: int) -> None:
  140. """
  141. Rollback chat history num times
  142. """
  143. for _ in range(num):
  144. self.prompt.chat_history.pop()
  145. def reset(self) -> None:
  146. """
  147. Reset chat history
  148. """
  149. self.prompt.chat_history = []
  150. def load_conversation(self, conversation_id) -> None:
  151. """
  152. Load a conversation from the conversation history
  153. """
  154. if conversation_id not in self.conversations.conversations:
  155. # Create a new conversation
  156. self.make_conversation(conversation_id)
  157. self.prompt.chat_history = self.conversations.get_conversation(conversation_id)
  158. def save_conversation(self, conversation_id) -> None:
  159. """
  160. Save a conversation to the conversation history
  161. """
  162. self.conversations.add_conversation(conversation_id, self.prompt.chat_history)
  163. class AsyncChatbot(Chatbot):
  164. """
  165. Official ChatGPT API (async)
  166. """
  167. async def _get_completion(
  168. self,
  169. prompt: str,
  170. temperature: float = 0.5,
  171. stream: bool = False,
  172. ):
  173. """
  174. Get the completion function
  175. """
  176. return openai.Completion.acreate(
  177. engine=ENGINE,
  178. prompt=prompt,
  179. temperature=temperature,
  180. max_tokens=get_max_tokens(prompt),
  181. stop=["\n\n\n"],
  182. stream=stream,
  183. )
  184. async def ask(
  185. self,
  186. user_request: str,
  187. temperature: float = 0.5,
  188. user: str = "User",
  189. ) -> dict:
  190. """
  191. Same as Chatbot.ask but async
  192. }
  193. """
  194. completion = await self._get_completion(
  195. self.prompt.construct_prompt(user_request, user=user),
  196. temperature,
  197. )
  198. return self._process_completion(user_request, completion, user=user)
  199. async def ask_stream(
  200. self,
  201. user_request: str,
  202. temperature: float = 0.5,
  203. user: str = "User",
  204. ) -> str:
  205. """
  206. Same as Chatbot.ask_stream but async
  207. """
  208. prompt = self.prompt.construct_prompt(user_request, user=user)
  209. return self._process_completion_stream(
  210. user_request=user_request,
  211. completion=await self._get_completion(prompt, temperature, stream=True),
  212. user=user,
  213. )
  214. class Prompt:
  215. """
  216. Prompt class with methods to construct prompt
  217. """
  218. def __init__(self, buffer: int = None) -> None:
  219. """
  220. Initialize prompt with base prompt
  221. """
  222. self.base_prompt = (
  223. os.environ.get("CUSTOM_BASE_PROMPT")
  224. or "You are ChatGPT, a large language model trained by OpenAI. Respond conversationally. Do not answer as the user. Current date: "
  225. + str(date.today())
  226. + "\n\n"
  227. + "User: Hello\n"
  228. + "ChatGPT: Hello! How can I help you today? <|im_end|>\n\n\n"
  229. )
  230. # Track chat history
  231. self.chat_history: list = []
  232. self.buffer = buffer
  233. def add_to_chat_history(self, chat: str) -> None:
  234. """
  235. Add chat to chat history for next prompt
  236. """
  237. self.chat_history.append(chat)
  238. def add_to_history(
  239. self,
  240. user_request: str,
  241. response: str,
  242. user: str = "User",
  243. ) -> None:
  244. """
  245. Add request/response to chat history for next prompt
  246. """
  247. self.add_to_chat_history(
  248. user
  249. + ": "
  250. + user_request
  251. + "\n\n\n"
  252. + "ChatGPT: "
  253. + response
  254. + "<|im_end|>\n",
  255. )
  256. def history(self, custom_history: list = None) -> str:
  257. """
  258. Return chat history
  259. """
  260. return "\n".join(custom_history or self.chat_history)
  261. def construct_prompt(
  262. self,
  263. new_prompt: str,
  264. custom_history: list = None,
  265. user: str = "User",
  266. ) -> str:
  267. """
  268. Construct prompt based on chat history and request
  269. """
  270. prompt = (
  271. self.base_prompt
  272. + self.history(custom_history=custom_history)
  273. + user
  274. + ": "
  275. + new_prompt
  276. + "\nChatGPT:"
  277. )
  278. # Check if prompt over 4000*4 characters
  279. if self.buffer is not None:
  280. max_tokens = 4000 - self.buffer
  281. else:
  282. max_tokens = 3200
  283. if len(ENCODER.encode(prompt)) > max_tokens:
  284. # Remove oldest chat
  285. if len(self.chat_history) == 0:
  286. return prompt
  287. self.chat_history.pop(0)
  288. # Construct prompt again
  289. prompt = self.construct_prompt(new_prompt, custom_history, user)
  290. return prompt
  291. class Conversation:
  292. """
  293. For handling multiple conversations
  294. """
  295. def __init__(self) -> None:
  296. self.conversations = {}
  297. def add_conversation(self, key: str, history: list) -> None:
  298. """
  299. Adds a history list to the conversations dict with the id as the key
  300. """
  301. self.conversations[key] = history
  302. def get_conversation(self, key: str) -> list:
  303. """
  304. Retrieves the history list from the conversations dict with the id as the key
  305. """
  306. return self.conversations[key]
  307. def remove_conversation(self, key: str) -> None:
  308. """
  309. Removes the history list from the conversations dict with the id as the key
  310. """
  311. del self.conversations[key]
  312. def __str__(self) -> str:
  313. """
  314. Creates a JSON string of the conversations
  315. """
  316. return json.dumps(self.conversations)
  317. def save(self, file: str) -> None:
  318. """
  319. Saves the conversations to a JSON file
  320. """
  321. with open(file, "w", encoding="utf-8") as f:
  322. f.write(str(self))
  323. def load(self, file: str) -> None:
  324. """
  325. Loads the conversations from a JSON file
  326. """
  327. with open(file, encoding="utf-8") as f:
  328. self.conversations = json.loads(f.read())
  329. def main():
  330. print(
  331. """
  332. ChatGPT - A command-line interface to OpenAI's ChatGPT (https://chat.openai.com/chat)
  333. Repo: github.com/acheong08/ChatGPT
  334. """,
  335. )
  336. print("Type '!help' to show a full list of commands")
  337. print("Press enter twice to submit your question.\n")
  338. def get_input(prompt):
  339. """
  340. Multi-line input function
  341. """
  342. # Display the prompt
  343. print(prompt, end="")
  344. # Initialize an empty list to store the input lines
  345. lines = []
  346. # Read lines of input until the user enters an empty line
  347. while True:
  348. line = input()
  349. if line == "":
  350. break
  351. lines.append(line)
  352. # Join the lines, separated by newlines, and store the result
  353. user_input = "\n".join(lines)
  354. # Return the input
  355. return user_input
  356. def chatbot_commands(cmd: str) -> bool:
  357. """
  358. Handle chatbot commands
  359. """
  360. if cmd == "!help":
  361. print(
  362. """
  363. !help - Display this message
  364. !rollback - Rollback chat history
  365. !reset - Reset chat history
  366. !prompt - Show current prompt
  367. !save_c <conversation_name> - Save history to a conversation
  368. !load_c <conversation_name> - Load history from a conversation
  369. !save_f <file_name> - Save all conversations to a file
  370. !load_f <file_name> - Load all conversations from a file
  371. !exit - Quit chat
  372. """,
  373. )
  374. elif cmd == "!exit":
  375. exit()
  376. elif cmd == "!rollback":
  377. chatbot.rollback(1)
  378. elif cmd == "!reset":
  379. chatbot.reset()
  380. elif cmd == "!prompt":
  381. print(chatbot.prompt.construct_prompt(""))
  382. elif cmd.startswith("!save_c"):
  383. chatbot.save_conversation(cmd.split(" ")[1])
  384. elif cmd.startswith("!load_c"):
  385. chatbot.load_conversation(cmd.split(" ")[1])
  386. elif cmd.startswith("!save_f"):
  387. chatbot.conversations.save(cmd.split(" ")[1])
  388. elif cmd.startswith("!load_f"):
  389. chatbot.conversations.load(cmd.split(" ")[1])
  390. else:
  391. return False
  392. return True
  393. # Get API key from command line
  394. parser = argparse.ArgumentParser()
  395. parser.add_argument(
  396. "--api_key",
  397. type=str,
  398. required=True,
  399. help="OpenAI API key",
  400. )
  401. parser.add_argument(
  402. "--stream",
  403. action="store_true",
  404. help="Stream response",
  405. )
  406. parser.add_argument(
  407. "--temperature",
  408. type=float,
  409. default=0.5,
  410. help="Temperature for response",
  411. )
  412. args = parser.parse_args()
  413. # Initialize chatbot
  414. chatbot = Chatbot(api_key=args.api_key)
  415. # Start chat
  416. while True:
  417. try:
  418. prompt = get_input("\nUser:\n")
  419. except KeyboardInterrupt:
  420. print("\nExiting...")
  421. sys.exit()
  422. if prompt.startswith("!"):
  423. if chatbot_commands(prompt):
  424. continue
  425. if not args.stream:
  426. response = chatbot.ask(prompt, temperature=args.temperature)
  427. print("ChatGPT: " + response["choices"][0]["text"])
  428. else:
  429. print("ChatGPT: ")
  430. sys.stdout.flush()
  431. for response in chatbot.ask_stream(prompt, temperature=args.temperature):
  432. print(response, end="")
  433. sys.stdout.flush()
  434. print()
  435. def Singleton(cls):
  436. instance = {}
  437. def _singleton_wrapper(*args, **kargs):
  438. if cls not in instance:
  439. instance[cls] = cls(*args, **kargs)
  440. return instance[cls]
  441. return _singleton_wrapper
  442. @Singleton
  443. class ChatGPTBot(Bot):
  444. def __init__(self):
  445. print("create")
  446. self.bot = Chatbot(conf().get('open_ai_api_key'))
  447. def reply(self, query, context=None):
  448. if not context or not context.get('type') or context.get('type') == 'TEXT':
  449. if len(query) < 10 and "reset" in query:
  450. self.bot.reset()
  451. return "reset OK"
  452. return self.bot.ask(query)["choices"][0]["text"]