reyfetch 1.0.35__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.
- reyfetch/__init__.py +21 -0
- reyfetch/rali.py +990 -0
- reyfetch/rall.py +19 -0
- reyfetch/rbaidu.py +467 -0
- reyfetch/rbase.py +243 -0
- reyfetch/rdouban.py +565 -0
- reyfetch/rgeneral.py +158 -0
- reyfetch/rsina.py +239 -0
- reyfetch/rtoutiao.py +71 -0
- reyfetch/rweibo.py +90 -0
- reyfetch-1.0.35.dist-info/METADATA +30 -0
- reyfetch-1.0.35.dist-info/RECORD +14 -0
- reyfetch-1.0.35.dist-info/WHEEL +4 -0
- reyfetch-1.0.35.dist-info/licenses/LICENSE +7 -0
reyfetch/rali.py
ADDED
@@ -0,0 +1,990 @@
|
|
1
|
+
# !/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
@Time : 2025-07-17 22:32:37
|
6
|
+
@Author : Rey
|
7
|
+
@Contact : reyxbo@163.com
|
8
|
+
@Explain : Ali Web fetch methods.
|
9
|
+
"""
|
10
|
+
|
11
|
+
|
12
|
+
from typing import Any, TypedDict, NotRequired, Literal, overload, NoReturn
|
13
|
+
from collections.abc import Hashable, Iterable, Generator
|
14
|
+
from json import loads as json_loads
|
15
|
+
from reydb import rorm
|
16
|
+
from reydb.rdb import Database
|
17
|
+
from reykit.rbase import throw
|
18
|
+
from reykit.rnet import request as reykit_request
|
19
|
+
from reykit.rtime import now
|
20
|
+
|
21
|
+
from .rbase import FetchRequest, FetchRequestWithDatabase, FetchRequestDatabaseRecord
|
22
|
+
|
23
|
+
|
24
|
+
__all__ = (
|
25
|
+
'DatabaseTableAliQwen',
|
26
|
+
'FetchRequestAli',
|
27
|
+
'FetchRequestAliQwen'
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
# Key 'role' value 'system' only in first.
|
32
|
+
# Key 'role' value 'user' and 'assistant' can mix.
|
33
|
+
type ChatRecordRole = Literal['system', 'user', 'assistant']
|
34
|
+
ChatRecordToken = TypedDict('ChatRecordToken', {'total': int, 'input': int, 'output': int, 'output_think': int | None})
|
35
|
+
ChatResponseWebItem = TypedDict('ChatResponseWebItem', {'site': str | None, 'icon': str | None, 'index': int, 'url': str, 'title': str})
|
36
|
+
type ChatResponseWeb = list[ChatResponseWebItem]
|
37
|
+
ChatRecord = TypedDict(
|
38
|
+
'ChatRecord',
|
39
|
+
{
|
40
|
+
'time': int,
|
41
|
+
'role': ChatRecordRole,
|
42
|
+
'content': str | None,
|
43
|
+
'len': int | None,
|
44
|
+
'token': ChatRecordToken | None,
|
45
|
+
'web': ChatResponseWeb | None,
|
46
|
+
'think': str | None
|
47
|
+
}
|
48
|
+
)
|
49
|
+
type ChatRecords = list[ChatRecord]
|
50
|
+
type ChatRecordsIndex = Hashable
|
51
|
+
type ChatRecordsData = dict[ChatRecordsIndex, ChatRecords]
|
52
|
+
ChatRecordsAppend = TypedDict('ChatRecordsAppend', {'time': NotRequired[int], 'role': NotRequired[ChatRecordRole], 'content': str})
|
53
|
+
type ChatRecordsAppends = list[ChatRecordsAppend]
|
54
|
+
ChatReplyGenerator = Generator[str, Any, None]
|
55
|
+
ChatThinkGenerator = Generator[str, Any, None]
|
56
|
+
|
57
|
+
|
58
|
+
class DatabaseTableAliQwen(rorm.Model, table=True):
|
59
|
+
"""
|
60
|
+
Database `ali_qwen` table model.
|
61
|
+
"""
|
62
|
+
|
63
|
+
__name__ = 'ali_qwen'
|
64
|
+
__comment__ = 'Ali API qwen model request record table.'
|
65
|
+
id: int = rorm.Field(rorm.types_mysql.INTEGER(unsigned=True), key_auto=True, comment='ID.')
|
66
|
+
request_time: rorm.Datetime = rorm.Field(not_null=True, comment='Request time.')
|
67
|
+
response_time: rorm.Datetime = rorm.Field(not_null=True, comment='Response time, when is stream response, then is full return after time.')
|
68
|
+
messages: str = rorm.Field(rorm.types.JSON, not_null=True, comment='Input messages data.')
|
69
|
+
reply: str = rorm.Field(rorm.types.TEXT, not_null=True, comment='Output reply text.')
|
70
|
+
think: str = rorm.Field(rorm.types.TEXT, comment='Output deep think text.')
|
71
|
+
web: str = rorm.Field(rorm.types.JSON, not_null=True, comment='Web search data.')
|
72
|
+
token_total: int = rorm.Field(rorm.types_mysql.MEDIUMINT(unsigned=True), not_null=True, comment='Usage total Token.')
|
73
|
+
token_input: int = rorm.Field(rorm.types_mysql.MEDIUMINT(unsigned=True), not_null=True, comment='Usage input Token.')
|
74
|
+
token_output: int = rorm.Field(rorm.types_mysql.MEDIUMINT(unsigned=True), not_null=True, comment='Usage output Token.')
|
75
|
+
token_output_think: int = rorm.Field(rorm.types_mysql.MEDIUMINT(unsigned=True), comment='Usage output think Token.')
|
76
|
+
model: str = rorm.Field(rorm.types.VARCHAR(100), not_null=True, comment='Model name.')
|
77
|
+
|
78
|
+
|
79
|
+
class FetchRequestAli(FetchRequest):
|
80
|
+
"""
|
81
|
+
Reuqest Ali API fetch type.
|
82
|
+
"""
|
83
|
+
|
84
|
+
|
85
|
+
class FetchRequestAliQwen(FetchRequestAli, FetchRequestWithDatabase):
|
86
|
+
"""
|
87
|
+
Request Ali QWen API fetch type.
|
88
|
+
Can create database used `self.build_db` method.
|
89
|
+
|
90
|
+
Attributes
|
91
|
+
----------
|
92
|
+
url_api : API request URL.
|
93
|
+
url_doc : API document URL.
|
94
|
+
model = API AI model type.
|
95
|
+
db_names : Database table name mapping dictionary.
|
96
|
+
"""
|
97
|
+
|
98
|
+
url_api = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation'
|
99
|
+
url_doc = 'https://help.aliyun.com/zh/model-studio/use-qwen-by-calling-api?spm=a2c4g.11186623.0.0.330e7d9dSBCaZQ'
|
100
|
+
model = 'qwen-turbo-latest'
|
101
|
+
db_names = {
|
102
|
+
'ali_qwen': 'ali_qwen',
|
103
|
+
'stats_ali_qwen': 'stats_ali_qwen'
|
104
|
+
}
|
105
|
+
|
106
|
+
|
107
|
+
def __init__(
|
108
|
+
self,
|
109
|
+
key: str,
|
110
|
+
db: Database | None = None,
|
111
|
+
system: str | None = None,
|
112
|
+
rand: float = 0.5,
|
113
|
+
history_max_char: int | None = None,
|
114
|
+
history_max_time: float | None = None
|
115
|
+
) -> None:
|
116
|
+
"""
|
117
|
+
Build instance attributes.
|
118
|
+
|
119
|
+
Parameters
|
120
|
+
----------
|
121
|
+
key : API key.
|
122
|
+
db : `Database` instance, insert request record to table.
|
123
|
+
system : AI system description.
|
124
|
+
rand : AI reply randomness, value range is `[0,1]`.
|
125
|
+
history_max_char : History messages record maximum character count.
|
126
|
+
history_max_time : History messages record maximum second.
|
127
|
+
"""
|
128
|
+
|
129
|
+
# Check.
|
130
|
+
if not 0 <= rand <= 1:
|
131
|
+
throw(ValueError, rand)
|
132
|
+
|
133
|
+
# Build.
|
134
|
+
self.key = key
|
135
|
+
self.auth = 'Bearer ' + key
|
136
|
+
self.db = db
|
137
|
+
self.system = system
|
138
|
+
self.rand = rand
|
139
|
+
self.history_max_char = history_max_char
|
140
|
+
self.history_max_time = history_max_time
|
141
|
+
self.data: ChatRecordsData = {}
|
142
|
+
|
143
|
+
# Database.
|
144
|
+
self.db_record = FetchRequestDatabaseRecord(self, 'api', 'ali_qwen')
|
145
|
+
|
146
|
+
|
147
|
+
@overload
|
148
|
+
def request(self, json: dict, stream: Literal[True]) -> Iterable[str]: ...
|
149
|
+
|
150
|
+
@overload
|
151
|
+
def request(self, json: dict, stream: Literal[False]) -> dict: ...
|
152
|
+
|
153
|
+
def request(self, json: dict, stream: bool) -> dict | Iterable[str]:
|
154
|
+
"""
|
155
|
+
Request API.
|
156
|
+
|
157
|
+
Parameters
|
158
|
+
----------
|
159
|
+
json : Request body.
|
160
|
+
stream : Whether use stream response.
|
161
|
+
|
162
|
+
Returns
|
163
|
+
-------
|
164
|
+
Response json or iterable.
|
165
|
+
"""
|
166
|
+
|
167
|
+
# Parameter.
|
168
|
+
json['model'] = self.model
|
169
|
+
json_params = json.setdefault('parameters', {})
|
170
|
+
json_params_temperature = self.rand * 2
|
171
|
+
if json_params_temperature == 2:
|
172
|
+
json_params_temperature = 1.99
|
173
|
+
json_params['temperature'] = json_params_temperature
|
174
|
+
json_params['presence_penalty'] = self.rand * 4 - 2
|
175
|
+
headers = {'Authorization': self.auth, 'Content-Type': 'application/json'}
|
176
|
+
if stream:
|
177
|
+
headers['X-DashScope-SSE'] = 'enable'
|
178
|
+
json_params['incremental_output'] = True
|
179
|
+
|
180
|
+
# Request.
|
181
|
+
response = reykit_request(
|
182
|
+
self.url_api,
|
183
|
+
json=json,
|
184
|
+
headers=headers,
|
185
|
+
stream=stream,
|
186
|
+
check=True
|
187
|
+
)
|
188
|
+
|
189
|
+
# Stream.
|
190
|
+
if stream:
|
191
|
+
iterable: Iterable[str] = response.iter_lines(decode_unicode=True)
|
192
|
+
return iterable
|
193
|
+
|
194
|
+
# Check.
|
195
|
+
content_type = response.headers['Content-Type']
|
196
|
+
if content_type.startswith('application/json'):
|
197
|
+
response_json: dict = response.json()
|
198
|
+
if 'code' in response_json:
|
199
|
+
throw(AssertionError, response_json)
|
200
|
+
else:
|
201
|
+
throw(AssertionError, content_type)
|
202
|
+
|
203
|
+
return response_json
|
204
|
+
|
205
|
+
|
206
|
+
def extract_response_text(self, response_json: dict) -> str | None:
|
207
|
+
"""
|
208
|
+
Extract reply text from response JSON.
|
209
|
+
|
210
|
+
Parameters
|
211
|
+
----------
|
212
|
+
response_json : Response JSON.
|
213
|
+
|
214
|
+
Returns
|
215
|
+
-------
|
216
|
+
Reply text.
|
217
|
+
"""
|
218
|
+
|
219
|
+
# Extract.
|
220
|
+
output_data = response_json.get('output')
|
221
|
+
if output_data is not None:
|
222
|
+
response_text: str = output_data['choices'][0]['message']['content']
|
223
|
+
else:
|
224
|
+
response_text = None
|
225
|
+
|
226
|
+
return response_text
|
227
|
+
|
228
|
+
|
229
|
+
def extract_response_token(self, response_json: dict) -> ChatRecordToken | None:
|
230
|
+
"""
|
231
|
+
Extract usage token data from response JSON.
|
232
|
+
|
233
|
+
Parameters
|
234
|
+
----------
|
235
|
+
response_json : Response JSON.
|
236
|
+
|
237
|
+
Returns
|
238
|
+
-------
|
239
|
+
Usage token data.
|
240
|
+
"""
|
241
|
+
|
242
|
+
# Extract.
|
243
|
+
token_data: dict | None = response_json.get('usage')
|
244
|
+
if token_data is not None:
|
245
|
+
token_data = {
|
246
|
+
'total': token_data['total_tokens'],
|
247
|
+
'input': token_data['input_tokens'],
|
248
|
+
'output': token_data['output_tokens'],
|
249
|
+
'output_think': token_data.get('output_tokens_details', {}).get('reasoning_tokens')
|
250
|
+
}
|
251
|
+
|
252
|
+
return token_data
|
253
|
+
|
254
|
+
|
255
|
+
def extract_response_web(self, response_json: dict) -> ChatResponseWeb | None:
|
256
|
+
"""
|
257
|
+
Extract web data from response JSON.
|
258
|
+
|
259
|
+
Parameters
|
260
|
+
----------
|
261
|
+
response_json : Response JSON.
|
262
|
+
|
263
|
+
Returns
|
264
|
+
-------
|
265
|
+
Web data.
|
266
|
+
"""
|
267
|
+
|
268
|
+
# Extract.
|
269
|
+
json_output: dict = response_json['output']
|
270
|
+
search_info: dict = json_output.get('search_info', {})
|
271
|
+
web_data: list[dict] = search_info.get('search_results', [])
|
272
|
+
for item in web_data:
|
273
|
+
item.setdefault('site_name', None)
|
274
|
+
item.setdefault('icon', None)
|
275
|
+
item['site'] = item.pop('site_name')
|
276
|
+
if item['site'] == '':
|
277
|
+
item['site'] = None
|
278
|
+
if item['icon'] == '':
|
279
|
+
item['icon'] = None
|
280
|
+
web_data = web_data or None
|
281
|
+
|
282
|
+
return web_data
|
283
|
+
|
284
|
+
|
285
|
+
def extract_response_think(self, response_json: dict) -> str | None:
|
286
|
+
"""
|
287
|
+
Extract deep think text from response JSON.
|
288
|
+
|
289
|
+
Parameters
|
290
|
+
----------
|
291
|
+
response_json : Response JSON.
|
292
|
+
|
293
|
+
Returns
|
294
|
+
-------
|
295
|
+
Deep think text.
|
296
|
+
"""
|
297
|
+
|
298
|
+
# Extract.
|
299
|
+
json_message: dict = response_json['output']['choices'][0]['message']
|
300
|
+
response_think = json_message.get('reasoning_content')
|
301
|
+
response_think = response_think or None
|
302
|
+
|
303
|
+
return response_think
|
304
|
+
|
305
|
+
|
306
|
+
def extract_response_record(self, response_json: dict) -> ChatRecord:
|
307
|
+
"""
|
308
|
+
Extract reply record from response JSON.
|
309
|
+
|
310
|
+
Parameters
|
311
|
+
----------
|
312
|
+
response_json : Response JSON.
|
313
|
+
|
314
|
+
Returns
|
315
|
+
-------
|
316
|
+
Reply record.
|
317
|
+
"""
|
318
|
+
|
319
|
+
# Extract.
|
320
|
+
response_text = self.extract_response_text(response_json)
|
321
|
+
response_token = self.extract_response_token(response_json)
|
322
|
+
response_web = self.extract_response_web(response_json)
|
323
|
+
response_think = self.extract_response_think(response_json)
|
324
|
+
if response_text is None:
|
325
|
+
response_text_len = None
|
326
|
+
else:
|
327
|
+
response_text_len = len(response_text)
|
328
|
+
chat_record_reply = {
|
329
|
+
'time': now('timestamp'),
|
330
|
+
'role': 'assistant',
|
331
|
+
'content': response_text,
|
332
|
+
'len': response_text_len,
|
333
|
+
'token': response_token,
|
334
|
+
'web': response_web,
|
335
|
+
'think': response_think
|
336
|
+
}
|
337
|
+
|
338
|
+
return chat_record_reply
|
339
|
+
|
340
|
+
|
341
|
+
def extract_response_generator(self, response_iter: Iterable[str]):
|
342
|
+
"""
|
343
|
+
Extract reply generator from response JSON.
|
344
|
+
|
345
|
+
Parameters
|
346
|
+
----------
|
347
|
+
response_iter : Response iterable.
|
348
|
+
|
349
|
+
Returns
|
350
|
+
-------
|
351
|
+
Reply Generator.
|
352
|
+
"""
|
353
|
+
|
354
|
+
# First.
|
355
|
+
response_line_first = None
|
356
|
+
for response_line in response_iter:
|
357
|
+
if not response_line.startswith(('data:{', 'data: {')):
|
358
|
+
continue
|
359
|
+
response_line_first = response_line
|
360
|
+
break
|
361
|
+
|
362
|
+
## Check.
|
363
|
+
if response_line_first is None:
|
364
|
+
throw(AssertionError, response_line_first)
|
365
|
+
|
366
|
+
response_line_first = response_line_first[5:].strip()
|
367
|
+
response_json_first: dict = json_loads(response_line_first)
|
368
|
+
chat_record_reply = self.extract_response_record(response_json_first)
|
369
|
+
is_think_emptied = not bool(chat_record_reply['think'])
|
370
|
+
|
371
|
+
### Define.
|
372
|
+
def _generator(mode: Literal['text', 'think']) -> Generator[str, Any, None]:
|
373
|
+
"""
|
374
|
+
Generator function of stream response.
|
375
|
+
|
376
|
+
Parameters
|
377
|
+
----------
|
378
|
+
mode : Generate value type.
|
379
|
+
- `Literal['text']`: Reply text.
|
380
|
+
- `Literal['think']`: Deep think text.
|
381
|
+
|
382
|
+
Returns
|
383
|
+
-------
|
384
|
+
Generator.
|
385
|
+
"""
|
386
|
+
|
387
|
+
# Parameter.
|
388
|
+
nonlocal is_think_emptied
|
389
|
+
chat_record_reply['content'] = chat_record_reply['content'] or ''
|
390
|
+
chat_record_reply['think'] = chat_record_reply['think'] or ''
|
391
|
+
|
392
|
+
# Check.
|
393
|
+
if (
|
394
|
+
not is_think_emptied
|
395
|
+
and mode == 'text'
|
396
|
+
):
|
397
|
+
text = 'must first used up think generator'
|
398
|
+
throw(AssertionError, text=text)
|
399
|
+
|
400
|
+
# First.
|
401
|
+
if mode == 'text':
|
402
|
+
yield chat_record_reply['content']
|
403
|
+
elif mode == 'think':
|
404
|
+
yield chat_record_reply['think']
|
405
|
+
|
406
|
+
# Next.
|
407
|
+
for response_line in response_iter:
|
408
|
+
|
409
|
+
## Filter.
|
410
|
+
if not response_line.startswith(('data:{', 'data: {')):
|
411
|
+
continue
|
412
|
+
|
413
|
+
## JSON.
|
414
|
+
response_line = response_line[5:]
|
415
|
+
response_line = response_line.strip()
|
416
|
+
response_json: dict = json_loads(response_line)
|
417
|
+
|
418
|
+
## Token.
|
419
|
+
response_token = self.extract_response_token(response_json)
|
420
|
+
chat_record_reply['token'] = response_token
|
421
|
+
|
422
|
+
## Web.
|
423
|
+
if chat_record_reply['web'] is None:
|
424
|
+
response_web = self.extract_response_web(response_json)
|
425
|
+
chat_record_reply['web'] = response_web
|
426
|
+
|
427
|
+
## Text.
|
428
|
+
if mode == 'text':
|
429
|
+
response_text = self.extract_response_text(response_json)
|
430
|
+
if response_text is None:
|
431
|
+
continue
|
432
|
+
chat_record_reply['content'] += response_text
|
433
|
+
chat_record_reply['len'] += len(response_text)
|
434
|
+
yield response_text
|
435
|
+
|
436
|
+
## Think.
|
437
|
+
elif mode == 'think':
|
438
|
+
response_think = self.extract_response_think(response_json)
|
439
|
+
|
440
|
+
### Last.
|
441
|
+
if response_think is None:
|
442
|
+
is_think_emptied = True
|
443
|
+
response_text = self.extract_response_text(response_json)
|
444
|
+
chat_record_reply['content'] = response_text
|
445
|
+
if response_text is not None:
|
446
|
+
chat_record_reply['len'] += len(response_text)
|
447
|
+
break
|
448
|
+
|
449
|
+
chat_record_reply['think'] += response_think
|
450
|
+
yield response_think
|
451
|
+
|
452
|
+
# Database.
|
453
|
+
else:
|
454
|
+
self.insert_db(chat_record_reply)
|
455
|
+
|
456
|
+
|
457
|
+
generator_text = _generator('text')
|
458
|
+
generator_think = _generator('think')
|
459
|
+
|
460
|
+
return chat_record_reply, generator_text, generator_think
|
461
|
+
|
462
|
+
|
463
|
+
def append_chat_records_history(
|
464
|
+
self,
|
465
|
+
records: ChatRecordsAppend | ChatRecordsAppends | str | list[str],
|
466
|
+
index: ChatRecordsIndex,
|
467
|
+
history_max_char: int | None = None,
|
468
|
+
history_max_time: float | None = None
|
469
|
+
) -> None:
|
470
|
+
"""
|
471
|
+
Append chat records.
|
472
|
+
Delete records of beyond the range from history.
|
473
|
+
|
474
|
+
Parameters
|
475
|
+
----------
|
476
|
+
records: Chat reocrds.
|
477
|
+
- `Key 'role'`: Message sender role, default `user`.
|
478
|
+
- `Key 'content'`: Message content, required.
|
479
|
+
- `str`: Message content.
|
480
|
+
- `list[str]`: Message content list.
|
481
|
+
index : Chat records index.
|
482
|
+
history_max_char : History messages record maximum character count.
|
483
|
+
- `None`: Use `self.history_max_char`.
|
484
|
+
history_max_time : History messages record maximum second.
|
485
|
+
- `None`: Use `self.history_max_time`.
|
486
|
+
"""
|
487
|
+
|
488
|
+
# Parameter.
|
489
|
+
if type(records) == str:
|
490
|
+
records = [{'content': records}]
|
491
|
+
elif type(records) == dict:
|
492
|
+
records = [records]
|
493
|
+
elif type(records) == list:
|
494
|
+
records = [
|
495
|
+
{'content': records}
|
496
|
+
if type(record) == str
|
497
|
+
else record
|
498
|
+
for record in records
|
499
|
+
]
|
500
|
+
now_timestamp = now('timestamp')
|
501
|
+
records = [
|
502
|
+
{
|
503
|
+
'time': record.get('time', now_timestamp),
|
504
|
+
'role': record.get('role', 'user'),
|
505
|
+
'content': record['content'],
|
506
|
+
'len': len(record['content']),
|
507
|
+
'token': None,
|
508
|
+
'web': None,
|
509
|
+
'think': None
|
510
|
+
}
|
511
|
+
for record in records
|
512
|
+
]
|
513
|
+
chat_records_history: ChatRecords = self.data.setdefault(index, [])
|
514
|
+
|
515
|
+
# Append.
|
516
|
+
chat_records_history.extend(records)
|
517
|
+
|
518
|
+
# Sort.
|
519
|
+
sort_key = lambda chat_record: chat_record['time']
|
520
|
+
chat_records_history.sort(key=sort_key)
|
521
|
+
|
522
|
+
# Beyond.
|
523
|
+
self.get_chat_records_history(index, history_max_char, history_max_time, True)
|
524
|
+
|
525
|
+
|
526
|
+
def get_chat_records_history(
|
527
|
+
self,
|
528
|
+
index: ChatRecordsIndex,
|
529
|
+
history_max_char: int | None = None,
|
530
|
+
history_max_time: float | None = None,
|
531
|
+
delete: bool = False
|
532
|
+
) -> ChatRecords:
|
533
|
+
"""
|
534
|
+
Get chat records.
|
535
|
+
|
536
|
+
Parameters
|
537
|
+
----------
|
538
|
+
index : Chat records index.
|
539
|
+
history_max_char : History messages record maximum character count.
|
540
|
+
- `None`: Use `self.history_max_char`.
|
541
|
+
history_max_time : History messages record maximum second.
|
542
|
+
- `None`: Use `self.history_max_time`.
|
543
|
+
delete : Whether delete records of beyond the range from history.
|
544
|
+
|
545
|
+
Returns
|
546
|
+
-------
|
547
|
+
Chat records.
|
548
|
+
"""
|
549
|
+
|
550
|
+
# Parameter.
|
551
|
+
now_timestamp = now('timestamp')
|
552
|
+
chat_records_history: ChatRecords = self.data.setdefault(index, [])
|
553
|
+
|
554
|
+
# Max.
|
555
|
+
if history_max_char is None:
|
556
|
+
history_max_char = self.history_max_char
|
557
|
+
if history_max_time is None:
|
558
|
+
history_max_time = self.history_max_time
|
559
|
+
if history_max_time is not None:
|
560
|
+
history_max_time_us = history_max_time * 1000
|
561
|
+
char_len = 0
|
562
|
+
chat_records_history_reverse = chat_records_history[::-1]
|
563
|
+
beyond_index = None
|
564
|
+
for index, chat_record in enumerate(chat_records_history_reverse):
|
565
|
+
if (
|
566
|
+
(
|
567
|
+
history_max_char is not None
|
568
|
+
and (char_len := char_len + chat_record['len']) > history_max_char
|
569
|
+
)
|
570
|
+
or (
|
571
|
+
history_max_time is not None
|
572
|
+
and now_timestamp - chat_record['time'] > history_max_time_us
|
573
|
+
)
|
574
|
+
):
|
575
|
+
beyond_index = -index
|
576
|
+
break
|
577
|
+
|
578
|
+
# Beyond.
|
579
|
+
if beyond_index is not None:
|
580
|
+
|
581
|
+
## Delete.
|
582
|
+
if delete:
|
583
|
+
if beyond_index == 0:
|
584
|
+
chat_records_history.clear()
|
585
|
+
else:
|
586
|
+
del chat_records_history[:beyond_index]
|
587
|
+
|
588
|
+
## Keep.
|
589
|
+
else:
|
590
|
+
if beyond_index == 0:
|
591
|
+
chat_records_history = []
|
592
|
+
else:
|
593
|
+
chat_records_history = chat_records_history[beyond_index:]
|
594
|
+
|
595
|
+
return chat_records_history
|
596
|
+
|
597
|
+
|
598
|
+
@overload
|
599
|
+
def chat(
|
600
|
+
self,
|
601
|
+
text: str,
|
602
|
+
index: ChatRecordsIndex | None = None,
|
603
|
+
role: str | None = None,
|
604
|
+
web: bool = False,
|
605
|
+
web_mark: bool = False,
|
606
|
+
history_max_char: int | None = None,
|
607
|
+
history_max_time: float | None = None
|
608
|
+
) -> ChatRecord: ...
|
609
|
+
|
610
|
+
@overload
|
611
|
+
def chat(
|
612
|
+
self,
|
613
|
+
text: str,
|
614
|
+
index: ChatRecordsIndex | None = None,
|
615
|
+
system: str | None = None,
|
616
|
+
web: bool = False,
|
617
|
+
web_mark: bool = False,
|
618
|
+
*,
|
619
|
+
stream: Literal[True],
|
620
|
+
history_max_char: int | None = None,
|
621
|
+
history_max_time: float | None = None
|
622
|
+
) -> tuple[ChatRecord, ChatReplyGenerator]: ...
|
623
|
+
|
624
|
+
@overload
|
625
|
+
def chat(
|
626
|
+
self,
|
627
|
+
text: str,
|
628
|
+
index: ChatRecordsIndex | None = None,
|
629
|
+
system: str | None = None,
|
630
|
+
web: bool = False,
|
631
|
+
web_mark: bool = False,
|
632
|
+
*,
|
633
|
+
think: Literal[True],
|
634
|
+
stream: Literal[True],
|
635
|
+
history_max_char: int | None = None,
|
636
|
+
history_max_time: float | None = None
|
637
|
+
) -> tuple[ChatRecord, ChatReplyGenerator, ChatThinkGenerator]: ...
|
638
|
+
|
639
|
+
@overload
|
640
|
+
def chat(
|
641
|
+
self,
|
642
|
+
text: str,
|
643
|
+
index: ChatRecordsIndex | None = None,
|
644
|
+
system: str | None = None,
|
645
|
+
web: bool = False,
|
646
|
+
web_mark: bool = False,
|
647
|
+
*,
|
648
|
+
think: Literal[True],
|
649
|
+
history_max_char: int | None = None,
|
650
|
+
history_max_time: float | None = None
|
651
|
+
) -> NoReturn: ...
|
652
|
+
|
653
|
+
def chat(
|
654
|
+
self,
|
655
|
+
text: str,
|
656
|
+
index: ChatRecordsIndex | None = None,
|
657
|
+
system: str | None = None,
|
658
|
+
web: bool = False,
|
659
|
+
web_mark: bool = False,
|
660
|
+
think: bool = False,
|
661
|
+
stream: bool = False,
|
662
|
+
history_max_char: int | None = None,
|
663
|
+
history_max_time: float | None = None
|
664
|
+
) -> ChatRecord | tuple[ChatRecord, ChatReplyGenerator] | tuple[ChatRecord, ChatReplyGenerator, ChatThinkGenerator]:
|
665
|
+
"""
|
666
|
+
Chat with AI.
|
667
|
+
|
668
|
+
Parameters
|
669
|
+
----------
|
670
|
+
text : User chat text.
|
671
|
+
index : Chat records index.
|
672
|
+
`None`: Not use record.
|
673
|
+
system : Extra AI system description, will be connected to `self.system`.
|
674
|
+
web : Whether use web search.
|
675
|
+
web_mark : Whether display web search citation mark, format is `[ref_<number>]`.
|
676
|
+
think : Whether use deep think, when is `True`, then parameter `stream` must also be `True`.
|
677
|
+
stream : Whether use stream response, record after full return values.
|
678
|
+
history_max_char : History messages record maximum character count.
|
679
|
+
- `None`: Use `self.history_max_char`.
|
680
|
+
history_max_time : History messages record maximum second.
|
681
|
+
- `None`: Use `self.history_max_time`.
|
682
|
+
|
683
|
+
Returns
|
684
|
+
-------
|
685
|
+
Response content.
|
686
|
+
"""
|
687
|
+
|
688
|
+
# Check.
|
689
|
+
if text == '':
|
690
|
+
throw(ValueError, text)
|
691
|
+
if think and not stream:
|
692
|
+
throw(ValueError, think, stream)
|
693
|
+
|
694
|
+
# Parameter.
|
695
|
+
if (
|
696
|
+
system is not None
|
697
|
+
and self.system is not None
|
698
|
+
):
|
699
|
+
system = ''.join([self.system, system])
|
700
|
+
elif system is None:
|
701
|
+
system = self.system
|
702
|
+
json = {'input': {}, 'parameters': {}}
|
703
|
+
|
704
|
+
## History.
|
705
|
+
if index is not None:
|
706
|
+
chat_records_history = self.get_chat_records_history(index, history_max_char, history_max_time, True)
|
707
|
+
else:
|
708
|
+
chat_records_history: ChatRecords = []
|
709
|
+
|
710
|
+
### Role.
|
711
|
+
if system is not None:
|
712
|
+
chat_record_role: ChatRecord = {
|
713
|
+
'time': now('timestamp'),
|
714
|
+
'role': 'system',
|
715
|
+
'content': system,
|
716
|
+
'len': len(system),
|
717
|
+
'token': None,
|
718
|
+
'web': None,
|
719
|
+
'think': None
|
720
|
+
}
|
721
|
+
chat_records_role: ChatRecords = [chat_record_role]
|
722
|
+
else:
|
723
|
+
chat_records_role: ChatRecords = []
|
724
|
+
|
725
|
+
### Now.
|
726
|
+
chat_record_now: ChatRecord= {
|
727
|
+
'time': now('timestamp'),
|
728
|
+
'role': 'user',
|
729
|
+
'content': text,
|
730
|
+
'len': len(text),
|
731
|
+
'token': None,
|
732
|
+
'web': None,
|
733
|
+
'think': None
|
734
|
+
}
|
735
|
+
chat_records_now: ChatRecords = [chat_record_now]
|
736
|
+
|
737
|
+
messages: ChatRecords = chat_records_role + chat_records_history + chat_records_now
|
738
|
+
messages = [
|
739
|
+
{
|
740
|
+
'role': message['role'],
|
741
|
+
'content': message['content']
|
742
|
+
}
|
743
|
+
for message in messages
|
744
|
+
]
|
745
|
+
|
746
|
+
## Database.
|
747
|
+
self.db_record['messages'] = messages
|
748
|
+
self.db_record['model'] = self.model
|
749
|
+
|
750
|
+
## Message.
|
751
|
+
json['input']['messages'] = messages
|
752
|
+
json['parameters']['result_format'] = 'message'
|
753
|
+
|
754
|
+
## Web.
|
755
|
+
if web:
|
756
|
+
json['parameters']['enable_search'] = True
|
757
|
+
json['parameters']['search_options'] = {
|
758
|
+
'enable_source': True,
|
759
|
+
'enable_citation': web_mark,
|
760
|
+
'citation_format': '[ref_<number>]',
|
761
|
+
'forced_search': False,
|
762
|
+
'search_strategy': 'max',
|
763
|
+
'prepend_search_result': False,
|
764
|
+
'enable_search_extension': True
|
765
|
+
}
|
766
|
+
else:
|
767
|
+
json['parameters']['enable_search'] = False
|
768
|
+
|
769
|
+
## Think.
|
770
|
+
json['parameters']['enable_thinking'] = think
|
771
|
+
|
772
|
+
## Stream.
|
773
|
+
json['stream'] = stream
|
774
|
+
|
775
|
+
# Request.
|
776
|
+
self.db_record['request_time'] = now()
|
777
|
+
response = self.request(json, stream)
|
778
|
+
self.db_record['response_time'] = now()
|
779
|
+
|
780
|
+
# Extract.
|
781
|
+
|
782
|
+
## Stream.
|
783
|
+
if stream:
|
784
|
+
response_iter: Iterable[str] = response
|
785
|
+
chat_record_reply, generator_text, generator_think = self.extract_response_generator(response_iter)
|
786
|
+
|
787
|
+
## Not Stream.
|
788
|
+
else:
|
789
|
+
response_json: dict = response
|
790
|
+
chat_record_reply = self.extract_response_record(response_json)
|
791
|
+
|
792
|
+
# Record.
|
793
|
+
if index is not None:
|
794
|
+
chat_records_history.append(chat_record_now)
|
795
|
+
chat_records_history.append(chat_record_reply)
|
796
|
+
|
797
|
+
# Return.
|
798
|
+
if stream:
|
799
|
+
if think:
|
800
|
+
return chat_record_reply, generator_text, generator_think
|
801
|
+
else:
|
802
|
+
return chat_record_reply, generator_text
|
803
|
+
else:
|
804
|
+
|
805
|
+
## Database.
|
806
|
+
self.insert_db(chat_record_reply)
|
807
|
+
|
808
|
+
return chat_record_reply
|
809
|
+
|
810
|
+
|
811
|
+
def polish(self, text: str) -> str:
|
812
|
+
"""
|
813
|
+
Let AI polish text.
|
814
|
+
|
815
|
+
Parameters
|
816
|
+
----------
|
817
|
+
text : Text.
|
818
|
+
|
819
|
+
Returns
|
820
|
+
-------
|
821
|
+
Polished text.
|
822
|
+
"""
|
823
|
+
|
824
|
+
# Parameter.
|
825
|
+
text = '润色冒号后的内容(注意!只返回润色后的内容正文,之后会直接整段使用):' + text
|
826
|
+
record = self.chat(text)
|
827
|
+
result: str = record['content']
|
828
|
+
result = result.strip()
|
829
|
+
|
830
|
+
return result
|
831
|
+
|
832
|
+
|
833
|
+
def build_db(self) -> None:
|
834
|
+
"""
|
835
|
+
Check and build database tables, by `self.db_names`.
|
836
|
+
"""
|
837
|
+
|
838
|
+
# Check.
|
839
|
+
if self.db is None:
|
840
|
+
throw(ValueError, self.db)
|
841
|
+
|
842
|
+
# Parameter.
|
843
|
+
|
844
|
+
## Table.
|
845
|
+
tables = [DatabaseTableAliQwen]
|
846
|
+
DatabaseTableAliQwen._set_name(self.db_names['ali_qwen'])
|
847
|
+
|
848
|
+
## View stats.
|
849
|
+
views_stats = [
|
850
|
+
{
|
851
|
+
'path': self.db_names['stats_ali_qwen'],
|
852
|
+
'items': [
|
853
|
+
{
|
854
|
+
'name': 'count',
|
855
|
+
'select': (
|
856
|
+
'SELECT COUNT(1)\n'
|
857
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
858
|
+
),
|
859
|
+
'comment': 'Request count.'
|
860
|
+
},
|
861
|
+
{
|
862
|
+
'name': 'past_day_count',
|
863
|
+
'select': (
|
864
|
+
'SELECT COUNT(1)\n'
|
865
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
866
|
+
'WHERE TIMESTAMPDIFF(DAY, `request_time`, NOW()) = 0'
|
867
|
+
),
|
868
|
+
'comment': 'Request count in the past day.'
|
869
|
+
},
|
870
|
+
{
|
871
|
+
'name': 'past_week_count',
|
872
|
+
'select': (
|
873
|
+
'SELECT COUNT(1)\n'
|
874
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
875
|
+
'WHERE TIMESTAMPDIFF(DAY, `request_time`, NOW()) <= 6'
|
876
|
+
),
|
877
|
+
'comment': 'Request count in the past week.'
|
878
|
+
},
|
879
|
+
{
|
880
|
+
'name': 'past_month_count',
|
881
|
+
'select': (
|
882
|
+
'SELECT COUNT(1)\n'
|
883
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
884
|
+
'WHERE TIMESTAMPDIFF(DAY, `request_time`, NOW()) <= 29'
|
885
|
+
),
|
886
|
+
'comment': 'Request count in the past month.'
|
887
|
+
},
|
888
|
+
{
|
889
|
+
'name': 'total_token',
|
890
|
+
'select': (
|
891
|
+
'SELECT FORMAT(SUM(`token_total`), 0)\n'
|
892
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
893
|
+
),
|
894
|
+
'comment': 'Usage total Token.'
|
895
|
+
},
|
896
|
+
{
|
897
|
+
'name': 'total_token_input',
|
898
|
+
'select': (
|
899
|
+
'SELECT FORMAT(SUM(`token_input`), 0)\n'
|
900
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
901
|
+
),
|
902
|
+
'comment': 'Usage input total Token.'
|
903
|
+
},
|
904
|
+
{
|
905
|
+
'name': 'total_token_output',
|
906
|
+
'select': (
|
907
|
+
'SELECT FORMAT(SUM(`token_output`), 0)\n'
|
908
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
909
|
+
),
|
910
|
+
'comment': 'Usage output total Token.'
|
911
|
+
},
|
912
|
+
{
|
913
|
+
'name': 'total_token_output_think',
|
914
|
+
'select': (
|
915
|
+
'SELECT FORMAT(SUM(`token_output_think`), 0)\n'
|
916
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
917
|
+
),
|
918
|
+
'comment': 'Usage output think total Token.'
|
919
|
+
},
|
920
|
+
{
|
921
|
+
'name': 'avg_token',
|
922
|
+
'select': (
|
923
|
+
'SELECT FORMAT(AVG(`token_total`), 0)\n'
|
924
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
925
|
+
),
|
926
|
+
'comment': 'Usage average Token.'
|
927
|
+
},
|
928
|
+
{
|
929
|
+
'name': 'avg_token_input',
|
930
|
+
'select': (
|
931
|
+
'SELECT FORMAT(AVG(`token_input`), 0)\n'
|
932
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
933
|
+
),
|
934
|
+
'comment': 'Usage input average Token.'
|
935
|
+
},
|
936
|
+
{
|
937
|
+
'name': 'avg_token_output',
|
938
|
+
'select': (
|
939
|
+
'SELECT FORMAT(AVG(`token_output`), 0)\n'
|
940
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
941
|
+
),
|
942
|
+
'comment': 'Usage output average Token.'
|
943
|
+
},
|
944
|
+
{
|
945
|
+
'name': 'avg_token_output_think',
|
946
|
+
'select': (
|
947
|
+
'SELECT FORMAT(AVG(`token_output_think`), 0)\n'
|
948
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
949
|
+
),
|
950
|
+
'comment': 'Usage output think average Token.'
|
951
|
+
},
|
952
|
+
{
|
953
|
+
'name': 'last_time',
|
954
|
+
'select': (
|
955
|
+
'SELECT MAX(`request_time`)\n'
|
956
|
+
f'FROM `{self.db.database}`.`{self.db_names['ali_qwen']}`'
|
957
|
+
),
|
958
|
+
'comment': 'Last record request time.'
|
959
|
+
}
|
960
|
+
]
|
961
|
+
}
|
962
|
+
]
|
963
|
+
|
964
|
+
# Build.
|
965
|
+
self.db.build.build(tables=tables, views_stats=views_stats, skip=True)
|
966
|
+
|
967
|
+
|
968
|
+
def insert_db(self, record: ChatRecord) -> None:
|
969
|
+
"""
|
970
|
+
Insert record to table of database.
|
971
|
+
|
972
|
+
Parameters
|
973
|
+
----------
|
974
|
+
record : Record data.
|
975
|
+
"""
|
976
|
+
|
977
|
+
# Parameter.
|
978
|
+
self.db_record['reply'] = record['content']
|
979
|
+
self.db_record['think'] = record['think']
|
980
|
+
self.db_record['web'] = record['web']
|
981
|
+
self.db_record['token_total'] = record['token']['total']
|
982
|
+
self.db_record['token_input'] = record['token']['input']
|
983
|
+
self.db_record['token_output'] = record['token']['output']
|
984
|
+
self.db_record['token_output_think'] = record['token']['output_think']
|
985
|
+
|
986
|
+
# Insert.
|
987
|
+
self.db_record.record()
|
988
|
+
|
989
|
+
|
990
|
+
__call__ = chat
|