edge_gpt_free.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125
  1. """
  2. ========================================================================
  3. 第一部分:来自EdgeGPT.py
  4. https://github.com/acheong08/EdgeGPT
  5. ========================================================================
  6. """
  7. """
  8. Main.py
  9. """
  10. import argparse
  11. import asyncio
  12. import json
  13. import os
  14. import random
  15. import re
  16. import ssl
  17. import sys
  18. import time
  19. import uuid
  20. from enum import Enum
  21. from pathlib import Path
  22. from typing import Generator
  23. from typing import Literal
  24. from typing import Optional
  25. from typing import Union
  26. import aiohttp
  27. import certifi
  28. import httpx
  29. from prompt_toolkit import PromptSession
  30. from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
  31. from prompt_toolkit.completion import WordCompleter
  32. from prompt_toolkit.history import InMemoryHistory
  33. from prompt_toolkit.key_binding import KeyBindings
  34. from rich.live import Live
  35. from rich.markdown import Markdown
  36. DELIMITER = "\x1e"
  37. # Generate random IP between range 13.104.0.0/14
  38. FORWARDED_IP = (
  39. f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}"
  40. )
  41. HEADERS = {
  42. "accept": "application/json",
  43. "accept-language": "en-US,en;q=0.9",
  44. "content-type": "application/json",
  45. "sec-ch-ua": '"Not_A Brand";v="99", "Microsoft Edge";v="110", "Chromium";v="110"',
  46. "sec-ch-ua-arch": '"x86"',
  47. "sec-ch-ua-bitness": '"64"',
  48. "sec-ch-ua-full-version": '"109.0.1518.78"',
  49. "sec-ch-ua-full-version-list": '"Chromium";v="110.0.5481.192", "Not A(Brand";v="24.0.0.0", "Microsoft Edge";v="110.0.1587.69"',
  50. "sec-ch-ua-mobile": "?0",
  51. "sec-ch-ua-model": "",
  52. "sec-ch-ua-platform": '"Windows"',
  53. "sec-ch-ua-platform-version": '"15.0.0"',
  54. "sec-fetch-dest": "empty",
  55. "sec-fetch-mode": "cors",
  56. "sec-fetch-site": "same-origin",
  57. "x-ms-client-request-id": str(uuid.uuid4()),
  58. "x-ms-useragent": "azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32",
  59. "Referer": "https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx",
  60. "Referrer-Policy": "origin-when-cross-origin",
  61. "x-forwarded-for": FORWARDED_IP,
  62. }
  63. HEADERS_INIT_CONVER = {
  64. "authority": "edgeservices.bing.com",
  65. "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  66. "accept-language": "en-US,en;q=0.9",
  67. "cache-control": "max-age=0",
  68. "sec-ch-ua": '"Chromium";v="110", "Not A(Brand";v="24", "Microsoft Edge";v="110"',
  69. "sec-ch-ua-arch": '"x86"',
  70. "sec-ch-ua-bitness": '"64"',
  71. "sec-ch-ua-full-version": '"110.0.1587.69"',
  72. "sec-ch-ua-full-version-list": '"Chromium";v="110.0.5481.192", "Not A(Brand";v="24.0.0.0", "Microsoft Edge";v="110.0.1587.69"',
  73. "sec-ch-ua-mobile": "?0",
  74. "sec-ch-ua-model": '""',
  75. "sec-ch-ua-platform": '"Windows"',
  76. "sec-ch-ua-platform-version": '"15.0.0"',
  77. "sec-fetch-dest": "document",
  78. "sec-fetch-mode": "navigate",
  79. "sec-fetch-site": "none",
  80. "sec-fetch-user": "?1",
  81. "upgrade-insecure-requests": "1",
  82. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.69",
  83. "x-edge-shopping-flag": "1",
  84. "x-forwarded-for": FORWARDED_IP,
  85. }
  86. ssl_context = ssl.create_default_context()
  87. ssl_context.load_verify_locations(certifi.where())
  88. class NotAllowedToAccess(Exception):
  89. pass
  90. class ConversationStyle(Enum):
  91. creative = [
  92. "nlu_direct_response_filter",
  93. "deepleo",
  94. "disable_emoji_spoken_text",
  95. "responsible_ai_policy_235",
  96. "enablemm",
  97. "h3imaginative",
  98. "travelansgnd",
  99. "dv3sugg",
  100. "clgalileo",
  101. "gencontentv3",
  102. "dv3sugg",
  103. "responseos",
  104. "e2ecachewrite",
  105. "cachewriteext",
  106. "nodlcpcwrite",
  107. "travelansgnd",
  108. "nojbfedge",
  109. ]
  110. balanced = [
  111. "nlu_direct_response_filter",
  112. "deepleo",
  113. "disable_emoji_spoken_text",
  114. "responsible_ai_policy_235",
  115. "enablemm",
  116. "galileo",
  117. "dv3sugg",
  118. "responseos",
  119. "e2ecachewrite",
  120. "cachewriteext",
  121. "nodlcpcwrite",
  122. "travelansgnd",
  123. "nojbfedge",
  124. ]
  125. precise = [
  126. "nlu_direct_response_filter",
  127. "deepleo",
  128. "disable_emoji_spoken_text",
  129. "responsible_ai_policy_235",
  130. "enablemm",
  131. "galileo",
  132. "dv3sugg",
  133. "responseos",
  134. "e2ecachewrite",
  135. "cachewriteext",
  136. "nodlcpcwrite",
  137. "travelansgnd",
  138. "h3precise",
  139. "clgalileo",
  140. "nojbfedge",
  141. ]
  142. CONVERSATION_STYLE_TYPE = Optional[
  143. Union[ConversationStyle, Literal["creative", "balanced", "precise"]]
  144. ]
  145. def _append_identifier(msg: dict) -> str:
  146. """
  147. Appends special character to end of message to identify end of message
  148. """
  149. # Convert dict to json string
  150. return json.dumps(msg, ensure_ascii=False) + DELIMITER
  151. def _get_ran_hex(length: int = 32) -> str:
  152. """
  153. Returns random hex string
  154. """
  155. return "".join(random.choice("0123456789abcdef") for _ in range(length))
  156. class _ChatHubRequest:
  157. """
  158. Request object for ChatHub
  159. """
  160. def __init__(
  161. self,
  162. conversation_signature: str,
  163. client_id: str,
  164. conversation_id: str,
  165. invocation_id: int = 0,
  166. ) -> None:
  167. self.struct: dict = {}
  168. self.client_id: str = client_id
  169. self.conversation_id: str = conversation_id
  170. self.conversation_signature: str = conversation_signature
  171. self.invocation_id: int = invocation_id
  172. def update(
  173. self,
  174. prompt: str,
  175. conversation_style: CONVERSATION_STYLE_TYPE,
  176. options = None,
  177. webpage_context = None,
  178. search_result = False,
  179. ) -> None:
  180. """
  181. Updates request object
  182. """
  183. if options is None:
  184. options = [
  185. "deepleo",
  186. "enable_debug_commands",
  187. "disable_emoji_spoken_text",
  188. "enablemm",
  189. ]
  190. if conversation_style:
  191. if not isinstance(conversation_style, ConversationStyle):
  192. conversation_style = getattr(ConversationStyle, conversation_style)
  193. options = conversation_style.value
  194. self.struct = {
  195. "arguments": [
  196. {
  197. "source": "cib",
  198. "optionsSets": options,
  199. "allowedMessageTypes": [
  200. "Chat",
  201. "Disengaged",
  202. "AdsQuery",
  203. "SemanticSerp",
  204. "GenerateContentQuery",
  205. "SearchQuery",
  206. ],
  207. "sliceIds": [
  208. "chk1cf",
  209. "nopreloadsscf",
  210. "winlongmsg2tf",
  211. "perfimpcomb",
  212. "sugdivdis",
  213. "sydnoinputt",
  214. "wpcssopt",
  215. "wintone2tf",
  216. "0404sydicnbs0",
  217. "405suggbs0",
  218. "scctl",
  219. "330uaugs0",
  220. "0329resp",
  221. "udscahrfon",
  222. "udstrblm5",
  223. "404e2ewrt",
  224. "408nodedups0",
  225. "403tvlansgnd",
  226. ],
  227. "traceId": _get_ran_hex(32),
  228. "isStartOfSession": self.invocation_id == 0,
  229. "message": {
  230. "author": "user",
  231. "inputMethod": "Keyboard",
  232. "text": prompt,
  233. "messageType": "Chat",
  234. },
  235. "conversationSignature": self.conversation_signature,
  236. "participant": {
  237. "id": self.client_id,
  238. },
  239. "conversationId": self.conversation_id,
  240. },
  241. ],
  242. "invocationId": str(self.invocation_id),
  243. "target": "chat",
  244. "type": 4,
  245. }
  246. if search_result:
  247. have_search_result = [
  248. "InternalSearchQuery",
  249. "InternalSearchResult",
  250. "InternalLoaderMessage",
  251. "RenderCardRequest",
  252. ]
  253. self.struct["arguments"][0]["allowedMessageTypes"] += have_search_result
  254. if webpage_context:
  255. self.struct["arguments"][0]["previousMessages"] = [
  256. {
  257. "author": "user",
  258. "description": webpage_context,
  259. "contextType": "WebPage",
  260. "messageType": "Context",
  261. "messageId": "discover-web--page-ping-mriduna-----",
  262. },
  263. ]
  264. self.invocation_id += 1
  265. class _Conversation:
  266. """
  267. Conversation API
  268. """
  269. def __init__(
  270. self,
  271. proxy = None,
  272. async_mode = False,
  273. cookies = None,
  274. ) -> None:
  275. if async_mode:
  276. return
  277. self.struct: dict = {
  278. "conversationId": None,
  279. "clientId": None,
  280. "conversationSignature": None,
  281. "result": {"value": "Success", "message": None},
  282. }
  283. self.proxy = proxy
  284. proxy = (
  285. proxy
  286. or os.environ.get("all_proxy")
  287. or os.environ.get("ALL_PROXY")
  288. or os.environ.get("https_proxy")
  289. or os.environ.get("HTTPS_PROXY")
  290. or None
  291. )
  292. if proxy is not None and proxy.startswith("socks5h://"):
  293. proxy = "socks5://" + proxy[len("socks5h://") :]
  294. self.session = httpx.Client(
  295. proxies=proxy,
  296. timeout=30,
  297. headers=HEADERS_INIT_CONVER,
  298. )
  299. if cookies:
  300. for cookie in cookies:
  301. self.session.cookies.set(cookie["name"], cookie["value"])
  302. # Send GET request
  303. response = self.session.get(
  304. url=os.environ.get("BING_PROXY_URL")
  305. or "https://edgeservices.bing.com/edgesvc/turing/conversation/create",
  306. )
  307. if response.status_code != 200:
  308. response = self.session.get(
  309. "https://edge.churchless.tech/edgesvc/turing/conversation/create",
  310. )
  311. if response.status_code != 200:
  312. print(f"Status code: {response.status_code}")
  313. print(response.text)
  314. print(response.url)
  315. raise Exception("Authentication failed")
  316. try:
  317. self.struct = response.json()
  318. except (json.decoder.JSONDecodeError, NotAllowedToAccess) as exc:
  319. raise Exception(
  320. "Authentication failed. You have not been accepted into the beta.",
  321. ) from exc
  322. if self.struct["result"]["value"] == "UnauthorizedRequest":
  323. raise NotAllowedToAccess(self.struct["result"]["message"])
  324. @staticmethod
  325. async def create(
  326. proxy = None,
  327. cookies = None,
  328. ):
  329. self = _Conversation(async_mode=True)
  330. self.struct = {
  331. "conversationId": None,
  332. "clientId": None,
  333. "conversationSignature": None,
  334. "result": {"value": "Success", "message": None},
  335. }
  336. self.proxy = proxy
  337. proxy = (
  338. proxy
  339. or os.environ.get("all_proxy")
  340. or os.environ.get("ALL_PROXY")
  341. or os.environ.get("https_proxy")
  342. or os.environ.get("HTTPS_PROXY")
  343. or None
  344. )
  345. if proxy is not None and proxy.startswith("socks5h://"):
  346. proxy = "socks5://" + proxy[len("socks5h://") :]
  347. transport = httpx.AsyncHTTPTransport(retries=10)
  348. # Convert cookie format to httpx format
  349. formatted_cookies = None
  350. if cookies:
  351. formatted_cookies = httpx.Cookies()
  352. for cookie in cookies:
  353. formatted_cookies.set(cookie["name"], cookie["value"])
  354. async with httpx.AsyncClient(
  355. proxies=proxy,
  356. timeout=30,
  357. headers=HEADERS_INIT_CONVER,
  358. transport=transport,
  359. cookies=formatted_cookies,
  360. ) as client:
  361. # Send GET request
  362. response = await client.get(
  363. url=os.environ.get("BING_PROXY_URL")
  364. or "https://edgeservices.bing.com/edgesvc/turing/conversation/create",
  365. )
  366. if response.status_code != 200:
  367. response = await client.get(
  368. "https://edge.churchless.tech/edgesvc/turing/conversation/create",
  369. )
  370. if response.status_code != 200:
  371. print(f"Status code: {response.status_code}")
  372. print(response.text)
  373. print(response.url)
  374. raise Exception("Authentication failed")
  375. try:
  376. self.struct = response.json()
  377. except (json.decoder.JSONDecodeError, NotAllowedToAccess) as exc:
  378. raise Exception(
  379. "Authentication failed. You have not been accepted into the beta.",
  380. ) from exc
  381. if self.struct["result"]["value"] == "UnauthorizedRequest":
  382. raise NotAllowedToAccess(self.struct["result"]["message"])
  383. return self
  384. class _ChatHub:
  385. """
  386. Chat API
  387. """
  388. def __init__(
  389. self,
  390. conversation: _Conversation,
  391. proxy = None,
  392. cookies = None,
  393. ) -> None:
  394. self.session = None
  395. self.wss = None
  396. self.request: _ChatHubRequest
  397. self.loop: bool
  398. self.task: asyncio.Task
  399. self.request = _ChatHubRequest(
  400. conversation_signature=conversation.struct["conversationSignature"],
  401. client_id=conversation.struct["clientId"],
  402. conversation_id=conversation.struct["conversationId"],
  403. )
  404. self.cookies = cookies
  405. self.proxy: str = proxy
  406. async def ask_stream(
  407. self,
  408. prompt: str,
  409. wss_link: str,
  410. conversation_style: CONVERSATION_STYLE_TYPE = None,
  411. raw: bool = False,
  412. options: dict = None,
  413. webpage_context = None,
  414. search_result: bool = False,
  415. ) -> Generator[str, None, None]:
  416. """
  417. Ask a question to the bot
  418. """
  419. req_header = HEADERS
  420. if self.cookies is not None:
  421. ws_cookies = []
  422. for cookie in self.cookies:
  423. ws_cookies.append(f"{cookie['name']}={cookie['value']}")
  424. req_header.update({
  425. 'Cookie': ';'.join(ws_cookies),
  426. })
  427. timeout = aiohttp.ClientTimeout(total=30)
  428. self.session = aiohttp.ClientSession(timeout=timeout)
  429. if self.wss and not self.wss.closed:
  430. await self.wss.close()
  431. # Check if websocket is closed
  432. self.wss = await self.session.ws_connect(
  433. wss_link,
  434. headers=req_header,
  435. ssl=ssl_context,
  436. proxy=self.proxy,
  437. autoping=False,
  438. )
  439. await self._initial_handshake()
  440. if self.request.invocation_id == 0:
  441. # Construct a ChatHub request
  442. self.request.update(
  443. prompt=prompt,
  444. conversation_style=conversation_style,
  445. options=options,
  446. webpage_context=webpage_context,
  447. search_result=search_result,
  448. )
  449. else:
  450. async with httpx.AsyncClient() as client:
  451. response = await client.post(
  452. "https://sydney.bing.com/sydney/UpdateConversation/",
  453. json={
  454. "messages": [
  455. {
  456. "author": "user",
  457. "description": webpage_context,
  458. "contextType": "WebPage",
  459. "messageType": "Context",
  460. },
  461. ],
  462. "conversationId": self.request.conversation_id,
  463. "source": "cib",
  464. "traceId": _get_ran_hex(32),
  465. "participant": {"id": self.request.client_id},
  466. "conversationSignature": self.request.conversation_signature,
  467. },
  468. )
  469. if response.status_code != 200:
  470. print(f"Status code: {response.status_code}")
  471. print(response.text)
  472. print(response.url)
  473. raise Exception("Update web page context failed")
  474. # Construct a ChatHub request
  475. self.request.update(
  476. prompt=prompt,
  477. conversation_style=conversation_style,
  478. options=options,
  479. )
  480. # Send request
  481. await self.wss.send_str(_append_identifier(self.request.struct))
  482. final = False
  483. draw = False
  484. resp_txt = ""
  485. result_text = ""
  486. resp_txt_no_link = ""
  487. while not final:
  488. msg = await self.wss.receive()
  489. try:
  490. objects = msg.data.split(DELIMITER)
  491. except :
  492. continue
  493. for obj in objects:
  494. if obj is None or not obj:
  495. continue
  496. response = json.loads(obj)
  497. if response.get("type") != 2 and raw:
  498. yield False, response
  499. elif response.get("type") == 1 and response["arguments"][0].get(
  500. "messages",
  501. ):
  502. if not draw:
  503. if (
  504. response["arguments"][0]["messages"][0].get("messageType")
  505. == "GenerateContentQuery"
  506. ):
  507. async with ImageGenAsync("", True) as image_generator:
  508. images = await image_generator.get_images(
  509. response["arguments"][0]["messages"][0]["text"],
  510. )
  511. for i, image in enumerate(images):
  512. resp_txt = resp_txt + f"\n![image{i}]({image})"
  513. draw = True
  514. if (
  515. response["arguments"][0]["messages"][0]["contentOrigin"]
  516. != "Apology"
  517. ) and not draw:
  518. resp_txt = result_text + response["arguments"][0][
  519. "messages"
  520. ][0]["adaptiveCards"][0]["body"][0].get("text", "")
  521. resp_txt_no_link = result_text + response["arguments"][0][
  522. "messages"
  523. ][0].get("text", "")
  524. if response["arguments"][0]["messages"][0].get(
  525. "messageType",
  526. ):
  527. resp_txt = (
  528. resp_txt
  529. + response["arguments"][0]["messages"][0][
  530. "adaptiveCards"
  531. ][0]["body"][0]["inlines"][0].get("text")
  532. + "\n"
  533. )
  534. result_text = (
  535. result_text
  536. + response["arguments"][0]["messages"][0][
  537. "adaptiveCards"
  538. ][0]["body"][0]["inlines"][0].get("text")
  539. + "\n"
  540. )
  541. yield False, resp_txt
  542. elif response.get("type") == 2:
  543. if response["item"]["result"].get("error"):
  544. await self.close()
  545. raise Exception(
  546. f"{response['item']['result']['value']}: {response['item']['result']['message']}",
  547. )
  548. if draw:
  549. cache = response["item"]["messages"][1]["adaptiveCards"][0][
  550. "body"
  551. ][0]["text"]
  552. response["item"]["messages"][1]["adaptiveCards"][0]["body"][0][
  553. "text"
  554. ] = (cache + resp_txt)
  555. if (
  556. response["item"]["messages"][-1]["contentOrigin"] == "Apology"
  557. and resp_txt
  558. ):
  559. response["item"]["messages"][-1]["text"] = resp_txt_no_link
  560. response["item"]["messages"][-1]["adaptiveCards"][0]["body"][0][
  561. "text"
  562. ] = resp_txt
  563. print(
  564. "Preserved the message from being deleted",
  565. file=sys.stderr,
  566. )
  567. final = True
  568. await self.close()
  569. yield True, response
  570. async def _initial_handshake(self) -> None:
  571. await self.wss.send_str(_append_identifier({"protocol": "json", "version": 1}))
  572. await self.wss.receive()
  573. async def close(self) -> None:
  574. """
  575. Close the connection
  576. """
  577. if self.wss and not self.wss.closed:
  578. await self.wss.close()
  579. if self.session and not self.session.closed:
  580. await self.session.close()
  581. class Chatbot:
  582. """
  583. Combines everything to make it seamless
  584. """
  585. def __init__(
  586. self,
  587. proxy = None,
  588. cookies = None,
  589. ) -> None:
  590. self.proxy = proxy
  591. self.chat_hub: _ChatHub = _ChatHub(
  592. _Conversation(self.proxy, cookies=cookies),
  593. proxy=self.proxy,
  594. cookies=cookies,
  595. )
  596. @staticmethod
  597. async def create(
  598. proxy = None,
  599. cookies = None,
  600. ):
  601. self = Chatbot.__new__(Chatbot)
  602. self.proxy = proxy
  603. self.chat_hub = _ChatHub(
  604. await _Conversation.create(self.proxy, cookies=cookies),
  605. proxy=self.proxy,
  606. cookies=cookies,
  607. )
  608. return self
  609. async def ask(
  610. self,
  611. prompt: str,
  612. wss_link: str = "wss://sydney.bing.com/sydney/ChatHub",
  613. conversation_style: CONVERSATION_STYLE_TYPE = None,
  614. options: dict = None,
  615. webpage_context = None,
  616. search_result: bool = False,
  617. ) -> dict:
  618. """
  619. Ask a question to the bot
  620. """
  621. async for final, response in self.chat_hub.ask_stream(
  622. prompt=prompt,
  623. conversation_style=conversation_style,
  624. wss_link=wss_link,
  625. options=options,
  626. webpage_context=webpage_context,
  627. search_result=search_result,
  628. ):
  629. if final:
  630. return response
  631. await self.chat_hub.wss.close()
  632. return {}
  633. async def ask_stream(
  634. self,
  635. prompt: str,
  636. wss_link: str = "wss://sydney.bing.com/sydney/ChatHub",
  637. conversation_style: CONVERSATION_STYLE_TYPE = None,
  638. raw: bool = False,
  639. options: dict = None,
  640. webpage_context = None,
  641. search_result: bool = False,
  642. ) -> Generator[str, None, None]:
  643. """
  644. Ask a question to the bot
  645. """
  646. async for response in self.chat_hub.ask_stream(
  647. prompt=prompt,
  648. conversation_style=conversation_style,
  649. wss_link=wss_link,
  650. raw=raw,
  651. options=options,
  652. webpage_context=webpage_context,
  653. search_result=search_result,
  654. ):
  655. yield response
  656. async def close(self) -> None:
  657. """
  658. Close the connection
  659. """
  660. await self.chat_hub.close()
  661. async def reset(self) -> None:
  662. """
  663. Reset the conversation
  664. """
  665. await self.close()
  666. self.chat_hub = _ChatHub(
  667. await _Conversation.create(self.proxy),
  668. proxy=self.proxy,
  669. cookies=self.chat_hub.cookies,
  670. )
  671. async def _get_input_async(
  672. session: PromptSession = None,
  673. completer: WordCompleter = None,
  674. ) -> str:
  675. """
  676. Multiline input function.
  677. """
  678. return await session.prompt_async(
  679. completer=completer,
  680. multiline=True,
  681. auto_suggest=AutoSuggestFromHistory(),
  682. )
  683. def _create_session() -> PromptSession:
  684. kb = KeyBindings()
  685. @kb.add("enter")
  686. def _(event):
  687. buffer_text = event.current_buffer.text
  688. if buffer_text.startswith("!"):
  689. event.current_buffer.validate_and_handle()
  690. else:
  691. event.current_buffer.insert_text("\n")
  692. @kb.add("escape")
  693. def _(event):
  694. if event.current_buffer.complete_state:
  695. # event.current_buffer.cancel_completion()
  696. event.current_buffer.text = ""
  697. return PromptSession(key_bindings=kb, history=InMemoryHistory())
  698. def _create_completer(commands: list, pattern_str: str = "$"):
  699. return WordCompleter(words=commands, pattern=re.compile(pattern_str))
  700. async def async_main(args: argparse.Namespace) -> None:
  701. """
  702. Main function
  703. """
  704. print("Initializing...")
  705. print("Enter `alt+enter` or `escape+enter` to send a message")
  706. # Read and parse cookies
  707. cookies = None
  708. if args.cookie_file:
  709. cookies = json.loads(open(args.cookie_file, encoding="utf-8").read())
  710. bot = await Chatbot.create(proxy=args.proxy, cookies=cookies)
  711. session = _create_session()
  712. completer = _create_completer(["!help", "!exit", "!reset"])
  713. initial_prompt = args.prompt
  714. while True:
  715. print("\nYou:")
  716. if initial_prompt:
  717. question = initial_prompt
  718. print(question)
  719. initial_prompt = None
  720. else:
  721. question = (
  722. input()
  723. if args.enter_once
  724. else await _get_input_async(session=session, completer=completer)
  725. )
  726. print()
  727. if question == "!exit":
  728. break
  729. if question == "!help":
  730. print(
  731. """
  732. !help - Show this help message
  733. !exit - Exit the program
  734. !reset - Reset the conversation
  735. """,
  736. )
  737. continue
  738. if question == "!reset":
  739. await bot.reset()
  740. continue
  741. print("Bot:")
  742. if args.no_stream:
  743. print(
  744. (
  745. await bot.ask(
  746. prompt=question,
  747. conversation_style=args.style,
  748. wss_link=args.wss_link,
  749. )
  750. )["item"]["messages"][1]["adaptiveCards"][0]["body"][0]["text"],
  751. )
  752. else:
  753. wrote = 0
  754. if args.rich:
  755. md = Markdown("")
  756. with Live(md, auto_refresh=False) as live:
  757. async for final, response in bot.ask_stream(
  758. prompt=question,
  759. conversation_style=args.style,
  760. wss_link=args.wss_link,
  761. ):
  762. if not final:
  763. if wrote > len(response):
  764. print(md)
  765. print(Markdown("***Bing revoked the response.***"))
  766. wrote = len(response)
  767. md = Markdown(response)
  768. live.update(md, refresh=True)
  769. else:
  770. async for final, response in bot.ask_stream(
  771. prompt=question,
  772. conversation_style=args.style,
  773. wss_link=args.wss_link,
  774. ):
  775. if not final:
  776. if not wrote:
  777. print(response, end="", flush=True)
  778. else:
  779. print(response[wrote:], end="", flush=True)
  780. wrote = len(response)
  781. print()
  782. await bot.close()
  783. def main() -> None:
  784. print(
  785. """
  786. EdgeGPT - A demo of reverse engineering the Bing GPT chatbot
  787. Repo: github.com/acheong08/EdgeGPT
  788. By: Antonio Cheong
  789. !help for help
  790. Type !exit to exit
  791. """,
  792. )
  793. parser = argparse.ArgumentParser()
  794. parser.add_argument("--enter-once", action="store_true")
  795. parser.add_argument("--no-stream", action="store_true")
  796. parser.add_argument("--rich", action="store_true")
  797. parser.add_argument(
  798. "--proxy",
  799. help="Proxy URL (e.g. socks5://127.0.0.1:1080)",
  800. type=str,
  801. )
  802. parser.add_argument(
  803. "--wss-link",
  804. help="WSS URL(e.g. wss://sydney.bing.com/sydney/ChatHub)",
  805. type=str,
  806. default="wss://sydney.bing.com/sydney/ChatHub",
  807. )
  808. parser.add_argument(
  809. "--style",
  810. choices=["creative", "balanced", "precise"],
  811. default="balanced",
  812. )
  813. parser.add_argument(
  814. "--prompt",
  815. type=str,
  816. default="",
  817. required=False,
  818. help="prompt to start with",
  819. )
  820. parser.add_argument(
  821. "--cookie-file",
  822. type=str,
  823. default="",
  824. required=False,
  825. help="path to cookie file",
  826. )
  827. args = parser.parse_args()
  828. asyncio.run(async_main(args))
  829. class Cookie:
  830. """
  831. Convenience class for Bing Cookie files, data, and configuration. This Class
  832. is updated dynamically by the Query class to allow cycling through >1
  833. cookie/credentials file e.g. when daily request limits (current 200 per
  834. account per day) are exceeded.
  835. """
  836. current_file_index = 0
  837. dirpath = Path("./").resolve()
  838. search_pattern = "bing_cookies_*.json"
  839. ignore_files = set()
  840. @classmethod
  841. def fetch_default(cls, path=None):
  842. from selenium import webdriver
  843. from selenium.webdriver.common.by import By
  844. driver = webdriver.Edge()
  845. driver.get("https://bing.com/chat")
  846. time.sleep(5)
  847. xpath = '//button[@id="bnp_btn_accept"]'
  848. driver.find_element(By.XPATH, xpath).click()
  849. time.sleep(2)
  850. xpath = '//a[@id="codexPrimaryButton"]'
  851. driver.find_element(By.XPATH, xpath).click()
  852. if path is None:
  853. path = Path("./bing_cookies__default.json")
  854. # Double underscore ensures this file is first when sorted
  855. cookies = driver.get_cookies()
  856. Path(path).write_text(json.dumps(cookies, indent=4), encoding="utf-8")
  857. # Path again in case supplied path is: str
  858. print(f"Cookies saved to: {path}")
  859. driver.quit()
  860. @classmethod
  861. def files(cls):
  862. """Return a sorted list of all cookie files matching .search_pattern"""
  863. all_files = set(cls.dirpath.glob(cls.search_pattern))
  864. return sorted(list(all_files - cls.ignore_files))
  865. @classmethod
  866. def import_data(cls):
  867. """
  868. Read the active cookie file and populate the following attributes:
  869. .current_filepath
  870. .current_data
  871. .image_token
  872. """
  873. try:
  874. cls.current_filepath = cls.files()[cls.current_file_index]
  875. except IndexError:
  876. print(
  877. "> Please set Cookie.current_filepath to a valid cookie file, then run Cookie.import_data()",
  878. )
  879. return
  880. print(f"> Importing cookies from: {cls.current_filepath.name}")
  881. with open(cls.current_filepath, encoding="utf-8") as file:
  882. cls.current_data = json.load(file)
  883. cls.image_token = [x for x in cls.current_data if x.get("name") == "_U"]
  884. cls.image_token = cls.image_token[0].get("value")
  885. @classmethod
  886. def import_next(cls):
  887. """
  888. Cycle through to the next cookies file. Import it. Mark the previous
  889. file to be ignored for the remainder of the current session.
  890. """
  891. cls.ignore_files.add(cls.current_filepath)
  892. if Cookie.current_file_index >= len(cls.files()):
  893. Cookie.current_file_index = 0
  894. Cookie.import_data()
  895. class Query:
  896. """
  897. A convenience class that wraps around EdgeGPT.Chatbot to encapsulate input,
  898. config, and output all together. Relies on Cookie class for authentication
  899. """
  900. def __init__(
  901. self,
  902. prompt,
  903. style="precise",
  904. content_type="text",
  905. cookie_file=0,
  906. echo=True,
  907. echo_prompt=False,
  908. ):
  909. """
  910. Arguments:
  911. prompt: Text to enter into Bing Chat
  912. style: creative, balanced, or precise
  913. content_type: "text" for Bing Chat; "image" for Dall-e
  914. cookie_file: Path, filepath string, or index (int) to list of cookie paths
  915. echo: Print something to confirm request made
  916. echo_prompt: Print confirmation of the evaluated prompt
  917. """
  918. self.index = []
  919. self.request_count = {}
  920. self.image_dirpath = Path("./").resolve()
  921. Cookie.import_data()
  922. self.index += [self]
  923. self.prompt = prompt
  924. files = Cookie.files()
  925. if isinstance(cookie_file, int):
  926. index = cookie_file if cookie_file < len(files) else 0
  927. else:
  928. if not isinstance(cookie_file, (str, Path)):
  929. message = "'cookie_file' must be an int, str, or Path object"
  930. raise TypeError(message)
  931. cookie_file = Path(cookie_file)
  932. if cookie_file in files(): # Supplied filepath IS in Cookie.dirpath
  933. index = files.index(cookie_file)
  934. else: # Supplied filepath is NOT in Cookie.dirpath
  935. if cookie_file.is_file():
  936. Cookie.dirpath = cookie_file.parent.resolve()
  937. if cookie_file.is_dir():
  938. Cookie.dirpath = cookie_file.resolve()
  939. index = 0
  940. Cookie.current_file_index = index
  941. if content_type == "text":
  942. self.style = style
  943. self.log_and_send_query(echo, echo_prompt)
  944. if content_type == "image":
  945. self.create_image()
  946. def log_and_send_query(self, echo, echo_prompt):
  947. self.response = asyncio.run(self.send_to_bing(echo, echo_prompt))
  948. name = str(Cookie.current_filepath.name)
  949. if not self.request_count.get(name):
  950. self.request_count[name] = 1
  951. else:
  952. self.request_count[name] += 1
  953. def create_image(self):
  954. image_generator = ImageGen(Cookie.image_token)
  955. image_generator.save_images(
  956. image_generator.get_images(self.prompt),
  957. output_dir=self.image_dirpath,
  958. )
  959. async def send_to_bing(self, echo=True, echo_prompt=False):
  960. """Creat, submit, then close a Chatbot instance. Return the response"""
  961. retries = len(Cookie.files())
  962. while retries:
  963. try:
  964. bot = await Chatbot.create()
  965. if echo_prompt:
  966. print(f"> {self.prompt=}")
  967. if echo:
  968. print("> Waiting for response...")
  969. if self.style.lower() not in "creative balanced precise".split():
  970. self.style = "precise"
  971. response = await bot.ask(
  972. prompt=self.prompt,
  973. conversation_style=getattr(ConversationStyle, self.style),
  974. # wss_link="wss://sydney.bing.com/sydney/ChatHub"
  975. # What other values can this parameter take? It seems to be optional
  976. )
  977. return response
  978. except KeyError:
  979. print(
  980. f"> KeyError [{Cookie.current_filepath.name} may have exceeded the daily limit]",
  981. )
  982. Cookie.import_next()
  983. retries -= 1
  984. finally:
  985. await bot.close()
  986. @property
  987. def output(self):
  988. """The response from a completed Chatbot request"""
  989. return self.response["item"]["messages"][1]["text"]
  990. @property
  991. def sources(self):
  992. """The source names and details parsed from a completed Chatbot request"""
  993. return self.response["item"]["messages"][1]["sourceAttributions"]
  994. @property
  995. def sources_dict(self):
  996. """The source names and details as a dictionary"""
  997. sources_dict = {}
  998. name = "providerDisplayName"
  999. url = "seeMoreUrl"
  1000. for source in self.sources:
  1001. if name in source.keys() and url in source.keys():
  1002. sources_dict[source[name]] = source[url]
  1003. else:
  1004. continue
  1005. return sources_dict
  1006. @property
  1007. def code(self):
  1008. """Extract and join any snippets of Python code in the response"""
  1009. code_blocks = self.output.split("```")[1:-1:2]
  1010. code_blocks = ["\n".join(x.splitlines()[1:]) for x in code_blocks]
  1011. return "\n\n".join(code_blocks)
  1012. @property
  1013. def languages(self):
  1014. """Extract all programming languages given in code blocks"""
  1015. code_blocks = self.output.split("```")[1:-1:2]
  1016. return {x.splitlines()[0] for x in code_blocks}
  1017. @property
  1018. def suggestions(self):
  1019. """Follow-on questions suggested by the Chatbot"""
  1020. return [
  1021. x["text"]
  1022. for x in self.response["item"]["messages"][1]["suggestedResponses"]
  1023. ]
  1024. def __repr__(self):
  1025. return f"<EdgeGPT.Query: {self.prompt}>"
  1026. def __str__(self):
  1027. return self.output
  1028. class ImageQuery(Query):
  1029. def __init__(self, prompt, **kwargs):
  1030. kwargs.update({"content_type": "image"})
  1031. super().__init__(prompt, **kwargs)
  1032. def __repr__(self):
  1033. return f"<EdgeGPT.ImageQuery: {self.prompt}>"
  1034. if __name__ == "__main__":
  1035. main()