From 45e75099b95e7c74ff5b56f71b8a0baaf5c3b359 Mon Sep 17 00:00:00 2001 From: chyroc Date: Wed, 25 Sep 2024 11:10:09 +0800 Subject: [PATCH] refactor: refactor folder struct (#11) --- cozepy/__init__.py | 7 - cozepy/bot/__init__.py | 27 +++ cozepy/{bot.py => bot/v1/__init__.py} | 6 +- cozepy/chat/__init__.py | 20 ++ cozepy/chat/v3/__init__.py | 190 ++++++++++++++++++ cozepy/conversation/__init__.py | 18 ++ .../v1/__init__.py} | 10 +- cozepy/coze.py | 4 +- cozepy/workspace/__init__.py | 22 ++ .../v1/__init__.py} | 2 +- tests/test_auth.py | 2 +- tests/test_bot.py | 2 +- tests/test_chat.py | 8 +- 13 files changed, 293 insertions(+), 25 deletions(-) create mode 100644 cozepy/bot/__init__.py rename cozepy/{bot.py => bot/v1/__init__.py} (95%) create mode 100644 cozepy/chat/__init__.py create mode 100644 cozepy/chat/v3/__init__.py create mode 100644 cozepy/conversation/__init__.py rename cozepy/{conversation.py => conversation/v1/__init__.py} (79%) create mode 100644 cozepy/workspace/__init__.py rename cozepy/{workspace.py => workspace/v1/__init__.py} (92%) diff --git a/cozepy/__init__.py b/cozepy/__init__.py index 3c265ec..80e4246 100644 --- a/cozepy/__init__.py +++ b/cozepy/__init__.py @@ -11,8 +11,6 @@ MessageObjectString, Message, ) -from .conversation import Conversation -from .chat import Chat, ChatEvent, ChatIterator, Event __all__ = [ "ApplicationOAuth", @@ -29,9 +27,4 @@ "MessageObjectStringType", "MessageObjectString", "Message", - "Conversation", - "Chat", - "ChatEvent", - "ChatIterator", - "Event", ] diff --git a/cozepy/bot/__init__.py b/cozepy/bot/__init__.py new file mode 100644 index 0000000..d56005f --- /dev/null +++ b/cozepy/bot/__init__.py @@ -0,0 +1,27 @@ +from typing import TYPE_CHECKING + +from cozepy.auth import Auth +from cozepy.request import Requester + +if TYPE_CHECKING: + from .v1 import BotClient as BotClientV1 + + +class BotClient(object): + """ + Bot class. + """ + + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = base_url + self._auth = auth + self._requester = requester + self._v1 = None + + @property + def v1(self) -> "BotClientV1": + if not self._v1: + from .v1 import BotClient + + self._v1 = BotClient(self._base_url, self._auth, self._requester) + return self._v1 diff --git a/cozepy/bot.py b/cozepy/bot/v1/__init__.py similarity index 95% rename from cozepy/bot.py rename to cozepy/bot/v1/__init__.py index 0f239e2..8a65ddf 100644 --- a/cozepy/bot.py +++ b/cozepy/bot/v1/__init__.py @@ -105,7 +105,7 @@ def __init__(self, base_url: str, auth: Auth, requester: Requester): self._auth = auth self._requester = requester - def get_online_info_v1(self, *, bot_id: str) -> Bot: + def get_online_info(self, *, bot_id: str) -> Bot: """ Get the configuration information of the bot, which must have been published to the Bot as API channel. @@ -118,9 +118,7 @@ def get_online_info_v1(self, *, bot_id: str) -> Bot: return self._requester.request("get", url, Bot, params=params) - def list_published_bots_v1( - self, *, space_id: str, page_num: int = 1, page_size: int = 20 - ) -> NumberPaged[SimpleBot]: + def list(self, *, space_id: str, page_num: int = 1, page_size: int = 20) -> NumberPaged[SimpleBot]: """ Get the bots published as API service. diff --git a/cozepy/chat/__init__.py b/cozepy/chat/__init__.py new file mode 100644 index 0000000..5acffd7 --- /dev/null +++ b/cozepy/chat/__init__.py @@ -0,0 +1,20 @@ +from enum import Enum + +from cozepy.auth import Auth +from cozepy.request import Requester + + +class ChatClient(object): + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = base_url + self._auth = auth + self._requester = requester + self._v3 = None + + @property + def v3(self): + if not self._v3: + from .v3 import ChatClient + + self._v3 = ChatClient(self._base_url, self._auth, self._requester) + return self._v3 diff --git a/cozepy/chat/v3/__init__.py b/cozepy/chat/v3/__init__.py new file mode 100644 index 0000000..df5730f --- /dev/null +++ b/cozepy/chat/v3/__init__.py @@ -0,0 +1,190 @@ +import json +from enum import Enum +from typing import Dict, List, Iterator, Union + +from cozepy.auth import Auth +from cozepy.model import Message, Chat, CozeModel +from cozepy.request import Requester + + +class Event(str, Enum): + # Event for creating a conversation, indicating the start of the conversation. + # 创建对话的事件,表示对话开始。 + conversation_chat_created = "conversation.chat.created" + + # The server is processing the conversation. + # 服务端正在处理对话。 + conversation_chat_in_progress = "conversation.chat.in_progress" + + # Incremental message, usually an incremental message when type=answer. + # 增量消息,通常是 type=answer 时的增量消息。 + conversation_message_delta = "conversation.message.delta" + + # The message has been completely replied to. At this point, the streaming package contains the spliced results of all message.delta, and each message is in a completed state. + # message 已回复完成。此时流式包中带有所有 message.delta 的拼接结果,且每个消息均为 completed 状态。 + conversation_message_completed = "conversation.message.completed" + + # The conversation is completed. + # 对话完成。 + conversation_chat_completed = "conversation.chat.completed" + + # This event is used to mark a failed conversation. + # 此事件用于标识对话失败。 + conversation_chat_failed = "conversation.chat.failed" + + # The conversation is interrupted and requires the user to report the execution results of the tool. + # 对话中断,需要使用方上报工具的执行结果。 + conversation_chat_requires_action = "conversation.chat.requires_action" + + # Error events during the streaming response process. For detailed explanations of code and msg, please refer to Error codes. + # 流式响应过程中的错误事件。关于 code 和 msg 的详细说明,可参考错误码。 + error = "error" + + # The streaming response for this session ended normally. + # 本次会话的流式返回正常结束。 + done = "done" + + +class ChatEvent(CozeModel): + event: Event + chat: Chat = None + message: Message = None + + +class ChatIterator(object): + def __init__(self, iters: Iterator[bytes]): + self._iters = iters + + def __iter__(self): + return self + + def __next__(self) -> ChatEvent: + event = "" + data = "" + line = "" + times = 0 + + while times < 2: + line = next(self._iters).decode("utf-8") + if line == "": + continue + elif line.startswith("event:"): + if event == "": + event = line[6:] + else: + raise Exception(f"invalid event: {line}") + elif line.startswith("data:"): + if data == "": + data = line[5:] + else: + raise Exception(f"invalid event: {line}") + else: + raise Exception(f"invalid event: {line}") + + times += 1 + + if event == Event.done: + raise StopIteration + elif event == Event.error: + raise Exception(f"error event: {line}") + elif event in [Event.conversation_message_delta, Event.conversation_message_completed]: + return ChatEvent(event=event, message=Message.model_validate(json.loads(data))) + elif event in [ + Event.conversation_chat_created, + Event.conversation_chat_in_progress, + Event.conversation_chat_completed, + Event.conversation_chat_failed, + Event.conversation_chat_requires_action, + ]: + return ChatEvent(event=event, chat=Chat.model_validate(json.loads(data))) + else: + raise Exception(f"unknown event: {line}") + + +class ChatClient(object): + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = base_url + self._auth = auth + self._requester = requester + + def create( + self, + *, + bot_id: str, + user_id: str, + additional_messages: List[Message] = None, + stream: bool = False, + custom_variables: Dict[str, str] = None, + auto_save_history: bool = True, + meta_data: Dict[str, str] = None, + conversation_id: str = None, + ) -> Union[Chat, ChatIterator]: + """ + Create a conversation. + Conversation is an interaction between a bot and a user, including one or more messages. + """ + url = f"{self._base_url}/v3/chat" + body = { + "bot_id": bot_id, + "user_id": user_id, + "additional_messages": [i.model_dump() for i in additional_messages] if additional_messages else [], + "stream": stream, + "custom_variables": custom_variables, + "auto_save_history": auto_save_history, + "conversation_id": conversation_id if conversation_id else None, + "meta_data": meta_data, + } + if not stream: + return self._requester.request("post", url, Chat, body=body, stream=stream) + + return ChatIterator(self._requester.request("post", url, Chat, body=body, stream=stream)) + + def get( + self, + *, + conversation_id: str, + chat_id: str, + ) -> Chat: + """ + Create a conversation. + Conversation is an interaction between a bot and a user, including one or more messages. + """ + url = f"{self._base_url}/v3/chat/retrieve" + params = { + "conversation_id": conversation_id, + "chat_id": chat_id, + } + return self._requester.request("post", url, Chat, params=params) + + def list_message( + self, + *, + conversation_id: str, + chat_id: str, + ) -> List[Message]: + """ + Create a conversation. + Conversation is an interaction between a bot and a user, including one or more messages. + """ + url = f"{self._base_url}/v3/chat/message/list" + params = { + "conversation_id": conversation_id, + "chat_id": chat_id, + } + return self._requester.request("post", url, List[Message], params=params) + + def cancel_v3( + self, + *, + conversation_id: str, + chat_id: str, + ) -> Chat: + """ + Call this API to cancel an ongoing chat. + """ + url = f"{self._base_url}/v3/chat/cancel" + params = { + "conversation_id": conversation_id, + "chat_id": chat_id, + } + return self._requester.request("post", url, Chat, params=params) diff --git a/cozepy/conversation/__init__.py b/cozepy/conversation/__init__.py new file mode 100644 index 0000000..1b90a24 --- /dev/null +++ b/cozepy/conversation/__init__.py @@ -0,0 +1,18 @@ +from cozepy.auth import Auth +from cozepy.request import Requester + + +class ConversationClient(object): + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = base_url + self._auth = auth + self._requester = requester + self._v1 = None + + @property + def v1(self): + if not self._v1: + from .v1 import ConversationClient + + self._v1 = ConversationClient(self._base_url, self._auth, self._requester) + return self._v1 diff --git a/cozepy/conversation.py b/cozepy/conversation/v1/__init__.py similarity index 79% rename from cozepy/conversation.py rename to cozepy/conversation/v1/__init__.py index 9dcbdad..6eedc24 100644 --- a/cozepy/conversation.py +++ b/cozepy/conversation/v1/__init__.py @@ -1,8 +1,8 @@ from typing import Dict, List -from .auth import Auth -from .model import CozeModel, Message -from .request import Requester +from cozepy.auth import Auth +from cozepy.model import CozeModel, Message +from cozepy.request import Requester class Conversation(CozeModel): @@ -17,7 +17,7 @@ def __init__(self, base_url: str, auth: Auth, requester: Requester): self._auth = auth self._requester = requester - def create_v1(self, *, messages: List[Message] = None, meta_data: Dict[str, str] = None) -> Conversation: + def create(self, *, messages: List[Message] = None, meta_data: Dict[str, str] = None) -> Conversation: """ Create a conversation. Conversation is an interaction between a bot and a user, including one or more messages. @@ -29,7 +29,7 @@ def create_v1(self, *, messages: List[Message] = None, meta_data: Dict[str, str] } return self._requester.request("post", url, Conversation, body=body) - def get_v1(self, *, conversation_id: str) -> Conversation: + def get(self, *, conversation_id: str) -> Conversation: """ Get the information of specific conversation. """ diff --git a/cozepy/coze.py b/cozepy/coze.py index 2c9b0d0..761cdd0 100644 --- a/cozepy/coze.py +++ b/cozepy/coze.py @@ -38,7 +38,7 @@ def bot(self) -> "BotClient": @property def workspace(self) -> "WorkspaceClient": if not self._workspace: - from cozepy.workspace import WorkspaceClient + from .workspace import WorkspaceClient self._workspace = WorkspaceClient(self._base_url, self._auth, self._requester) return self._workspace @@ -46,7 +46,7 @@ def workspace(self) -> "WorkspaceClient": @property def conversation(self) -> "ConversationClient": if not self._conversation: - from cozepy.conversation import ConversationClient + from .conversation import ConversationClient self._conversation = ConversationClient(self._base_url, self._auth, self._requester) return self._conversation diff --git a/cozepy/workspace/__init__.py b/cozepy/workspace/__init__.py new file mode 100644 index 0000000..a26bca8 --- /dev/null +++ b/cozepy/workspace/__init__.py @@ -0,0 +1,22 @@ +from cozepy.auth import Auth +from cozepy.request import Requester + + +class WorkspaceClient(object): + """ + Bot class. + """ + + def __init__(self, base_url: str, auth: Auth, requester: Requester): + self._base_url = base_url + self._auth = auth + self._requester = requester + self._v1 = None + + @property + def v1(self): + if not self._v1: + from .v1 import WorkspaceClient + + self._v1 = WorkspaceClient(self._base_url, self._auth, self._requester) + return self._v1 diff --git a/cozepy/workspace.py b/cozepy/workspace/v1/__init__.py similarity index 92% rename from cozepy/workspace.py rename to cozepy/workspace/v1/__init__.py index 12e1a08..23bdd11 100644 --- a/cozepy/workspace.py +++ b/cozepy/workspace/v1/__init__.py @@ -40,7 +40,7 @@ def __init__(self, base_url: str, auth: Auth, requester: Requester): self._auth = auth self._requester = requester - def list_workspace_v1(self, *, page_num: int = 1, page_size: int = 20, headers=None) -> NumberPaged[Workspace]: + def list(self, *, page_num: int = 1, page_size: int = 20, headers=None) -> NumberPaged[Workspace]: url = f"{self._base_url}/v1/workspaces" params = { "page_size": page_size, diff --git a/tests/test_auth.py b/tests/test_auth.py index e63e9c7..ec56c35 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -16,5 +16,5 @@ def test_jwt_auth(): token = app.jwt_auth(private_key, key_id, 30) assert token.access_token != "" assert token.token_type == "Bearer" - assert token.expires_in - int(time.time()) <= 30 + assert token.expires_in - int(time.time()) <= 31 assert token.refresh_token == "" diff --git a/tests/test_bot.py b/tests/test_bot.py index 4a3f404..f458816 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -11,7 +11,7 @@ def test_list_published_bots_v1(self): auth = TokenAuth(token) cli = Coze(auth=auth, base_url=COZE_CN_BASE_URL) - res = cli.bot.list_published_bots_v1(space_id=space_id, page_size=2) + res = cli.bot.v1.list(space_id=space_id, page_size=2) assert res.total > 1 assert res.has_more assert len(res.items) > 1 diff --git a/tests/test_chat.py b/tests/test_chat.py index e5ef42d..febd5cd 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1,8 +1,8 @@ import os -from cozepy import TokenAuth, Coze, COZE_CN_BASE_URL, Message, ChatIterator, Event +from cozepy import TokenAuth, Coze, COZE_CN_BASE_URL, Message from cozepy.auth import _random_hex -from cozepy.model import ChatStatus +from cozepy.chat.v3 import ChatIterator, Event def test_chat_v3_not_stream(): @@ -12,7 +12,7 @@ def test_chat_v3_not_stream(): auth = TokenAuth(token) cli = Coze(auth=auth, base_url=COZE_CN_BASE_URL) - chat = cli.chat.chat_v3( + chat = cli.chat.v3.create( bot_id=bot_id, user_id=_random_hex(10), additional_messages=[Message.user_text_message("Hi, how are you?")], @@ -37,7 +37,7 @@ def test_chat_v3_stream(): auth = TokenAuth(token) cli = Coze(auth=auth, base_url=COZE_CN_BASE_URL) - chat_iter: ChatIterator = cli.chat.chat_v3( + chat_iter: ChatIterator = cli.chat.v3.create( bot_id=bot_id, user_id=_random_hex(10), additional_messages=[Message.user_text_message("Hi, how are you?")],