XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Support synthesize in Openai generator (#2394)

* Improve download of generated images, serve images in the api * Add support for conversation handling in the api * Add orginal prompt to image response * Add download images option in gui, fix loading model list in Airforce * Support speech synthesize in Openai generator

eae317a1
H Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

8 个文件 +259 -76
Modified g4f/Provider/needs_auth/OpenaiChat.py +46 -18
@@ -7,7 +7,6 @@ import json
7 7 import base64
8 8 import time
9 9 import requests
10 from aiohttp import ClientWebSocketResponse
11 10 from copy import copy
12 11
13 12 try:
@@ -28,7 +27,7 @@ from ...requests.raise_for_status import raise_for_status
28 27 from ...requests.aiohttp import StreamSession
29 28 from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
30 29 from ...errors import MissingAuthError, ResponseError
31 from ...providers.response import BaseConversation
30 from ...providers.response import BaseConversation, FinishReason, SynthesizeData
32 31 from ..helper import format_cookies
33 32 from ..openai.har_file import get_request_config, NoValidHarFileError
34 33 from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, backend_anon_url
@@ -367,19 +366,13 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
367 366 Raises:
368 367 RuntimeError: If an error occurs during processing.
369 368 """
369 await cls.login(proxy)
370
370 371 async with StreamSession(
371 372 proxy=proxy,
372 373 impersonate="chrome",
373 374 timeout=timeout
374 375 ) as session:
375 if cls._expires is not None and cls._expires < time.time():
376 cls._headers = cls._api_key = None
377 try:
378 await get_request_config(proxy)
379 cls._create_request_args(RequestConfig.cookies, RequestConfig.headers)
380 cls._set_api_key(RequestConfig.access_token)
381 except NoValidHarFileError as e:
382 await cls.nodriver_auth(proxy)
383 376 try:
384 377 image_request = await cls.upload_image(session, cls._headers, image, image_name) if image else None
385 378 except Exception as e:
@@ -469,18 +462,25 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
469 462 await asyncio.sleep(5)
470 463 continue
471 464 await raise_for_status(response)
472 async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, conversation):
473 if return_conversation:
474 history_disabled = False
475 return_conversation = False
476 yield conversation
477 yield chunk
465 if return_conversation:
466 history_disabled = False
467 yield conversation
468 async for line in response.iter_lines():
469 async for chunk in cls.iter_messages_line(session, line, conversation):
470 yield chunk
471 if not history_disabled:
472 yield SynthesizeData(cls.__name__, {
473 "conversation_id": conversation.conversation_id,
474 "message_id": conversation.message_id,
475 "voice": "maple",
476 })
478 477 if auto_continue and conversation.finish_reason == "max_tokens":
479 478 conversation.finish_reason = None
480 479 action = "continue"
481 480 await asyncio.sleep(5)
482 481 else:
483 482 break
483 yield FinishReason(conversation.finish_reason)
484 484 if history_disabled and auto_continue:
485 485 await cls.delete_conversation(session, cls._headers, conversation.conversation_id)
486 486
@@ -541,10 +541,38 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
541 541 if "error" in line and line.get("error"):
542 542 raise RuntimeError(line.get("error"))
543 543
544 @classmethod
545 async def synthesize(cls, params: dict) -> AsyncIterator[bytes]:
546 await cls.login()
547 async with StreamSession(
548 impersonate="chrome",
549 timeout=900
550 ) as session:
551 async with session.get(
552 f"{cls.url}/backend-api/synthesize",
553 params=params,
554 headers=cls._headers
555 ) as response:
556 await raise_for_status(response)
557 async for chunk in response.iter_content():
558 yield chunk
559
560 @classmethod
561 async def login(cls, proxy: str = None):
562 if cls._expires is not None and cls._expires < time.time():
563 cls._headers = cls._api_key = None
564 try:
565 await get_request_config(proxy)
566 cls._create_request_args(RequestConfig.cookies, RequestConfig.headers)
567 cls._set_api_key(RequestConfig.access_token)
568 except NoValidHarFileError:
569 if has_nodriver:
570 await cls.nodriver_auth(proxy)
571 else:
572 raise
573
544 574 @classmethod
545 575 async def nodriver_auth(cls, proxy: str = None):
546 if not has_nodriver:
547 return
548 576 if has_platformdirs:
549 577 user_data_dir = user_config_dir("g4f-nodriver")
550 578 else:
Modified g4f/client/__init__.py +5 -1
@@ -13,7 +13,7 @@ from ..providers.base_provider import AsyncGeneratorProvider
13 13 from ..image import ImageResponse, copy_images, images_dir
14 14 from ..typing import Messages, Image, ImageType
15 15 from ..providers.types import ProviderType
16 from ..providers.response import ResponseType, FinishReason, BaseConversation
16 from ..providers.response import ResponseType, FinishReason, BaseConversation, SynthesizeData
17 17 from ..errors import NoImageResponseError, ModelNotFoundError
18 18 from ..providers.retry_provider import IterListProvider
19 19 from ..providers.base_provider import get_running_loop
@@ -60,6 +60,8 @@ def iter_response(
60 60 elif isinstance(chunk, BaseConversation):
61 61 yield chunk
62 62 continue
63 elif isinstance(chunk, SynthesizeData):
64 continue
63 65
64 66 chunk = str(chunk)
65 67 content += chunk
@@ -121,6 +123,8 @@ async def async_iter_response(
121 123 elif isinstance(chunk, BaseConversation):
122 124 yield chunk
123 125 continue
126 elif isinstance(chunk, SynthesizeData):
127 continue
124 128
125 129 chunk = str(chunk)
126 130 content += chunk
Modified g4f/gui/client/static/css/style.css +20 -4
@@ -259,7 +259,6 @@ body {
259 259 flex-direction: column;
260 260 gap: var(--section-gap);
261 261 padding: var(--inner-gap) var(--section-gap);
262 padding-bottom: 0;
263 262 }
264 263
265 264 .message.print {
@@ -271,7 +270,11 @@ body {
271 270 }
272 271
273 272 .message.regenerate {
274 opacity: 0.75;
273 background-color: var(--colour-6);
274 }
275
276 .white .message.regenerate {
277 background-color: var(--colour-4);
275 278 }
276 279
277 280 .message:last-child {
@@ -407,6 +410,7 @@ body {
407 410
408 411 .message .count .fa-clipboard.clicked,
409 412 .message .count .fa-print.clicked,
413 .message .count .fa-rotate.clicked,
410 414 .message .count .fa-volume-high.active {
411 415 color: var(--accent);
412 416 }
@@ -430,6 +434,15 @@ body {
430 434 font-size: 12px;
431 435 }
432 436
437 .message audio {
438 display: none;
439 max-width: 400px;
440 }
441
442 .message audio.show {
443 display: block;
444 }
445
433 446 .count_total {
434 447 font-size: 12px;
435 448 padding-left: 25px;
@@ -1159,7 +1172,10 @@ a:-webkit-any-link {
1159 1172 .message .user {
1160 1173 display: none;
1161 1174 }
1162 .message.regenerate {
1163 opacity: 1;
1175 body {
1176 height: auto;
1177 }
1178 .box {
1179 backdrop-filter: none;
1164 1180 }
1165 1181 }
Modified g4f/gui/client/static/js/chat.v1.js +120 -49
@@ -28,6 +28,7 @@ let message_storage = {};
28 28 let controller_storage = {};
29 29 let content_storage = {};
30 30 let error_storage = {};
31 let synthesize_storage = {};
31 32
32 33 messageInput.addEventListener("blur", () => {
33 34 window.scrollTo(0, 0);
@@ -134,6 +135,13 @@ const register_message_buttons = async () => {
134 135 if (!("click" in el.dataset)) {
135 136 el.dataset.click = "true";
136 137 el.addEventListener("click", async () => {
138 const content_el = el.parentElement.parentElement;
139 const audio = content_el.querySelector("audio");
140 if (audio) {
141 audio.classList.add("show");
142 audio.play();
143 return;
144 }
137 145 let playlist = [];
138 146 function play_next() {
139 147 const next = playlist.shift();
@@ -155,7 +163,6 @@ const register_message_buttons = async () => {
155 163 el.dataset.running = true;
156 164 el.classList.add("blink")
157 165 el.classList.add("active")
158 const content_el = el.parentElement.parentElement;
159 166 const message_el = content_el.parentElement;
160 167 let speechText = await get_message(window.conversation_id, message_el.dataset.index);
161 168
@@ -215,8 +222,8 @@ const register_message_buttons = async () => {
215 222 const message_el = el.parentElement.parentElement.parentElement;
216 223 el.classList.add("clicked");
217 224 setTimeout(() => el.classList.remove("clicked"), 1000);
218 await ask_gpt(message_el.dataset.index, get_message_id());
219 })
225 await ask_gpt(get_message_id(), message_el.dataset.index);
226 });
220 227 }
221 228 });
222 229 document.querySelectorAll(".message .fa-whatsapp").forEach(async (el) => {
@@ -301,25 +308,29 @@ const handle_ask = async () => {
301 308 <i class="fa-regular fa-clipboard"></i>
302 309 <a><i class="fa-brands fa-whatsapp"></i></a>
303 310 <i class="fa-solid fa-print"></i>
311 <i class="fa-solid fa-rotate"></i>
304 312 </div>
305 313 </div>
306 314 </div>
307 315 `;
308 316 highlight(message_box);
309 stop_generating.classList.remove("stop_generating-hidden");
310 await ask_gpt(-1, message_id);
317 await ask_gpt(message_id);
311 318 };
312 319
313 async function remove_cancel_button() {
320 async function safe_remove_cancel_button() {
321 for (let key in controller_storage) {
322 if (!controller_storage[key].signal.aborted) {
323 return;
324 }
325 }
314 326 stop_generating.classList.add("stop_generating-hidden");
315 327 }
316 328
317 329 regenerate.addEventListener("click", async () => {
318 330 regenerate.classList.add("regenerate-hidden");
319 331 setTimeout(()=>regenerate.classList.remove("regenerate-hidden"), 3000);
320 stop_generating.classList.remove("stop_generating-hidden");
321 332 await hide_message(window.conversation_id);
322 await ask_gpt(-1, get_message_id());
333 await ask_gpt(get_message_id());
323 334 });
324 335
325 336 stop_generating.addEventListener("click", async () => {
@@ -337,21 +348,21 @@ stop_generating.addEventListener("click", async () => {
337 348 }
338 349 }
339 350 }
340 await load_conversation(window.conversation_id);
351 await load_conversation(window.conversation_id, false);
341 352 });
342 353
343 354 const prepare_messages = (messages, message_index = -1) => {
355 if (message_index >= 0) {
356 messages = messages.filter((_, index) => message_index >= index);
357 }
358
344 359 // Removes none user messages at end
345 if (message_index == -1) {
346 let last_message;
347 while (last_message = messages.pop()) {
348 if (last_message["role"] == "user") {
349 messages.push(last_message);
350 break;
351 }
360 let last_message;
361 while (last_message = messages.pop()) {
362 if (last_message["role"] == "user") {
363 messages.push(last_message);
364 break;
352 365 }
353 } else if (message_index >= 0) {
354 messages = messages.filter((_, index) => message_index >= index);
355 366 }
356 367
357 368 let new_messages = [];
@@ -377,9 +388,11 @@ const prepare_messages = (messages, message_index = -1) => {
377 388 // Remove generated images from history
378 389 new_message.content = filter_message(new_message.content);
379 390 delete new_message.provider;
391 delete new_message.synthesize;
380 392 new_messages.push(new_message)
381 393 }
382 394 });
395
383 396 return new_messages;
384 397 }
385 398
@@ -427,6 +440,8 @@ async function add_message_chunk(message, message_id) {
427 440 let p = document.createElement("p");
428 441 p.innerText = message.log;
429 442 log_storage.appendChild(p);
443 } else if (message.type == "synthesize") {
444 synthesize_storage[message_id] = message.synthesize;
430 445 }
431 446 let scroll_down = ()=>{
432 447 if (message_box.scrollTop >= message_box.scrollHeight - message_box.clientHeight - 100) {
@@ -434,8 +449,10 @@ async function add_message_chunk(message, message_id) {
434 449 message_box.scrollTo({ top: message_box.scrollHeight, behavior: "auto" });
435 450 }
436 451 }
437 setTimeout(scroll_down, 200);
438 setTimeout(scroll_down, 1000);
452 if (!content_map.container.classList.contains("regenerate")) {
453 scroll_down();
454 setTimeout(scroll_down, 200);
455 }
439 456 }
440 457
441 458 cameraInput?.addEventListener("click", (e) => {
@@ -452,45 +469,58 @@ imageInput?.addEventListener("click", (e) => {
452 469 }
453 470 });
454 471
455 const ask_gpt = async (message_index = -1, message_id) => {
472 const ask_gpt = async (message_id, message_index = -1) => {
456 473 let messages = await get_messages(window.conversation_id);
457 let total_messages = messages.length;
458 474 messages = prepare_messages(messages, message_index);
459 message_index = total_messages
460 475 message_storage[message_id] = "";
461 stop_generating.classList.remove(".stop_generating-hidden");
476 stop_generating.classList.remove("stop_generating-hidden");
462 477
463 message_box.scrollTop = message_box.scrollHeight;
464 window.scrollTo(0, 0);
478 if (message_index == -1) {
479 await scroll_to_bottom();
480 }
465 481
466 482 let count_total = message_box.querySelector('.count_total');
467 483 count_total ? count_total.parentElement.removeChild(count_total) : null;
468 484
469 message_box.innerHTML += `
470 <div class="message" data-index="${message_index}">
471 <div class="assistant">
472 ${gpt_image}
473 <i class="fa-solid fa-xmark"></i>
474 <i class="fa-regular fa-phone-arrow-down-left"></i>
475 </div>
476 <div class="content" id="gpt_${message_id}">
477 <div class="provider"></div>
478 <div class="content_inner"><span class="cursor"></span></div>
479 <div class="count"></div>
480 </div>
485 const message_el = document.createElement("div");
486 message_el.classList.add("message");
487 if (message_index != -1) {
488 message_el.classList.add("regenerate");
489 }
490 message_el.innerHTML += `
491 <div class="assistant">
492 ${gpt_image}
493 <i class="fa-solid fa-xmark"></i>
494 <i class="fa-regular fa-phone-arrow-down-left"></i>
495 </div>
496 <div class="content" id="gpt_${message_id}">
497 <div class="provider"></div>
498 <div class="content_inner"><span class="cursor"></span></div>
499 <div class="count"></div>
481 500 </div>
482 501 `;
502 if (message_index == -1) {
503 message_box.appendChild(message_el);
504 } else {
505 parent_message = message_box.querySelector(`.message[data-index="${message_index}"]`);
506 if (!parent_message) {
507 return;
508 }
509 parent_message.after(message_el);
510 }
483 511
484 512 controller_storage[message_id] = new AbortController();
485 513
486 514 let content_el = document.getElementById(`gpt_${message_id}`)
487 515 let content_map = content_storage[message_id] = {
516 container: message_el,
488 517 content: content_el,
489 518 inner: content_el.querySelector('.content_inner'),
490 519 count: content_el.querySelector('.count'),
491 520 }
492
493 await scroll_to_bottom();
521 if (message_index == -1) {
522 await scroll_to_bottom();
523 }
494 524 try {
495 525 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput;
496 526 const file = input && input.files.length > 0 ? input.files[0] : null;
@@ -527,14 +557,23 @@ const ask_gpt = async (message_index = -1, message_id) => {
527 557 delete controller_storage[message_id];
528 558 if (!error_storage[message_id] && message_storage[message_id]) {
529 559 const message_provider = message_id in provider_storage ? provider_storage[message_id] : null;
530 await add_message(window.conversation_id, "assistant", message_storage[message_id], message_provider);
531 await safe_load_conversation(window.conversation_id);
560 await add_message(
561 window.conversation_id,
562 "assistant",
563 message_storage[message_id],
564 message_provider,
565 message_index,
566 synthesize_storage[message_id]
567 );
568 await safe_load_conversation(window.conversation_id, message_index == -1);
532 569 } else {
533 let cursorDiv = message_box.querySelector(".cursor");
570 let cursorDiv = message_el.querySelector(".cursor");
534 571 if (cursorDiv) cursorDiv.parentNode.removeChild(cursorDiv);
535 572 }
536 await scroll_to_bottom();
537 await remove_cancel_button();
573 if (message_index == -1) {
574 await scroll_to_bottom();
575 }
576 await safe_remove_cancel_button();
538 577 await register_message_buttons();
539 578 await load_conversations();
540 579 regenerate.classList.remove("regenerate-hidden");
@@ -687,6 +726,15 @@ const load_conversation = async (conversation_id, scroll=true) => {
687 726 ${item.provider.model ? ' with ' + item.provider.model : ''}
688 727 </div>
689 728 ` : "";
729 let audio = "";
730 if (item.synthesize) {
731 const synthesize_params = (new URLSearchParams(item.synthesize.data)).toString();
732 audio = `
733 <audio controls preload="none">
734 <source src="/backend-api/v2/synthesize/${item.synthesize.provider}?${synthesize_params}" type="audio/mpeg">
735 </audio>
736 `;
737 }
690 738 elements += `
691 739 <div class="message${item.regenerate ? " regenerate": ""}" data-index="${i}">
692 740 <div class="${item.role}">
@@ -700,12 +748,14 @@ const load_conversation = async (conversation_id, scroll=true) => {
700 748 <div class="content">
701 749 ${provider}
702 750 <div class="content_inner">${markdown_render(item.content)}</div>
751 ${audio}
703 752 <div class="count">
704 753 ${count_words_and_tokens(item.content, next_provider?.model)}
705 754 <i class="fa-solid fa-volume-high"></i>
706 755 <i class="fa-regular fa-clipboard"></i>
707 756 <a><i class="fa-brands fa-whatsapp"></i></a>
708 757 <i class="fa-solid fa-print"></i>
758 <i class="fa-solid fa-rotate"></i>
709 759 </div>
710 760 </div>
711 761 </div>
@@ -830,14 +880,35 @@ const get_message = async (conversation_id, index) => {
830 880 return messages[index]["content"];
831 881 };
832 882
833 const add_message = async (conversation_id, role, content, provider) => {
883 const add_message = async (
884 conversation_id, role, content,
885 provider = null,
886 message_index = -1,
887 synthesize_data = null
888 ) => {
834 889 const conversation = await get_conversation(conversation_id);
835 890 if (!conversation) return;
836 conversation.items.push({
891 const new_message = {
837 892 role: role,
838 893 content: content,
839 provider: provider
840 });
894 provider: provider,
895 };
896 if (synthesize_data) {
897 new_message.synthesize = synthesize_data;
898 }
899 if (message_index == -1) {
900 conversation.items.push(new_message);
901 } else {
902 const new_messages = [];
903 conversation.items.forEach((item, index)=>{
904 new_messages.push(item);
905 if (index == message_index) {
906 new_message.regenerate = true;
907 new_messages.push(new_message);
908 }
909 });
910 conversation.items = new_messages;
911 }
841 912 await save_conversation(conversation_id, conversation);
842 913 return conversation.items.length - 1;
843 914 };
Modified g4f/gui/server/api.py +3 -1
@@ -13,7 +13,7 @@ from g4f.errors import VersionNotFoundError
13 13 from g4f.image import ImagePreview, ImageResponse, copy_images, ensure_images_dir, images_dir
14 14 from g4f.Provider import ProviderType, __providers__, __map__
15 15 from g4f.providers.base_provider import ProviderModelMixin
16 from g4f.providers.response import BaseConversation, FinishReason
16 from g4f.providers.response import BaseConversation, FinishReason, SynthesizeData
17 17 from g4f.client.service import convert_to_provider
18 18 from g4f import debug
19 19
@@ -177,6 +177,8 @@ class Api:
177 177 images = asyncio.run(copy_images(chunk.get_list(), chunk.options.get("cookies")))
178 178 images = ImageResponse(images, chunk.alt)
179 179 yield self._format_json("content", str(images))
180 elif isinstance(chunk, SynthesizeData):
181 yield self._format_json("synthesize", chunk.to_json())
180 182 elif not isinstance(chunk, FinishReason):
181 183 yield self._format_json("content", str(chunk))
182 184 if debug.logs:
Modified g4f/gui/server/backend.py +50 -1
@@ -1,8 +1,36 @@
1 1 import json
2 import asyncio
3 import flask
2 4 from flask import request, Flask
5 from typing import AsyncGenerator, Generator
6
3 7 from g4f.image import is_allowed_extension, to_image
8 from g4f.client.service import convert_to_provider
9 from g4f.errors import ProviderNotFoundError
4 10 from .api import Api
5 11
12 def safe_iter_generator(generator: Generator) -> Generator:
13 start = next(generator)
14 def iter_generator():
15 yield start
16 yield from generator
17 return iter_generator()
18
19 def to_sync_generator(gen: AsyncGenerator) -> Generator:
20 loop = asyncio.new_event_loop()
21 asyncio.set_event_loop(loop)
22 gen = gen.__aiter__()
23 async def get_next():
24 try:
25 obj = await gen.__anext__()
26 return False, obj
27 except StopAsyncIteration: return True, None
28 while True:
29 done, obj = loop.run_until_complete(get_next())
30 if done:
31 break
32 yield obj
33
6 34 class Backend_Api(Api):
7 35 """
8 36 Handles various endpoints in a Flask application for backend operations.
@@ -47,6 +75,10 @@ class Backend_Api(Api):
47 75 'function': self.handle_conversation,
48 76 'methods': ['POST']
49 77 },
78 '/backend-api/v2/synthesize/<provider>': {
79 'function': self.handle_synthesize,
80 'methods': ['GET']
81 },
50 82 '/backend-api/v2/error': {
51 83 'function': self.handle_error,
52 84 'methods': ['POST']
@@ -98,11 +130,28 @@ class Backend_Api(Api):
98 130 mimetype='text/event-stream'
99 131 )
100 132
133 def handle_synthesize(self, provider: str):
134 try:
135 provider_handler = convert_to_provider(provider)
136 except ProviderNotFoundError:
137 return "Provider not found", 404
138 if not hasattr(provider_handler, "synthesize"):
139 return "Provider doesn't support synthesize", 500
140 try:
141 response_generator = provider_handler.synthesize({**request.args})
142 if hasattr(response_generator, "__aiter__"):
143 response_generator = to_sync_generator(response_generator)
144 response = flask.Response(safe_iter_generator(response_generator), content_type="audio/mpeg")
145 response.headers['Cache-Control'] = "max-age=604800"
146 return response
147 except Exception as e:
148 return f"{e.__class__.__name__}: {e}", 500
149
101 150 def get_provider_models(self, provider: str):
102 151 api_key = None if request.authorization is None else request.authorization.token
103 152 models = super().get_provider_models(provider, api_key)
104 153 if models is None:
105 return 404, "Provider not found"
154 return "Provider not found", 404
106 155 return models
107 156
108 157 def _format_json(self, response_type: str, content) -> str:
Modified g4f/providers/base_provider.py +2 -2
@@ -11,7 +11,7 @@ from typing import Callable, Union
11 11
12 12 from ..typing import CreateResult, AsyncResult, Messages
13 13 from .types import BaseProvider
14 from .response import FinishReason, BaseConversation
14 from .response import FinishReason, BaseConversation, SynthesizeData
15 15 from ..errors import NestAsyncioError, ModelNotSupportedError
16 16 from .. import debug
17 17
@@ -259,7 +259,7 @@ class AsyncGeneratorProvider(AsyncProvider):
259 259 """
260 260 return "".join([
261 261 str(chunk) async for chunk in cls.create_async_generator(model, messages, stream=False, **kwargs)
262 if not isinstance(chunk, (Exception, FinishReason, BaseConversation))
262 if not isinstance(chunk, (Exception, FinishReason, BaseConversation, SynthesizeData))
263 263 ])
264 264
265 265 @staticmethod
Modified g4f/providers/response.py +13 -0
@@ -22,5 +22,18 @@ class Sources(ResponseType):
22 22 return "\n\n" + ("\n".join([f"{idx+1}. [{link['title']}]({link['url']})" for idx, link in enumerate(self.list)]))
23 23
24 24 class BaseConversation(ResponseType):
25 def __str__(self) -> str:
26 return ""
27
28 class SynthesizeData(ResponseType):
29 def __init__(self, provider: str, data: dict):
30 self.provider = provider
31 self.data = data
32
33 def to_json(self) -> dict:
34 return {
35 **self.__dict__
36 }
37
25 38 def __str__(self) -> str:
26 39 return ""