Copilot.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. from __future__ import annotations
  2. import json
  3. import asyncio
  4. from http.cookiejar import CookieJar
  5. from urllib.parse import quote
  6. try:
  7. from curl_cffi.requests import Session, CurlWsFlag
  8. has_curl_cffi = True
  9. except ImportError:
  10. has_curl_cffi = False
  11. try:
  12. import nodriver
  13. has_nodriver = True
  14. except ImportError:
  15. has_nodriver = False
  16. from .base_provider import AbstractProvider, BaseConversation
  17. from .helper import format_prompt
  18. from ..typing import CreateResult, Messages, ImageType
  19. from ..errors import MissingRequirementsError
  20. from ..requests.raise_for_status import raise_for_status
  21. from ..providers.helper import format_cookies
  22. from ..requests import get_nodriver
  23. from ..image import ImageResponse, to_bytes, is_accepted_format
  24. from .. import debug
  25. class Conversation(BaseConversation):
  26. conversation_id: str
  27. cookie_jar: CookieJar
  28. access_token: str
  29. def __init__(self, conversation_id: str, cookie_jar: CookieJar, access_token: str = None):
  30. self.conversation_id = conversation_id
  31. self.cookie_jar = cookie_jar
  32. self.access_token = access_token
  33. class Copilot(AbstractProvider):
  34. label = "Microsoft Copilot"
  35. url = "https://copilot.microsoft.com"
  36. working = True
  37. supports_stream = True
  38. default_model = "Copilot"
  39. websocket_url = "wss://copilot.microsoft.com/c/api/chat?api-version=2"
  40. conversation_url = f"{url}/c/api/conversations"
  41. @classmethod
  42. def create_completion(
  43. cls,
  44. model: str,
  45. messages: Messages,
  46. stream: bool = False,
  47. proxy: str = None,
  48. timeout: int = 900,
  49. image: ImageType = None,
  50. conversation: Conversation = None,
  51. return_conversation: bool = False,
  52. web_search: bool = True,
  53. **kwargs
  54. ) -> CreateResult:
  55. if not has_curl_cffi:
  56. raise MissingRequirementsError('Install or update "curl_cffi" package | pip install -U curl_cffi')
  57. websocket_url = cls.websocket_url
  58. access_token = None
  59. headers = None
  60. cookies = conversation.cookie_jar if conversation is not None else None
  61. if cls.needs_auth or image is not None:
  62. if conversation is None or conversation.access_token is None:
  63. access_token, cookies = asyncio.run(cls.get_access_token_and_cookies(proxy))
  64. else:
  65. access_token = conversation.access_token
  66. debug.log(f"Copilot: Access token: {access_token[:7]}...{access_token[-5:]}")
  67. debug.log(f"Copilot: Cookies: {';'.join([*cookies])}")
  68. websocket_url = f"{websocket_url}&accessToken={quote(access_token)}"
  69. headers = {"authorization": f"Bearer {access_token}", "cookie": format_cookies(cookies)}
  70. with Session(
  71. timeout=timeout,
  72. proxy=proxy,
  73. impersonate="chrome",
  74. headers=headers,
  75. cookies=cookies,
  76. ) as session:
  77. response = session.get("https://copilot.microsoft.com/c/api/user")
  78. raise_for_status(response)
  79. debug.log(f"Copilot: User: {response.json().get('firstName', 'null')}")
  80. if conversation is None:
  81. response = session.post(cls.conversation_url)
  82. raise_for_status(response)
  83. conversation_id = response.json().get("id")
  84. if return_conversation:
  85. yield Conversation(conversation_id, session.cookies.jar, access_token)
  86. prompt = format_prompt(messages)
  87. debug.log(f"Copilot: Created conversation: {conversation_id}")
  88. else:
  89. conversation_id = conversation.conversation_id
  90. prompt = messages[-1]["content"]
  91. debug.log(f"Copilot: Use conversation: {conversation_id}")
  92. images = []
  93. if image is not None:
  94. data = to_bytes(image)
  95. response = session.post(
  96. "https://copilot.microsoft.com/c/api/attachments",
  97. headers={"content-type": is_accepted_format(data)},
  98. data=data
  99. )
  100. raise_for_status(response)
  101. images.append({"type":"image", "url": response.json().get("url")})
  102. wss = session.ws_connect(cls.websocket_url)
  103. wss.send(json.dumps({
  104. "event": "send",
  105. "conversationId": conversation_id,
  106. "content": [*images, {
  107. "type": "text",
  108. "text": prompt,
  109. }],
  110. "mode": "chat"
  111. }).encode(), CurlWsFlag.TEXT)
  112. is_started = False
  113. msg = None
  114. image_prompt: str = None
  115. last_msg = None
  116. while True:
  117. try:
  118. msg = wss.recv()[0]
  119. msg = json.loads(msg)
  120. except:
  121. break
  122. last_msg = msg
  123. if msg.get("event") == "appendText":
  124. is_started = True
  125. yield msg.get("text")
  126. elif msg.get("event") == "generatingImage":
  127. image_prompt = msg.get("prompt")
  128. elif msg.get("event") == "imageGenerated":
  129. yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")})
  130. elif msg.get("event") == "done":
  131. break
  132. elif msg.get("event") == "error":
  133. raise RuntimeError(f"Error: {msg}")
  134. elif msg.get("event") not in ["received", "startMessage", "citation", "partCompleted"]:
  135. debug.log(f"Copilot Message: {msg}")
  136. if not is_started:
  137. raise RuntimeError(f"Invalid response: {last_msg}")
  138. @classmethod
  139. async def get_access_token_and_cookies(cls, proxy: str = None):
  140. browser = await get_nodriver(proxy=proxy)
  141. page = await browser.get(cls.url)
  142. access_token = None
  143. while access_token is None:
  144. access_token = await page.evaluate("""
  145. (() => {
  146. for (var i = 0; i < localStorage.length; i++) {
  147. try {
  148. item = JSON.parse(localStorage.getItem(localStorage.key(i)));
  149. if (item.credentialType == "AccessToken") {
  150. return item.secret;
  151. }
  152. } catch(e) {}
  153. }
  154. })()
  155. """)
  156. if access_token is None:
  157. await asyncio.sleep(1)
  158. cookies = {}
  159. for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])):
  160. cookies[c.name] = c.value
  161. await page.close()
  162. return access_token, cookies