gemini-webapi 1.14.3__py3-none-any.whl → 1.15.0__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_webapi/client.py +28 -180
- gemini_webapi/components/__init__.py +3 -0
- gemini_webapi/components/gem_mixin.py +288 -0
- gemini_webapi/constants.py +3 -0
- gemini_webapi/types/image.py +4 -4
- gemini_webapi/utils/__init__.py +1 -0
- gemini_webapi/utils/decorators.py +52 -0
- {gemini_webapi-1.14.3.dist-info → gemini_webapi-1.15.0.dist-info}/METADATA +75 -2
- {gemini_webapi-1.14.3.dist-info → gemini_webapi-1.15.0.dist-info}/RECORD +12 -9
- {gemini_webapi-1.14.3.dist-info → gemini_webapi-1.15.0.dist-info}/WHEEL +0 -0
- {gemini_webapi-1.14.3.dist-info → gemini_webapi-1.15.0.dist-info}/licenses/LICENSE +0 -0
- {gemini_webapi-1.14.3.dist-info → gemini_webapi-1.15.0.dist-info}/top_level.txt +0 -0
gemini_webapi/client.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import functools
|
|
3
|
-
import itertools
|
|
4
2
|
import re
|
|
5
3
|
from asyncio import Task
|
|
6
4
|
from pathlib import Path
|
|
@@ -9,7 +7,8 @@ from typing import Any, Optional
|
|
|
9
7
|
import orjson as json
|
|
10
8
|
from httpx import AsyncClient, ReadTimeout, Response
|
|
11
9
|
|
|
12
|
-
from .
|
|
10
|
+
from .components import GemMixin
|
|
11
|
+
from .constants import Endpoint, ErrorCode, Headers, Model
|
|
13
12
|
from .exceptions import (
|
|
14
13
|
AuthError,
|
|
15
14
|
APIError,
|
|
@@ -26,7 +25,6 @@ from .types import (
|
|
|
26
25
|
Candidate,
|
|
27
26
|
ModelOutput,
|
|
28
27
|
Gem,
|
|
29
|
-
GemJar,
|
|
30
28
|
RPCData,
|
|
31
29
|
)
|
|
32
30
|
from .utils import (
|
|
@@ -35,60 +33,13 @@ from .utils import (
|
|
|
35
33
|
rotate_1psidts,
|
|
36
34
|
get_access_token,
|
|
37
35
|
load_browser_cookies,
|
|
36
|
+
running,
|
|
38
37
|
rotate_tasks,
|
|
39
38
|
logger,
|
|
40
39
|
)
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
"""
|
|
45
|
-
Decorator to check if client is running before making a request.
|
|
46
|
-
|
|
47
|
-
Parameters
|
|
48
|
-
----------
|
|
49
|
-
retry: `int`, optional
|
|
50
|
-
Max number of retries when `gemini_webapi.APIError` is raised.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def decorator(func):
|
|
54
|
-
@functools.wraps(func)
|
|
55
|
-
async def wrapper(client: "GeminiClient", *args, retry=retry, **kwargs):
|
|
56
|
-
try:
|
|
57
|
-
if not client.running:
|
|
58
|
-
await client.init(
|
|
59
|
-
timeout=client.timeout,
|
|
60
|
-
auto_close=client.auto_close,
|
|
61
|
-
close_delay=client.close_delay,
|
|
62
|
-
auto_refresh=client.auto_refresh,
|
|
63
|
-
refresh_interval=client.refresh_interval,
|
|
64
|
-
verbose=False,
|
|
65
|
-
)
|
|
66
|
-
if client.running:
|
|
67
|
-
return await func(client, *args, **kwargs)
|
|
68
|
-
|
|
69
|
-
# Should not reach here
|
|
70
|
-
raise APIError(
|
|
71
|
-
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
72
|
-
)
|
|
73
|
-
else:
|
|
74
|
-
return await func(client, *args, **kwargs)
|
|
75
|
-
except APIError as e:
|
|
76
|
-
# Image generation takes too long, only retry once
|
|
77
|
-
if isinstance(e, ImageGenerationError):
|
|
78
|
-
retry = min(1, retry)
|
|
79
|
-
|
|
80
|
-
if retry > 0:
|
|
81
|
-
await asyncio.sleep(1)
|
|
82
|
-
return await wrapper(client, *args, retry=retry - 1, **kwargs)
|
|
83
|
-
|
|
84
|
-
raise
|
|
85
|
-
|
|
86
|
-
return wrapper
|
|
87
|
-
|
|
88
|
-
return decorator
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class GeminiClient:
|
|
42
|
+
class GeminiClient(GemMixin):
|
|
92
43
|
"""
|
|
93
44
|
Async httpx client interface for gemini.google.com.
|
|
94
45
|
|
|
@@ -125,7 +76,7 @@ class GeminiClient:
|
|
|
125
76
|
"close_task",
|
|
126
77
|
"auto_refresh",
|
|
127
78
|
"refresh_interval",
|
|
128
|
-
"_gems",
|
|
79
|
+
"_gems", # From GemMixin
|
|
129
80
|
"kwargs",
|
|
130
81
|
]
|
|
131
82
|
|
|
@@ -136,6 +87,7 @@ class GeminiClient:
|
|
|
136
87
|
proxy: str | None = None,
|
|
137
88
|
**kwargs,
|
|
138
89
|
):
|
|
90
|
+
super().__init__()
|
|
139
91
|
self.cookies = {}
|
|
140
92
|
self.proxy = proxy
|
|
141
93
|
self.running: bool = False
|
|
@@ -147,7 +99,6 @@ class GeminiClient:
|
|
|
147
99
|
self.close_task: Task | None = None
|
|
148
100
|
self.auto_refresh: bool = True
|
|
149
101
|
self.refresh_interval: float = 540
|
|
150
|
-
self._gems: GemJar | None = None
|
|
151
102
|
self.kwargs = kwargs
|
|
152
103
|
|
|
153
104
|
# Validate cookies
|
|
@@ -285,119 +236,6 @@ class GeminiClient:
|
|
|
285
236
|
self.cookies["__Secure-1PSIDTS"] = new_1psidts
|
|
286
237
|
await asyncio.sleep(self.refresh_interval)
|
|
287
238
|
|
|
288
|
-
@property
|
|
289
|
-
def gems(self) -> GemJar:
|
|
290
|
-
"""
|
|
291
|
-
Returns a `GemJar` object containing cached gems.
|
|
292
|
-
Only available after calling `GeminiClient.fetch_gems()`.
|
|
293
|
-
|
|
294
|
-
Returns
|
|
295
|
-
-------
|
|
296
|
-
:class:`GemJar`
|
|
297
|
-
Refer to `gemini_webapi.types.GemJar`.
|
|
298
|
-
|
|
299
|
-
Raises
|
|
300
|
-
------
|
|
301
|
-
`RuntimeError`
|
|
302
|
-
If `GeminiClient.fetch_gems()` has not been called before accessing this property.
|
|
303
|
-
"""
|
|
304
|
-
|
|
305
|
-
if self._gems is None:
|
|
306
|
-
raise RuntimeError(
|
|
307
|
-
"Gems not fetched yet. Call `GeminiClient.fetch_gems()` method to fetch gems from gemini.google.com."
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
return self._gems
|
|
311
|
-
|
|
312
|
-
async def fetch_gems(self, include_hidden: bool = False, **kwargs) -> GemJar:
|
|
313
|
-
"""
|
|
314
|
-
Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
|
|
315
|
-
|
|
316
|
-
Note that network request will be sent every time this method is called.
|
|
317
|
-
Once the gems are fetched, they will be cached and accessible via `GeminiClient.gems` property.
|
|
318
|
-
|
|
319
|
-
Parameters
|
|
320
|
-
----------
|
|
321
|
-
include_hidden: `bool`, optional
|
|
322
|
-
There are some predefined gems that by default are not shown to users (and therefore may not work properly).
|
|
323
|
-
Set this parameter to `True` to include them in the fetched gem list.
|
|
324
|
-
|
|
325
|
-
Returns
|
|
326
|
-
-------
|
|
327
|
-
:class:`GemJar`
|
|
328
|
-
Refer to `gemini_webapi.types.GemJar`.
|
|
329
|
-
"""
|
|
330
|
-
|
|
331
|
-
response = await self._batch_execute(
|
|
332
|
-
[
|
|
333
|
-
RPCData(
|
|
334
|
-
rpcid=GRPC.LIST_GEMS,
|
|
335
|
-
payload="[4]" if include_hidden else "[3]",
|
|
336
|
-
identifier="system",
|
|
337
|
-
),
|
|
338
|
-
RPCData(
|
|
339
|
-
rpcid=GRPC.LIST_GEMS,
|
|
340
|
-
payload="[2]",
|
|
341
|
-
identifier="custom",
|
|
342
|
-
),
|
|
343
|
-
],
|
|
344
|
-
**kwargs,
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
try:
|
|
348
|
-
response_json = json.loads(response.text.split("\n")[2])
|
|
349
|
-
|
|
350
|
-
predefined_gems, custom_gems = [], []
|
|
351
|
-
|
|
352
|
-
for part in response_json:
|
|
353
|
-
if part[-1] == "system":
|
|
354
|
-
predefined_gems = json.loads(part[2])[2]
|
|
355
|
-
elif part[-1] == "custom":
|
|
356
|
-
if custom_gems_container := json.loads(part[2]):
|
|
357
|
-
custom_gems = custom_gems_container[2]
|
|
358
|
-
|
|
359
|
-
if not predefined_gems and not custom_gems:
|
|
360
|
-
raise Exception
|
|
361
|
-
except Exception:
|
|
362
|
-
await self.close()
|
|
363
|
-
logger.debug(f"Invalid response: {response.text}")
|
|
364
|
-
raise APIError(
|
|
365
|
-
"Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request."
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
self._gems = GemJar(
|
|
369
|
-
itertools.chain(
|
|
370
|
-
(
|
|
371
|
-
(
|
|
372
|
-
gem[0],
|
|
373
|
-
Gem(
|
|
374
|
-
id=gem[0],
|
|
375
|
-
name=gem[1][0],
|
|
376
|
-
description=gem[1][1],
|
|
377
|
-
prompt=gem[2] and gem[2][0] or None,
|
|
378
|
-
predefined=True,
|
|
379
|
-
),
|
|
380
|
-
)
|
|
381
|
-
for gem in predefined_gems
|
|
382
|
-
),
|
|
383
|
-
(
|
|
384
|
-
(
|
|
385
|
-
gem[0],
|
|
386
|
-
Gem(
|
|
387
|
-
id=gem[0],
|
|
388
|
-
name=gem[1][0],
|
|
389
|
-
description=gem[1][1],
|
|
390
|
-
prompt=gem[2] and gem[2][0] or None,
|
|
391
|
-
predefined=False,
|
|
392
|
-
),
|
|
393
|
-
)
|
|
394
|
-
for gem in custom_gems
|
|
395
|
-
),
|
|
396
|
-
)
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
return self._gems
|
|
400
|
-
|
|
401
239
|
@running(retry=2)
|
|
402
240
|
async def generate_content(
|
|
403
241
|
self,
|
|
@@ -454,7 +292,9 @@ class GeminiClient:
|
|
|
454
292
|
model = Model.from_name(model)
|
|
455
293
|
|
|
456
294
|
if isinstance(gem, Gem):
|
|
457
|
-
|
|
295
|
+
gem_id = gem.id
|
|
296
|
+
else:
|
|
297
|
+
gem_id = gem
|
|
458
298
|
|
|
459
299
|
if self.auto_close:
|
|
460
300
|
await self.reset_close_task()
|
|
@@ -487,7 +327,7 @@ class GeminiClient:
|
|
|
487
327
|
None,
|
|
488
328
|
chat and chat.metadata,
|
|
489
329
|
]
|
|
490
|
-
+ (
|
|
330
|
+
+ (gem_id and [None] * 16 + [gem_id] or [])
|
|
491
331
|
).decode(),
|
|
492
332
|
]
|
|
493
333
|
).decode(),
|
|
@@ -611,10 +451,21 @@ class GeminiClient:
|
|
|
611
451
|
generated_images = [
|
|
612
452
|
GeneratedImage(
|
|
613
453
|
url=generated_image[0][3][3],
|
|
614
|
-
title=
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
454
|
+
title=(
|
|
455
|
+
f"[Generated Image {generated_image[3][6]}]"
|
|
456
|
+
if generated_image[3][6]
|
|
457
|
+
else "[Generated Image]"
|
|
458
|
+
),
|
|
459
|
+
alt=(
|
|
460
|
+
generated_image[3][5][image_index]
|
|
461
|
+
if generated_image[3][5]
|
|
462
|
+
and len(generated_image[3][5]) > image_index
|
|
463
|
+
else (
|
|
464
|
+
generated_image[3][5][0]
|
|
465
|
+
if generated_image[3][5]
|
|
466
|
+
else ""
|
|
467
|
+
)
|
|
468
|
+
),
|
|
618
469
|
proxy=self.proxy,
|
|
619
470
|
cookies=self.cookies,
|
|
620
471
|
)
|
|
@@ -667,7 +518,6 @@ class GeminiClient:
|
|
|
667
518
|
|
|
668
519
|
return ChatSession(geminiclient=self, **kwargs)
|
|
669
520
|
|
|
670
|
-
@running(retry=2)
|
|
671
521
|
async def _batch_execute(self, payloads: list[RPCData], **kwargs) -> Response:
|
|
672
522
|
"""
|
|
673
523
|
Execute a batch of requests to Gemini API.
|
|
@@ -703,12 +553,10 @@ class GeminiClient:
|
|
|
703
553
|
"consider setting a higher `timeout` value when initializing GeminiClient."
|
|
704
554
|
)
|
|
705
555
|
|
|
556
|
+
# ? Seems like batch execution will immediately invalidate the current access token,
|
|
557
|
+
# ? causing the next request to fail with 401 Unauthorized.
|
|
706
558
|
if response.status_code != 200:
|
|
707
|
-
|
|
708
|
-
f"Batch execution failed with status code {response.status_code}. "
|
|
709
|
-
f"RPC: {', '.join({payload.rpcid.name for payload in payloads})}; "
|
|
710
|
-
f"Invalid response: {response.text}"
|
|
711
|
-
)
|
|
559
|
+
await self.close()
|
|
712
560
|
raise APIError(
|
|
713
561
|
f"Batch execution failed with status code {response.status_code}"
|
|
714
562
|
)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import orjson as json
|
|
4
|
+
|
|
5
|
+
from ..constants import GRPC
|
|
6
|
+
from ..exceptions import APIError
|
|
7
|
+
from ..types import Gem, GemJar, RPCData
|
|
8
|
+
from ..utils import running, logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GemMixin:
|
|
12
|
+
"""
|
|
13
|
+
Mixin class providing gem-related functionality for GeminiClient.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self._gems: GemJar | None = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def gems(self) -> GemJar:
|
|
22
|
+
"""
|
|
23
|
+
Returns a `GemJar` object containing cached gems.
|
|
24
|
+
Only available after calling `GeminiClient.fetch_gems()`.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
:class:`GemJar`
|
|
29
|
+
Refer to `gemini_webapi.types.GemJar`.
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
`RuntimeError`
|
|
34
|
+
If `GeminiClient.fetch_gems()` has not been called before accessing this property.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if self._gems is None:
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
"Gems not fetched yet. Call `GeminiClient.fetch_gems()` method to fetch gems from gemini.google.com."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return self._gems
|
|
43
|
+
|
|
44
|
+
@running(retry=2)
|
|
45
|
+
async def fetch_gems(self, include_hidden: bool = False, **kwargs) -> GemJar:
|
|
46
|
+
"""
|
|
47
|
+
Get a list of available gems from gemini, including system predefined gems and user-created custom gems.
|
|
48
|
+
|
|
49
|
+
Note that network request will be sent every time this method is called.
|
|
50
|
+
Once the gems are fetched, they will be cached and accessible via `GeminiClient.gems` property.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
include_hidden: `bool`, optional
|
|
55
|
+
There are some predefined gems that by default are not shown to users (and therefore may not work properly).
|
|
56
|
+
Set this parameter to `True` to include them in the fetched gem list.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
:class:`GemJar`
|
|
61
|
+
Refer to `gemini_webapi.types.GemJar`.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
response = await self._batch_execute(
|
|
65
|
+
[
|
|
66
|
+
RPCData(
|
|
67
|
+
rpcid=GRPC.LIST_GEMS,
|
|
68
|
+
payload="[4]" if include_hidden else "[3]",
|
|
69
|
+
identifier="system",
|
|
70
|
+
),
|
|
71
|
+
RPCData(
|
|
72
|
+
rpcid=GRPC.LIST_GEMS,
|
|
73
|
+
payload="[2]",
|
|
74
|
+
identifier="custom",
|
|
75
|
+
),
|
|
76
|
+
],
|
|
77
|
+
**kwargs,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
response_json = json.loads(response.text.split("\n")[2])
|
|
82
|
+
|
|
83
|
+
predefined_gems, custom_gems = [], []
|
|
84
|
+
|
|
85
|
+
for part in response_json:
|
|
86
|
+
if part[-1] == "system":
|
|
87
|
+
predefined_gems = json.loads(part[2])[2]
|
|
88
|
+
elif part[-1] == "custom":
|
|
89
|
+
if custom_gems_container := json.loads(part[2]):
|
|
90
|
+
custom_gems = custom_gems_container[2]
|
|
91
|
+
|
|
92
|
+
if not predefined_gems and not custom_gems:
|
|
93
|
+
raise Exception
|
|
94
|
+
except Exception:
|
|
95
|
+
await self.close()
|
|
96
|
+
logger.debug(f"Invalid response: {response.text}")
|
|
97
|
+
raise APIError(
|
|
98
|
+
"Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self._gems = GemJar(
|
|
102
|
+
itertools.chain(
|
|
103
|
+
(
|
|
104
|
+
(
|
|
105
|
+
gem[0],
|
|
106
|
+
Gem(
|
|
107
|
+
id=gem[0],
|
|
108
|
+
name=gem[1][0],
|
|
109
|
+
description=gem[1][1],
|
|
110
|
+
prompt=gem[2] and gem[2][0] or None,
|
|
111
|
+
predefined=True,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
for gem in predefined_gems
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
(
|
|
118
|
+
gem[0],
|
|
119
|
+
Gem(
|
|
120
|
+
id=gem[0],
|
|
121
|
+
name=gem[1][0],
|
|
122
|
+
description=gem[1][1],
|
|
123
|
+
prompt=gem[2] and gem[2][0] or None,
|
|
124
|
+
predefined=False,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
for gem in custom_gems
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return self._gems
|
|
133
|
+
|
|
134
|
+
@running(retry=2)
|
|
135
|
+
async def create_gem(self, name: str, prompt: str, description: str = "") -> Gem:
|
|
136
|
+
"""
|
|
137
|
+
Create a new custom gem.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
name: `str`
|
|
142
|
+
Name of the custom gem.
|
|
143
|
+
prompt: `str`
|
|
144
|
+
System instructions for the custom gem.
|
|
145
|
+
description: `str`, optional
|
|
146
|
+
Description of the custom gem (has no effect on the model's behavior).
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
:class:`Gem`
|
|
151
|
+
The created gem.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
response = await self._batch_execute(
|
|
155
|
+
[
|
|
156
|
+
RPCData(
|
|
157
|
+
rpcid=GRPC.CREATE_GEM,
|
|
158
|
+
payload=json.dumps(
|
|
159
|
+
[
|
|
160
|
+
[
|
|
161
|
+
name,
|
|
162
|
+
description,
|
|
163
|
+
prompt,
|
|
164
|
+
None,
|
|
165
|
+
None,
|
|
166
|
+
None,
|
|
167
|
+
None,
|
|
168
|
+
None,
|
|
169
|
+
0,
|
|
170
|
+
None,
|
|
171
|
+
1,
|
|
172
|
+
None,
|
|
173
|
+
None,
|
|
174
|
+
None,
|
|
175
|
+
[],
|
|
176
|
+
]
|
|
177
|
+
]
|
|
178
|
+
).decode(),
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
response_json = json.loads(response.text.split("\n")[2])
|
|
185
|
+
gem_id = json.loads(response_json[0][2])[0]
|
|
186
|
+
except Exception:
|
|
187
|
+
await self.close()
|
|
188
|
+
logger.debug(f"Invalid response: {response.text}")
|
|
189
|
+
raise APIError(
|
|
190
|
+
"Failed to create gem. Invalid response data received. Client will try to re-initialize on next request."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return Gem(
|
|
194
|
+
id=gem_id,
|
|
195
|
+
name=name,
|
|
196
|
+
description=description,
|
|
197
|
+
prompt=prompt,
|
|
198
|
+
predefined=False,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@running(retry=2)
|
|
202
|
+
async def update_gem(
|
|
203
|
+
self, gem: Gem | str, name: str, prompt: str, description: str = ""
|
|
204
|
+
) -> Gem:
|
|
205
|
+
"""
|
|
206
|
+
Update an existing custom gem.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
gem: `Gem | str`
|
|
211
|
+
Gem to update, can be either a `gemini_webapi.types.Gem` object or a gem id string.
|
|
212
|
+
name: `str`
|
|
213
|
+
New name for the custom gem.
|
|
214
|
+
prompt: `str`
|
|
215
|
+
New system instructions for the custom gem.
|
|
216
|
+
description: `str`, optional
|
|
217
|
+
New description of the custom gem (has no effect on the model's behavior).
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
:class:`Gem`
|
|
222
|
+
The updated gem.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if isinstance(gem, Gem):
|
|
226
|
+
gem_id = gem.id
|
|
227
|
+
else:
|
|
228
|
+
gem_id = gem
|
|
229
|
+
|
|
230
|
+
await self._batch_execute(
|
|
231
|
+
[
|
|
232
|
+
RPCData(
|
|
233
|
+
rpcid=GRPC.UPDATE_GEM,
|
|
234
|
+
payload=json.dumps(
|
|
235
|
+
[
|
|
236
|
+
gem_id,
|
|
237
|
+
[
|
|
238
|
+
name,
|
|
239
|
+
description,
|
|
240
|
+
prompt,
|
|
241
|
+
None,
|
|
242
|
+
None,
|
|
243
|
+
None,
|
|
244
|
+
None,
|
|
245
|
+
None,
|
|
246
|
+
0,
|
|
247
|
+
None,
|
|
248
|
+
1,
|
|
249
|
+
None,
|
|
250
|
+
None,
|
|
251
|
+
None,
|
|
252
|
+
[],
|
|
253
|
+
0,
|
|
254
|
+
],
|
|
255
|
+
]
|
|
256
|
+
).decode(),
|
|
257
|
+
)
|
|
258
|
+
]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return Gem(
|
|
262
|
+
id=gem_id,
|
|
263
|
+
name=name,
|
|
264
|
+
description=description,
|
|
265
|
+
prompt=prompt,
|
|
266
|
+
predefined=False,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@running(retry=2)
|
|
270
|
+
async def delete_gem(self, gem: Gem | str, **kwargs) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Delete a custom gem.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
gem: `Gem | str`
|
|
277
|
+
Gem to delete, can be either a `gemini_webapi.types.Gem` object or a gem id string.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
if isinstance(gem, Gem):
|
|
281
|
+
gem_id = gem.id
|
|
282
|
+
else:
|
|
283
|
+
gem_id = gem
|
|
284
|
+
|
|
285
|
+
await self._batch_execute(
|
|
286
|
+
[RPCData(rpcid=GRPC.DELETE_GEM, payload=json.dumps([gem_id]).decode())],
|
|
287
|
+
**kwargs,
|
|
288
|
+
)
|
gemini_webapi/constants.py
CHANGED
gemini_webapi/types/image.py
CHANGED
|
@@ -30,10 +30,10 @@ class Image(BaseModel):
|
|
|
30
30
|
proxy: str | None = None
|
|
31
31
|
|
|
32
32
|
def __str__(self):
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
return (
|
|
34
|
+
f"Image(title='{self.title}', alt='{self.alt}', "
|
|
35
|
+
f"url='{len(self.url) <= 20 and self.url or self.url[:8] + '...' + self.url[-12:]}')"
|
|
36
|
+
)
|
|
37
37
|
|
|
38
38
|
async def save(
|
|
39
39
|
self,
|
gemini_webapi/utils/__init__.py
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
|
|
4
|
+
from ..exceptions import APIError, ImageGenerationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def running(retry: int = 0) -> callable:
|
|
8
|
+
"""
|
|
9
|
+
Decorator to check if GeminiClient is running before making a request.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
retry: `int`, optional
|
|
14
|
+
Max number of retries when `gemini_webapi.APIError` is raised.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def decorator(func):
|
|
18
|
+
@functools.wraps(func)
|
|
19
|
+
async def wrapper(client, *args, retry=retry, **kwargs):
|
|
20
|
+
try:
|
|
21
|
+
if not client.running:
|
|
22
|
+
await client.init(
|
|
23
|
+
timeout=client.timeout,
|
|
24
|
+
auto_close=client.auto_close,
|
|
25
|
+
close_delay=client.close_delay,
|
|
26
|
+
auto_refresh=client.auto_refresh,
|
|
27
|
+
refresh_interval=client.refresh_interval,
|
|
28
|
+
verbose=False,
|
|
29
|
+
)
|
|
30
|
+
if client.running:
|
|
31
|
+
return await func(client, *args, **kwargs)
|
|
32
|
+
|
|
33
|
+
# Should not reach here
|
|
34
|
+
raise APIError(
|
|
35
|
+
f"Invalid function call: GeminiClient.{func.__name__}. Client initialization failed."
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
return await func(client, *args, **kwargs)
|
|
39
|
+
except APIError as e:
|
|
40
|
+
# Image generation takes too long, only retry once
|
|
41
|
+
if isinstance(e, ImageGenerationError):
|
|
42
|
+
retry = min(1, retry)
|
|
43
|
+
|
|
44
|
+
if retry > 0:
|
|
45
|
+
await asyncio.sleep(1)
|
|
46
|
+
return await wrapper(client, *args, retry=retry - 1, **kwargs)
|
|
47
|
+
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
return wrapper
|
|
51
|
+
|
|
52
|
+
return decorator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemini-webapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Summary: ✨ An elegant async Python wrapper for Google Gemini web app
|
|
5
5
|
Author: UZQueen
|
|
6
6
|
License: GNU AFFERO GENERAL PUBLIC LICENSE
|
|
@@ -678,7 +678,7 @@ Description-Content-Type: text/markdown
|
|
|
678
678
|
License-File: LICENSE
|
|
679
679
|
Requires-Dist: httpx[http2]~=0.28.1
|
|
680
680
|
Requires-Dist: loguru~=0.7.3
|
|
681
|
-
Requires-Dist: orjson~=3.
|
|
681
|
+
Requires-Dist: orjson~=3.11.1
|
|
682
682
|
Requires-Dist: pydantic~=2.11.5
|
|
683
683
|
Dynamic: license-file
|
|
684
684
|
|
|
@@ -734,6 +734,10 @@ A reverse-engineered asynchronous python wrapper for [Google Gemini](https://gem
|
|
|
734
734
|
- [Continue previous conversations](#continue-previous-conversations)
|
|
735
735
|
- [Select language model](#select-language-model)
|
|
736
736
|
- [Apply system prompt with Gemini Gems](#apply-system-prompt-with-gemini-gems)
|
|
737
|
+
- [Manage Custom Gems](#manage-custom-gems)
|
|
738
|
+
- [Create a custom gem](#create-a-custom-gem)
|
|
739
|
+
- [Update an existing gem](#update-an-existing-gem)
|
|
740
|
+
- [Delete a custom gem](#delete-a-custom-gem)
|
|
737
741
|
- [Retrieve model's thought process](#retrieve-models-thought-process)
|
|
738
742
|
- [Retrieve images in response](#retrieve-images-in-response)
|
|
739
743
|
- [Generate images with Imagen4](#generate-images-with-imagen4)
|
|
@@ -963,6 +967,75 @@ async def main():
|
|
|
963
967
|
print(response2)
|
|
964
968
|
```
|
|
965
969
|
|
|
970
|
+
### Manage Custom Gems
|
|
971
|
+
|
|
972
|
+
You can create, update, and delete your custom gems programmatically with the API. Note that predefined system gems cannot be modified or deleted.
|
|
973
|
+
|
|
974
|
+
#### Create a custom gem
|
|
975
|
+
|
|
976
|
+
Create a new custom gem with a name, system prompt (instructions), and optional description:
|
|
977
|
+
|
|
978
|
+
```python
|
|
979
|
+
async def main():
|
|
980
|
+
# Create a new custom gem
|
|
981
|
+
new_gem = await client.create_gem(
|
|
982
|
+
name="Python Tutor",
|
|
983
|
+
prompt="You are a helpful Python programming tutor.",
|
|
984
|
+
description="A specialized gem for Python programming"
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
print(f"Custom gem created: {new_gem}")
|
|
988
|
+
|
|
989
|
+
# Use the newly created gem in a conversation
|
|
990
|
+
response = await client.generate_content(
|
|
991
|
+
"Explain how list comprehensions work in Python",
|
|
992
|
+
gem=new_gem
|
|
993
|
+
)
|
|
994
|
+
print(response.text)
|
|
995
|
+
|
|
996
|
+
asyncio.run(main())
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### Update an existing gem
|
|
1000
|
+
|
|
1001
|
+
> [!NOTE]
|
|
1002
|
+
>
|
|
1003
|
+
> When updating a gem, you must provide all parameters (name, prompt, description) even if you only want to change one of them.
|
|
1004
|
+
|
|
1005
|
+
```python
|
|
1006
|
+
async def main():
|
|
1007
|
+
# Get a custom gem (assuming you have one named "Python Tutor")
|
|
1008
|
+
await client.fetch_gems()
|
|
1009
|
+
python_tutor = client.gems.get(name="Python Tutor")
|
|
1010
|
+
|
|
1011
|
+
# Update the gem with new instructions
|
|
1012
|
+
updated_gem = await client.update_gem(
|
|
1013
|
+
gem=python_tutor, # Can also pass gem ID string
|
|
1014
|
+
name="Advanced Python Tutor",
|
|
1015
|
+
prompt="You are an expert Python programming tutor.",
|
|
1016
|
+
description="An advanced Python programming assistant"
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
print(f"Custom gem updated: {updated_gem}")
|
|
1020
|
+
|
|
1021
|
+
asyncio.run(main())
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
#### Delete a custom gem
|
|
1025
|
+
|
|
1026
|
+
```python
|
|
1027
|
+
async def main():
|
|
1028
|
+
# Get the gem to delete
|
|
1029
|
+
await client.fetch_gems()
|
|
1030
|
+
gem_to_delete = client.gems.get(name="Advanced Python Tutor")
|
|
1031
|
+
|
|
1032
|
+
# Delete the gem
|
|
1033
|
+
await client.delete_gem(gem_to_delete) # Can also pass gem ID string
|
|
1034
|
+
print(f"Custom gem deleted: {gem_to_delete.name}")
|
|
1035
|
+
|
|
1036
|
+
asyncio.run(main())
|
|
1037
|
+
```
|
|
1038
|
+
|
|
966
1039
|
### Retrieve model's thought process
|
|
967
1040
|
|
|
968
1041
|
When using models with thinking capabilities, the model's thought process will be populated in `ModelOutput.thoughts`.
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
gemini_webapi/__init__.py,sha256=7ELCiUoI10ea3daeJxnv0UwqLVKpM7rxsgOZsPMstO8,150
|
|
2
|
-
gemini_webapi/client.py,sha256=
|
|
3
|
-
gemini_webapi/constants.py,sha256=
|
|
2
|
+
gemini_webapi/client.py,sha256=G0nmRCeEmT0BpqgYY4zxb_ozFDmb39JqtddKpIN6JlA,26870
|
|
3
|
+
gemini_webapi/constants.py,sha256=VE3URBOjORTXu7fTHAkjGgOHphGNlx1iA1T9EHdapnE,2597
|
|
4
4
|
gemini_webapi/exceptions.py,sha256=qkXrIpr0L7LtGbq3VcTO8D1xZ50pJtt0dDRp5I3uDSg,1038
|
|
5
|
+
gemini_webapi/components/__init__.py,sha256=wolxuAJJ32-jmHOKgpsesexP7hXea1JMo5vI52wysTI,48
|
|
6
|
+
gemini_webapi/components/gem_mixin.py,sha256=WPJkYDS4yQpLMBNQ94LQo5w59RgkllWaSiHsFG1k5GU,8795
|
|
5
7
|
gemini_webapi/types/__init__.py,sha256=1DU4JEw2KHQJbtOgOuvoEXtzQCMwAkyo0koSFVdT9JY,192
|
|
6
8
|
gemini_webapi/types/candidate.py,sha256=67BhY75toE5mVuB21cmHcTFtw332V_KmCjr3-9VTbJo,1477
|
|
7
9
|
gemini_webapi/types/gem.py,sha256=3Ppjq9V22Zp4Lb9a9ZnDviDKQpfSQf8UZxqOEjeEWd4,4070
|
|
8
10
|
gemini_webapi/types/grpc.py,sha256=S64h1oeC7ZJC50kmS_C2CQ7WVTanhJ4kqTFx5ZYayXI,917
|
|
9
|
-
gemini_webapi/types/image.py,sha256=
|
|
11
|
+
gemini_webapi/types/image.py,sha256=NhW1QY2Agzb6UbrIjZLnqxqukabDSGbmoCsVguzko10,5395
|
|
10
12
|
gemini_webapi/types/modeloutput.py,sha256=h07kQOkL5r-oPLvZ59uVtO1eP4FGy5ZpzuYQzAeQdr8,1196
|
|
11
|
-
gemini_webapi/utils/__init__.py,sha256=
|
|
13
|
+
gemini_webapi/utils/__init__.py,sha256=RIU1MBqYNjih0XMMt7wCxAA-X9KVF53nDbAjaxaGTo8,352
|
|
14
|
+
gemini_webapi/utils/decorators.py,sha256=AuY6sU1_6_ZqeL92dTAi3eRPQ7zubB5VuBZEiz16aBM,1741
|
|
12
15
|
gemini_webapi/utils/get_access_token.py,sha256=eNn1omFO41wWXco1eM-KXR2CEi0Tb-chlph7H-PCNjg,6137
|
|
13
16
|
gemini_webapi/utils/load_browser_cookies.py,sha256=A5n_VsB7Rm8ck5lpy856UNJEhv30l3dvQ3j0g3ln1fE,1535
|
|
14
17
|
gemini_webapi/utils/logger.py,sha256=0VcxhVLhHBRDQutNCpapP1y_MhPoQ2ud1uIFLqxC3Z8,958
|
|
15
18
|
gemini_webapi/utils/rotate_1psidts.py,sha256=NyQ9OYPLBOcvpc8bodvEYDIVFrsYN0kdfc831lPEctM,1680
|
|
16
19
|
gemini_webapi/utils/upload_file.py,sha256=SJOMr6kryK_ClrKmqI96fqZBNFOMPsyAvFINAGAU3rk,1468
|
|
17
|
-
gemini_webapi-1.
|
|
18
|
-
gemini_webapi-1.
|
|
19
|
-
gemini_webapi-1.
|
|
20
|
-
gemini_webapi-1.
|
|
21
|
-
gemini_webapi-1.
|
|
20
|
+
gemini_webapi-1.15.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
21
|
+
gemini_webapi-1.15.0.dist-info/METADATA,sha256=KwfKZ9MwLiae-xnxzRT6YH4p7olvTqZudmqZa234v0g,61279
|
|
22
|
+
gemini_webapi-1.15.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
gemini_webapi-1.15.0.dist-info/top_level.txt,sha256=dtWtug_ZrmnUqCYuu8NmGzTgWglHeNzhHU_hXmqZGWE,14
|
|
24
|
+
gemini_webapi-1.15.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|