Skip to content

Commit 5def2a7

Browse files
committed
Add copilot target
1 parent da49a84 commit 5def2a7

File tree

5 files changed

+377
-1
lines changed

5 files changed

+377
-1
lines changed

aisploit/targets/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from .copilot import CopilotClient, CopilotTarget
12
from .email import EmailReceiver, EmailSender, EmailTarget, UserPasswordAuth
23
from .image import OpenAIImageTarget
34
from .langchain import LangchainTarget
45
from .stdout import StdOutTarget
56
from .target import WrapperTarget, target
67

78
__all__ = [
9+
"CopilotTarget",
10+
"CopilotClient",
811
"EmailTarget",
912
"EmailSender",
1013
"EmailReceiver",

aisploit/targets/copilot.py

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
import json
2+
import os
3+
from dataclasses import dataclass, field
4+
from typing import Any, Dict, Literal
5+
from urllib import parse
6+
7+
import websocket
8+
from requests.sessions import Session
9+
10+
from ..core import BasePromptValue, BaseTarget, Response
11+
from ..utils import cookies_as_dict
12+
13+
BUNDLE_VERSION = "1.1690.0"
14+
15+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.122 Safari/537.36"
16+
17+
CREATE_HEADERS = {
18+
"Accept": "application/json",
19+
"Accept-Encoding": "gzip, deflate, br",
20+
"Accept-Language": "en-US,en;q=0.9",
21+
"Referer": "https://copilot.microsoft.com/",
22+
"Sec-Ch-Ua": '"Chromium";v="123", "Not:A-Brand";v="8"',
23+
"Sec-Ch-Ua-Mobile": "?0",
24+
"Sec-Ch-Ua-Platform": "macOS",
25+
"Sec-Fetch-Dest": "empty",
26+
"Sec-Fetch-Mode": "cors",
27+
"Sec-Fetch-Site": "same-origin",
28+
"User-Agent": USER_AGENT,
29+
}
30+
31+
CHATHUB_HEADERS = {
32+
"Accept-Encoding": "gzip, deflate, br",
33+
"Accept-Language": "en-US,en;q=0.9",
34+
"Cache-Control": "no-cache",
35+
"Connection": "Upgrade",
36+
"Origin": "https://copilot.microsoft.com",
37+
"Pragma": "no-cache",
38+
"User-Agent": USER_AGENT,
39+
}
40+
41+
BING_CREATE_CONVERSATION_URL = "https://copilot.microsoft.com/turing/conversation/create"
42+
43+
BING_CHATHUB_URL = "wss://sydney.bing.com/sydney/ChatHub"
44+
45+
DELIMETER = "\x1e" # Record separator character.
46+
47+
_conversation_style_option_sets = {
48+
"creative": ["h3imaginative", "clgalileo", "gencontentv3"],
49+
"balanced": ["galileo"],
50+
"precise": ["h3precise", "clgalileo"],
51+
}
52+
53+
54+
@dataclass
55+
class CopilotTarget(BaseTarget):
56+
"""
57+
A class representing the target for sending prompts to Copilot.
58+
"""
59+
60+
conversation_style: Literal["balanced", "creative", "precise"] = "balanced"
61+
bing_cookies: str | None = None
62+
bundle_version: str = BUNDLE_VERSION
63+
64+
def send_prompt(self, prompt: BasePromptValue) -> Response:
65+
"""
66+
Send a prompt to Copilot and receive the response.
67+
68+
Args:
69+
prompt (BasePromptValue): The prompt value to send.
70+
71+
Returns:
72+
Response: The response received from Copilot.
73+
"""
74+
with CopilotClient(
75+
conversation_style=self.conversation_style,
76+
bing_cookies=self.bing_cookies,
77+
) as client:
78+
response = client.create_completion(prompt.to_string())
79+
return Response(content=response["text"])
80+
81+
82+
@dataclass(kw_only=True)
83+
class CopilotClient:
84+
"""
85+
A class representing the client for interacting with Copilot.
86+
"""
87+
88+
conversation_style: Literal["balanced", "creative", "precise"] = "balanced"
89+
bing_cookies: str | None = None
90+
bundle_version: str = BUNDLE_VERSION
91+
92+
session: Session | None = field(default=None, init=False)
93+
ws_connection: websocket.WebSocket | None = field(default=None, init=False)
94+
conversation_id: str | None = field(default=None, init=False)
95+
conversation_signature: str | None = field(default=None, init=False)
96+
client_id: str | None = field(default=None, init=False)
97+
invocation_id: int = field(default=0, init=False)
98+
99+
def __post_init__(self) -> None:
100+
if not self.bing_cookies:
101+
self.bing_cookies = os.getenv("BING_COOKIES")
102+
103+
def __enter__(self):
104+
self.start_conversation()
105+
return self
106+
107+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
108+
self.close_conversation()
109+
110+
def start_conversation(self) -> None:
111+
"""
112+
Start a conversation with Copilot.
113+
"""
114+
session = self._get_session(force_close=True)
115+
116+
response = session.get(f"{BING_CREATE_CONVERSATION_URL}?bundleVersion={self.bundle_version}")
117+
response.raise_for_status()
118+
119+
response_dict = response.json()
120+
if response_dict["result"]["value"] != "Success":
121+
raise ValueError(f"Failed to authenticate, received message: {response_dict['result']['message']}")
122+
123+
self.conversation_id = response_dict["conversationId"]
124+
self.client_id = response_dict["clientId"]
125+
self.conversation_signature = response.headers["X-Sydney-Conversationsignature"]
126+
self.encrypted_conversation_signature = response.headers["X-Sydney-Encryptedconversationsignature"]
127+
128+
self.invocation_id = 0
129+
130+
def close_conversation(self) -> None:
131+
"""
132+
Close the conversation with Copilot.
133+
"""
134+
if self.ws_connection:
135+
self.ws_connection.close()
136+
self.ws_connection = None
137+
138+
if self.session:
139+
self.session.close()
140+
self.session = None
141+
142+
def create_completion(
143+
self,
144+
prompt: str,
145+
search: bool = True,
146+
raw: bool = False,
147+
) -> Dict[str, Any]:
148+
"""
149+
Create a completion request and interact with Copilot.
150+
151+
Args:
152+
prompt (str): The prompt to send to Copilot.
153+
search (bool, optional): Whether to allow search. Defaults to True.
154+
raw (bool, optional): Whether to return raw response. Defaults to False.
155+
156+
Returns:
157+
Dict[str, Any]: The response received from Copilot.
158+
"""
159+
bing_chathub_url = BING_CHATHUB_URL
160+
if self.encrypted_conversation_signature:
161+
bing_chathub_url += f"?sec_access_token={parse.quote(self.encrypted_conversation_signature)}"
162+
163+
self.ws_connection = websocket.create_connection(bing_chathub_url, extra_headers=CHATHUB_HEADERS, max_size=None)
164+
165+
assert self.ws_connection is not None, "ws_connection should not be None"
166+
167+
self.ws_connection.send(as_json({"protocol": "json", "version": 1}))
168+
169+
self.ws_connection.recv()
170+
171+
request = self._build_arguments(prompt, search)
172+
173+
self.invocation_id += 1
174+
175+
self.ws_connection.send(as_json(request))
176+
177+
while True:
178+
objects = str(self.ws_connection.recv()).split(DELIMETER)
179+
for obj in objects:
180+
if not obj:
181+
continue
182+
response = json.loads(obj)
183+
184+
# Ignore type 1 messages (streaming).
185+
if response.get("type") == 1:
186+
continue
187+
# Handle type 2 messages.
188+
elif response.get("type") == 2:
189+
# Check if reached conversation limit.
190+
if response["item"].get("throttling"):
191+
number_of_messages = response["item"]["throttling"].get("numUserMessagesInConversation", 0)
192+
max_messages = response["item"]["throttling"]["maxNumUserMessagesInConversation"]
193+
if number_of_messages == max_messages:
194+
raise ValueError(f"Reached conversation limit of {max_messages} messages")
195+
196+
messages = response["item"].get("messages")
197+
if not messages:
198+
result_value = response["item"]["result"]["value"]
199+
if result_value == "Throttled":
200+
raise ValueError("Request is throttled")
201+
elif result_value == "CaptchaChallenge":
202+
raise ValueError("Solve CAPTCHA challenge to continue")
203+
else:
204+
raise ValueError(f"Unknown result value: '{result_value}'")
205+
206+
if raw:
207+
return response
208+
else:
209+
i = -1
210+
adaptiveCards = messages[-1].get("adaptiveCards")
211+
if adaptiveCards and adaptiveCards[-1]["body"][0].get("inlines"):
212+
# Adjust the index in situations where the last message
213+
# is an inline message, which often happens when an
214+
# attachment is included.
215+
i = -2
216+
return messages[i]
217+
218+
def _get_session(self, force_close: bool = False) -> Session:
219+
"""
220+
Get a requests session.
221+
222+
Args:
223+
force_close (bool, optional): Whether to force close the session. Defaults to False.
224+
225+
Returns:
226+
Session: The requests session.
227+
"""
228+
cookies = cookies_as_dict(self.bing_cookies) if self.bing_cookies else {}
229+
230+
if self.session and force_close:
231+
self.session.close()
232+
self.session = None
233+
234+
if not self.session:
235+
self.session = Session()
236+
self.session.headers.update(CREATE_HEADERS)
237+
self.session.cookies.update(cookies)
238+
239+
return self.session
240+
241+
def _build_arguments(
242+
self,
243+
prompt: str,
244+
search: bool,
245+
) -> dict:
246+
"""
247+
Build arguments for the completion request.
248+
249+
Args:
250+
prompt (str): The prompt to send.
251+
search (bool): Whether to allow search.
252+
253+
Returns:
254+
dict: The arguments for the completion request.
255+
"""
256+
options_sets = [
257+
"nlu_direct_response_filter",
258+
"deepleo",
259+
"disable_emoji_spoken_text",
260+
"responsible_ai_policy_235",
261+
"enablemm",
262+
"dv3sugg",
263+
"iyxapbing",
264+
"iycapbing",
265+
"saharagenconv5",
266+
"eredirecturl",
267+
]
268+
269+
options_sets.extend(_conversation_style_option_sets[self.conversation_style])
270+
271+
if self.bing_cookies:
272+
options_sets.extend("autosave")
273+
274+
if not search:
275+
options_sets.extend("nosearchall")
276+
277+
arguments: dict = {
278+
"arguments": [
279+
{
280+
"source": "cib",
281+
"optionsSets": options_sets,
282+
"allowedMessageTypes": [
283+
"ActionRequest",
284+
"Chat",
285+
"ConfirmationCard",
286+
"Context",
287+
"InternalSearchQuery",
288+
"InternalSearchResult",
289+
"Disengaged",
290+
"InternalLoaderMessage",
291+
"Progress",
292+
"RenderCardRequest",
293+
"RenderContentRequest",
294+
"AdsQuery",
295+
"SemanticSerp",
296+
"GenerateContentQuery",
297+
"SearchQuery",
298+
"GeneratedCode",
299+
"InternalTasksMessage",
300+
"Disclaimer",
301+
],
302+
"sliceIds": [],
303+
"verbosity": "verbose",
304+
"scenario": "SERP",
305+
"plugins": [],
306+
"conversationHistoryOptionsSets": ["autosave", "savemem", "uprofupd", "uprofgen"],
307+
"gptId": "copilot",
308+
"isStartOfSession": self.invocation_id == 0,
309+
"message": {
310+
"author": "user",
311+
"inputMethod": "Keyboard",
312+
"text": prompt,
313+
"messageType": "Chat",
314+
"imageUrl": None,
315+
"originalImageUrl": None,
316+
},
317+
"conversationSignature": self.conversation_signature,
318+
"participant": {
319+
"id": self.client_id,
320+
},
321+
"tone": self.conversation_style.title(),
322+
"spokenTextMode": "None",
323+
"conversationId": self.conversation_id,
324+
}
325+
],
326+
"invocationId": str(self.invocation_id),
327+
"target": "chat",
328+
"type": 4,
329+
}
330+
331+
return arguments
332+
333+
334+
def as_json(message: dict) -> str:
335+
"""
336+
Convert a dictionary to a JSON string and append a delimiter.
337+
338+
Args:
339+
message (dict): The dictionary to convert.
340+
341+
Returns:
342+
str: The JSON string with delimiter appended.
343+
"""
344+
return json.dumps(message) + DELIMETER

aisploit/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from .distance import cosine_distance, euclidean_distance
2+
from .http import cookies_as_dict
23
from .smtp import SMTPClient
34

45
__all__ = [
56
"cosine_distance",
67
"euclidean_distance",
8+
"cookies_as_dict",
79
"SMTPClient",
810
]

aisploit/utils/http.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def cookies_as_dict(cookies: str) -> dict:
2+
"""
3+
Convert a string of cookies into a dictionary.
4+
"""
5+
return {key_value.strip().split("=")[0]: "=".join(key_value.split("=")[1:]) for key_value in cookies.split(";")}

0 commit comments

Comments
 (0)