gemini-webapi 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gemini/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .client import GeminiClient, ChatSession # noqa: F401
2
+ from .types import Image, Candidate, ModelOutput # noqa: F401
gemini/client.py ADDED
@@ -0,0 +1,353 @@
1
+ import re
2
+ import json
3
+ import asyncio
4
+ from asyncio import Task
5
+ from typing import Any, Optional
6
+
7
+ from httpx import AsyncClient
8
+ from loguru import logger
9
+
10
+ from .consts import HEADERS
11
+ from .types import Image, Candidate, ModelOutput
12
+
13
+
14
+ def running(func) -> callable:
15
+ """
16
+ Decorator to check if client is running before making a request.
17
+ """
18
+
19
+ async def wrapper(self: "GeminiClient", *args, **kwargs):
20
+ if not self.running:
21
+ await self.init(auto_close=self.auto_close, close_delay=self.close_delay)
22
+ if self.running:
23
+ return await func(self, *args, **kwargs)
24
+
25
+ raise Exception(
26
+ f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
27
+ )
28
+ else:
29
+ return await func(self, *args, **kwargs)
30
+
31
+ return wrapper
32
+
33
+
34
+ class GeminiClient:
35
+ """
36
+ Async httpx client interface for gemini.google.com
37
+
38
+ Parameters
39
+ ----------
40
+ secure_1psid: `str`
41
+ __Secure-1PSID cookie value
42
+ secure_1psidts: `str`, optional
43
+ __Secure-1PSIDTS cookie value, some google accounts don't require this value, provide only if it's in the cookie list
44
+ proxy: `dict`, optional
45
+ Dict of proxies
46
+ """
47
+
48
+ __slots__ = [
49
+ "cookies",
50
+ "proxy",
51
+ "client",
52
+ "access_token",
53
+ "running",
54
+ "auto_close",
55
+ "close_delay",
56
+ "close_task",
57
+ ]
58
+
59
+ def __init__(
60
+ self,
61
+ secure_1psid: str,
62
+ secure_1psidts: Optional[str] = None,
63
+ proxy: Optional[dict] = None,
64
+ ):
65
+ self.cookies = {
66
+ "__Secure-1PSID": secure_1psid,
67
+ "__Secure-1PSIDTS": secure_1psidts,
68
+ }
69
+ self.proxy = proxy
70
+ self.client: AsyncClient | None = None
71
+ self.access_token: Optional[str] = None
72
+ self.running: bool = False
73
+ self.auto_close: bool = False
74
+ self.close_delay: int = 0
75
+ self.close_task: Task | None = None
76
+
77
+ async def init(
78
+ self, timeout: float = 30, auto_close: bool = False, close_delay: int = 300
79
+ ) -> None:
80
+ """
81
+ Get SNlM0e value as access token. Without this token posting will fail with 400 bad request.
82
+
83
+ Parameters
84
+ ----------
85
+ timeout: `int`, optional
86
+ Request timeout of the client in seconds. Used to limit the max waiting time when sending a request
87
+ auto_close: `bool`, optional
88
+ If `True`, the client will close connections and clear resource usage after a certain period
89
+ of inactivity. Useful for keep-alive services
90
+ close_delay: `int`, optional
91
+ Time to wait before auto-closing the client in seconds. Effective only if `auto_close` is `True`
92
+ """
93
+ try:
94
+ self.client = AsyncClient(
95
+ timeout=timeout,
96
+ proxies=self.proxy,
97
+ follow_redirects=True,
98
+ headers=HEADERS,
99
+ cookies=self.cookies,
100
+ )
101
+
102
+ response = await self.client.get("https://gemini.google.com/app")
103
+
104
+ if response.status_code != 200:
105
+ raise Exception(
106
+ f"Failed to initiate client. Request failed with status code {response.status_code}"
107
+ )
108
+ else:
109
+ match = re.search(r'"SNlM0e":"(.*?)"', response.text)
110
+ if match:
111
+ self.access_token = match.group(1)
112
+ self.running = True
113
+ logger.success("Gemini client initiated successfully.")
114
+ else:
115
+ raise Exception(
116
+ "Failed to initiate client. SNlM0e not found in response, make sure cookie values are valid."
117
+ )
118
+
119
+ self.auto_close = auto_close
120
+ self.close_delay = close_delay
121
+ if self.auto_close:
122
+ await self.reset_close_task()
123
+ except Exception:
124
+ await self.close(0)
125
+ raise
126
+
127
+ async def close(self, wait: int | None = None) -> None:
128
+ """
129
+ Close the client after a certain period of inactivity, or call manually to close immediately.
130
+
131
+ Parameters
132
+ ----------
133
+ wait: `int`, optional
134
+ Time to wait before closing the client in seconds
135
+ """
136
+ await asyncio.sleep(wait is not None and wait or self.close_delay)
137
+ await self.client.aclose()
138
+ self.running = False
139
+
140
+ async def reset_close_task(self) -> None:
141
+ """
142
+ Reset the timer for closing the client when a new request is made.
143
+ """
144
+ if self.close_task:
145
+ self.close_task.cancel()
146
+ self.close_task = None
147
+ self.close_task = asyncio.create_task(self.close())
148
+
149
+ @running
150
+ async def generate_content(
151
+ self, prompt: str, chat: Optional["ChatSession"] = None
152
+ ) -> ModelOutput:
153
+ """
154
+ Generates contents with prompt.
155
+
156
+ Parameters
157
+ ----------
158
+ prompt: `str`
159
+ Prompt provided by user
160
+ chat: `ChatSession`, optional
161
+ Chat data to retrieve conversation history. If None, will automatically generate a new chat id when sending post request
162
+
163
+ Returns
164
+ -------
165
+ :class:`ModelOutput`
166
+ Output data from gemini.google.com, use `ModelOutput.text` to get the default text reply, `ModelOutput.images` to get a list
167
+ of images in the default reply, `ModelOutput.candidates` to get a list of all answer candidates in the output
168
+ """
169
+ assert prompt, "Prompt cannot be empty."
170
+
171
+ if self.auto_close:
172
+ await self.reset_close_task()
173
+
174
+ response = await self.client.post(
175
+ "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
176
+ data={
177
+ "at": self.access_token,
178
+ "f.req": json.dumps(
179
+ [None, json.dumps([[prompt], None, chat and chat.metadata])]
180
+ ),
181
+ },
182
+ )
183
+
184
+ if response.status_code != 200:
185
+ raise Exception(
186
+ f"Failed to generate contents. Request failed with status code {response.status_code}"
187
+ )
188
+ else:
189
+ body = json.loads(json.loads(response.text.split("\n")[2])[0][2])
190
+
191
+ candidates = []
192
+ for candidate in body[4]:
193
+ images = (
194
+ candidate[4]
195
+ and [
196
+ Image(url=image[0][0][0], title=image[2], alt=image[0][4])
197
+ for image in candidate[4]
198
+ ]
199
+ or []
200
+ )
201
+ candidates.append(
202
+ Candidate(rcid=candidate[0], text=candidate[1][0], images=images)
203
+ )
204
+ if not candidates:
205
+ raise Exception(
206
+ "Failed to generate contents. No output data found in response."
207
+ )
208
+
209
+ output = ModelOutput(metadata=body[1], candidates=candidates)
210
+
211
+ if isinstance(chat, ChatSession):
212
+ chat.last_output = output
213
+
214
+ return output
215
+
216
+ def start_chat(self, **kwargs) -> "ChatSession":
217
+ """
218
+ Returns a `ChatSession` object attached to this model.
219
+
220
+ Returns
221
+ -------
222
+ :class:`ChatSession`
223
+ Empty chat object for retrieving conversation history
224
+ """
225
+ return ChatSession(geminiclient=self, **kwargs)
226
+
227
+
228
+ class ChatSession:
229
+ """
230
+ Chat data to retrieve conversation history. Only if all 3 ids are provided will the conversation history be retrieved.
231
+
232
+ Parameters
233
+ ----------
234
+ geminiclient: `GeminiClient`
235
+ Async httpx client interface for gemini.google.com
236
+ metadata: `list[str]`, optional
237
+ List of chat metadata `[cid, rid, rcid]`, can be shorter than 3 elements, like `[cid, rid]` or `[cid]` only
238
+ cid: `str`, optional
239
+ Chat id, if provided together with metadata, will override the first value in it
240
+ rid: `str`, optional
241
+ Reply id, if provided together with metadata, will override the second value in it
242
+ rcid: `str`, optional
243
+ Reply candidate id, if provided together with metadata, will override the third value in it
244
+ """
245
+
246
+ # @properties needn't have their slots pre-defined
247
+ __slots__ = ["__metadata", "geminiclient", "last_output"]
248
+
249
+ def __init__(
250
+ self,
251
+ geminiclient: GeminiClient,
252
+ metadata: Optional[list[str]] = None,
253
+ cid: Optional[str] = None, # chat id
254
+ rid: Optional[str] = None, # reply id
255
+ rcid: Optional[str] = None, # reply candidate id
256
+ ):
257
+ self.__metadata: list[Optional[str]] = [None, None, None]
258
+ self.geminiclient: GeminiClient = geminiclient
259
+ self.last_output: Optional[ModelOutput] = None
260
+
261
+ if metadata:
262
+ self.metadata = metadata
263
+ if cid:
264
+ self.cid = cid
265
+ if rid:
266
+ self.rid = rid
267
+ if rcid:
268
+ self.rcid = rcid
269
+
270
+ def __str__(self):
271
+ return f"ChatSession(cid='{self.cid}', rid='{self.rid}', rcid='{self.rcid}')"
272
+
273
+ __repr__ = __str__
274
+
275
+ def __setattr__(self, name: str, value: Any) -> None:
276
+ super().__setattr__(name, value)
277
+ # update conversation history when last output is updated
278
+ if name == "last_output" and isinstance(value, ModelOutput):
279
+ self.metadata = value.metadata
280
+ self.rcid = value.rcid
281
+
282
+ async def send_message(self, prompt: str) -> ModelOutput:
283
+ """
284
+ Generates contents with prompt.
285
+ Use as a shortcut for `GeminiClient.generate_content(prompt, self)`.
286
+
287
+ Parameters
288
+ ----------
289
+ prompt: `str`
290
+ Prompt provided by user
291
+
292
+ Returns
293
+ -------
294
+ :class:`ModelOutput`
295
+ Output data from gemini.google.com, use `ModelOutput.text` to get the default text reply, `ModelOutput.images` to get a list
296
+ of images in the default reply, `ModelOutput.candidates` to get a list of all answer candidates in the output
297
+ """
298
+ return await self.geminiclient.generate_content(prompt, self)
299
+
300
+ def choose_candidate(self, index: int) -> ModelOutput:
301
+ """
302
+ Choose a candidate from the last `ModelOutput` to control the ongoing conversation flow.
303
+
304
+ Parameters
305
+ ----------
306
+ index: `int`
307
+ Index of the candidate to choose, starting from 0
308
+ """
309
+ if not self.last_output:
310
+ raise Exception("No previous output data found in this chat session.")
311
+
312
+ if index >= len(self.last_output.candidates):
313
+ raise ValueError(
314
+ f"Index {index} exceeds the number of candidates in last model output."
315
+ )
316
+
317
+ self.last_output.chosen = index
318
+ self.rcid = self.last_output.rcid
319
+ return self.last_output
320
+
321
+ @property
322
+ def metadata(self):
323
+ return self.__metadata
324
+
325
+ @metadata.setter
326
+ def metadata(self, value: list[str]):
327
+ if len(value) > 3:
328
+ raise ValueError("metadata cannot exceed 3 elements")
329
+ self.__metadata[: len(value)] = value
330
+
331
+ @property
332
+ def cid(self):
333
+ return self.__metadata[0]
334
+
335
+ @cid.setter
336
+ def cid(self, value: str):
337
+ self.__metadata[0] = value
338
+
339
+ @property
340
+ def rid(self):
341
+ return self.__metadata[1]
342
+
343
+ @rid.setter
344
+ def rid(self, value: str):
345
+ self.__metadata[1] = value
346
+
347
+ @property
348
+ def rcid(self):
349
+ return self.__metadata[2]
350
+
351
+ @rcid.setter
352
+ def rcid(self, value: str):
353
+ self.__metadata[2] = value
gemini/consts.py ADDED
@@ -0,0 +1,8 @@
1
+ HEADERS = {
2
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
3
+ "Host": "gemini.google.com",
4
+ "Origin": "https://gemini.google.com",
5
+ "Referer": "https://gemini.google.com/",
6
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
7
+ "X-Same-Domain": "1",
8
+ }
gemini/types.py ADDED
@@ -0,0 +1,64 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class Image(BaseModel):
7
+ url: str
8
+ title: Optional[str] = "[Image]"
9
+ alt: Optional[str] = ""
10
+
11
+ def __str__(self):
12
+ return f"{self.title}({self.url}) - {self.alt}"
13
+
14
+ def __repr__(self):
15
+ return f"Image(title='{self.title}', url='{len(self.url)<=20 and self.url or self.url[:8] + '...' + self.url[-12:]}', alt='{self.alt}')"
16
+
17
+
18
+ class Candidate(BaseModel):
19
+ rcid: str
20
+ text: str
21
+ images: list[Image]
22
+
23
+ def __str__(self):
24
+ return self.text
25
+
26
+ def __repr__(self):
27
+ return f"Candidate(rcid='{self.rcid}', text='{len(self.text)<=20 and self.text or self.text[:20] + '...'}', images={self.images})"
28
+
29
+
30
+ class ModelOutput(BaseModel):
31
+ """
32
+ Classified output from gemini.google.com
33
+
34
+ Parameters
35
+ ----------
36
+ metadata: `list[str]`
37
+ List of chat metadata `[cid, rid, rcid]`, can be shorter than 3 elements, like `[cid, rid]` or `[cid]` only
38
+ candidates: `list[Candidate]`
39
+ List of all candidates returned from gemini
40
+ chosen: `int`, optional
41
+ Index of the chosen candidate, by default will choose the first one
42
+ """
43
+
44
+ metadata: list[str]
45
+ candidates: list[Candidate]
46
+ chosen: int = 0
47
+
48
+ def __str__(self):
49
+ return self.text
50
+
51
+ def __repr__(self):
52
+ return f"ModelOutput(metadata={self.metadata}, chosen={self.chosen}, candidates={self.candidates})"
53
+
54
+ @property
55
+ def text(self):
56
+ return self.candidates[self.chosen].text
57
+
58
+ @property
59
+ def images(self):
60
+ return self.candidates[self.chosen].images
61
+
62
+ @property
63
+ def rcid(self):
64
+ return self.candidates[self.chosen].rcid
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 GM Development Department
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.1
2
+ Name: gemini-webapi
3
+ Version: 0.0.1
4
+ Summary: A reverse-engineered async wrapper for Google Gemini web client
5
+ Author: UZQueen
6
+ License: MIT License
7
+ Project-URL: Repository, https://github.com/HanaokaYuzu/Gemini-API
8
+ Project-URL: Issues, https://github.com/HanaokaYuzu/Gemini-API/issues
9
+ Keywords: API,async,Gemini,Bard,Google,Generative AI,LLM
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.7
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: httpx >=0.25.2
20
+ Requires-Dist: pydantic >=2.5.3
21
+ Requires-Dist: loguru >=0.7.2
22
+
23
+ # <img src="https://www.gstatic.com/lamda/images/favicon_v1_150160cddff7f294ce30.svg" width="35px" alt="Gemini Icon" /> Gemini-API
24
+
25
+ Reverse-engineered asynchronous python wrapper for Google Gemini (formerly Bard) web client, providing a simple and elegant interface inspired by Google GenerativeAI's official API.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install gemini-webapi
31
+ ```
32
+
33
+ ## Authentication
34
+
35
+ - Go to <https://gemini.google.com/> and login with your Google account
36
+ - Press F12 for web inspector, go to `Network` tab and refresh the page
37
+ - Click any request and copy cookie values of `__Secure-1PSID` and `__Secure-1PSIDTS`
38
+
39
+ ## Usage
40
+
41
+ ### Initialization
42
+
43
+ ```python
44
+ from gemini import GeminiClient
45
+
46
+ # Replace "COOKIE VALUE HERE" with your actual cookie values as strings
47
+ Secure_1PSID = "COOKIE VALUE HERE"
48
+ Secure_1PSIDTS = "COOKIE VALUE HERE"
49
+
50
+ client = GeminiClient(Secure_1PSID, Secure_1PSIDTS, proxy=None)
51
+ await client.init()
52
+ ```
53
+
54
+ ### Generate contents from text inputs
55
+
56
+ ```python
57
+ response = await client.generate_content("Hello World!")
58
+ print(response.text) # Note: simply use print(response) to get the same output if you just want to see the response text
59
+ ```
60
+
61
+ ### Retrieve images in response
62
+
63
+ ```python
64
+ response = await client.generate_content("Send me some pictures of cats")
65
+ images = response.images
66
+ for image in images:
67
+ print(f"{image.title}({image.url}) - {image.alt}", sep="\n")
68
+ ```
69
+
70
+ ### Conversations across multiple turns
71
+
72
+ ```python
73
+ chat = client.start_chat() # A chat stores the metadata to keep a conversation continuous. It will automatically get updated after each turn
74
+ response1 = await chat.send_message("Briefly introduce Europe")
75
+ response2 = await chat.send_message("What's the population there?")
76
+ print(response1.text, response2.text, sep="\n----------------------------------\n")
77
+ ```
78
+
79
+ ### Check and switch to other answer candidates
80
+
81
+ ```python
82
+ chat = client.start_chat()
83
+ response = await chat.send_message("What's the best Japanese dish in your mind? Choose one only.")
84
+ for candidate in response.candidates:
85
+ print(candidate, "\n----------------------------------\n")
86
+
87
+ # Control the ongoing conversation flow by choosing candidate manually
88
+ new_candidate = chat.choose_candidate(index=1) # Choose the second candidate here
89
+ followup_response = await chat.send_message("Tell me more about it.") # Will generate contents based on the chosen candidate
90
+ print(new_candidate, followup_response, sep="\n----------------------------------\n")
91
+ ```
@@ -0,0 +1,9 @@
1
+ gemini/__init__.py,sha256=0y3v6LIcyYaVHeOF0ncZsd-f1blH4SDGAvvOD5nt6hY,123
2
+ gemini/client.py,sha256=6-Nd3iEaqxknSM9KOK2bi-AdH3vVtzsqipvNUL5cmWQ,11641
3
+ gemini/consts.py,sha256=fM-qSEWK9wDBy_kTyl7_KZtxSVyF48KZucsujs26og8,365
4
+ gemini/types.py,sha256=M4gbw1RAlyss38tDrdYiyOIOrf0CA-5M33IYJzRxPPU,1680
5
+ gemini_webapi-0.0.1.dist-info/LICENSE,sha256=syoVjv1ye4WBYEgJOlRkRE8trYjBMFnqz2kP7ta9934,1082
6
+ gemini_webapi-0.0.1.dist-info/METADATA,sha256=_zE3rMMubULAE1lqew_tXH-dxVmQlfKF9cY-gIwjK8o,3292
7
+ gemini_webapi-0.0.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
8
+ gemini_webapi-0.0.1.dist-info/top_level.txt,sha256=HC3nun2yFuNkNiRuKIq3MP_Izk5CqGNHxn2nl0-ozn0,7
9
+ gemini_webapi-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ gemini