Copilot.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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. websocket_url = f"{websocket_url}&accessToken={quote(access_token)}"
  68. headers = {"authorization": f"Bearer {access_token}"}
  69. with Session(
  70. timeout=timeout,
  71. proxy=proxy,
  72. impersonate="chrome",
  73. headers=headers,
  74. cookies=cookies,
  75. ) as session:
  76. response = session.get("https://copilot.microsoft.com/c/api/user")
  77. raise_for_status(response)
  78. debug.log(f"Copilot: User: {response.json().get('firstName', 'null')}")
  79. if conversation is None:
  80. response = session.post(cls.conversation_url)
  81. raise_for_status(response)
  82. conversation_id = response.json().get("id")
  83. if return_conversation:
  84. yield Conversation(conversation_id, session.cookies.jar, access_token)
  85. prompt = format_prompt(messages)
  86. debug.log(f"Copilot: Created conversation: {conversation_id}")
  87. else:
  88. conversation_id = conversation.conversation_id
  89. prompt = messages[-1]["content"]
  90. debug.log(f"Copilot: Use conversation: {conversation_id}")
  91. images = []
  92. if image is not None:
  93. data = to_bytes(image)
  94. response = session.post(
  95. "https://copilot.microsoft.com/c/api/attachments",
  96. headers={"content-type": is_accepted_format(data)},
  97. data=data
  98. )
  99. raise_for_status(response)
  100. images.append({"type":"image", "url": response.json().get("url")})
  101. wss = session.ws_connect(cls.websocket_url)
  102. wss.send(json.dumps({
  103. "event": "send",
  104. "conversationId": conversation_id,
  105. "content": [*images, {
  106. "type": "text",
  107. "text": prompt,
  108. }],
  109. "mode": "chat"
  110. }).encode(), CurlWsFlag.TEXT)
  111. is_started = False
  112. msg = None
  113. image_prompt: str = None
  114. last_msg = None
  115. while True:
  116. try:
  117. msg = wss.recv()[0]
  118. msg = json.loads(msg)
  119. except:
  120. break
  121. last_msg = msg
  122. if msg.get("event") == "appendText":
  123. is_started = True
  124. yield msg.get("text")
  125. elif msg.get("event") == "generatingImage":
  126. image_prompt = msg.get("prompt")
  127. elif msg.get("event") == "imageGenerated":
  128. yield ImageResponse(msg.get("url"), image_prompt, {"preview": msg.get("thumbnailUrl")})
  129. elif msg.get("event") == "done":
  130. break
  131. elif msg.get("event") == "error":
  132. raise RuntimeError(f"Error: {msg}")
  133. elif msg.get("event") not in ["received", "startMessage", "citation", "partCompleted"]:
  134. debug.log(f"Copilot Message: {msg}")
  135. if not is_started:
  136. raise RuntimeError(f"Invalid response: {last_msg}")
  137. @classmethod
  138. async def get_access_token_and_cookies(cls, proxy: str = None):
  139. browser = await get_nodriver(proxy=proxy)
  140. page = await browser.get(cls.url)
  141. access_token = None
  142. while access_token is None:
  143. access_token = await page.evaluate("""
  144. (() => {
  145. for (var i = 0; i < localStorage.length; i++) {
  146. try {
  147. item = JSON.parse(localStorage.getItem(localStorage.key(i)));
  148. if (item.credentialType == "AccessToken") {
  149. return item.secret;
  150. }
  151. } catch(e) {}
  152. }
  153. })()
  154. """)
  155. if access_token is None:
  156. await asyncio.sleep(1)
  157. cookies = {}
  158. for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])):
  159. cookies[c.name] = c.value
  160. await page.close()
  161. return access_token, cookies