diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0efd7e8..e39bd53 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -43,6 +43,7 @@ jobs:
COZE_JWT_AUTH_CLIENT_ID: ${{ secrets.COZE_JWT_AUTH_CLIENT_ID }}
COZE_JWT_AUTH_PRIVATE_KEY: ${{ secrets.COZE_JWT_AUTH_PRIVATE_KEY }}
COZE_JWT_AUTH_KEY_ID: ${{ secrets.COZE_JWT_AUTH_KEY_ID }}
+ COZE_BOT_ID_TRANSLATE: ${{ secrets.COZE_BOT_ID_TRANSLATE }}
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
diff --git a/cozepy/__init__.py b/cozepy/__init__.py
index fe3a6d2..3c265ec 100644
--- a/cozepy/__init__.py
+++ b/cozepy/__init__.py
@@ -1,16 +1,18 @@
from .auth import ApplicationOAuth, Auth, TokenAuth
from .config import COZE_COM_BASE_URL, COZE_CN_BASE_URL
from .coze import Coze
-from .model import TokenPaged, NumberPaged
-from .conversation import (
+from .model import (
+ TokenPaged,
+ NumberPaged,
MessageRole,
MessageType,
MessageContentType,
MessageObjectStringType,
MessageObjectString,
Message,
- Conversation,
)
+from .conversation import Conversation
+from .chat import Chat, ChatEvent, ChatIterator, Event
__all__ = [
"ApplicationOAuth",
@@ -28,4 +30,8 @@
"MessageObjectString",
"Message",
"Conversation",
+ "Chat",
+ "ChatEvent",
+ "ChatIterator",
+ "Event",
]
diff --git a/cozepy/chat.py b/cozepy/chat.py
new file mode 100644
index 0000000..eb84d9a
--- /dev/null
+++ b/cozepy/chat.py
@@ -0,0 +1,140 @@
+import json
+from enum import Enum
+from typing import Dict, List, Iterator, Union
+
+from .auth import Auth
+from .model import Message, Chat, CozeModel
+from .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 chat_v3(
+ 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))
diff --git a/cozepy/conversation.py b/cozepy/conversation.py
index 91a7c1e..9dcbdad 100644
--- a/cozepy/conversation.py
+++ b/cozepy/conversation.py
@@ -1,119 +1,10 @@
-from enum import Enum
-from typing import Dict, List, Optional
+from typing import Dict, List
from .auth import Auth
-from .model import CozeModel
+from .model import CozeModel, Message
from .request import Requester
-class MessageRole(str, Enum):
- # Indicates that the content of the message is sent by the user.
- user = "user"
- # Indicates that the content of the message is sent by the bot.
- assistant = "assistant"
-
-
-class MessageType(str, Enum):
- # User input content.
- # 用户输入内容。
- question = "question"
- # The message content returned by the Bot to the user, supporting incremental return. If the workflow is bound to a message node, there may be multiple answer scenarios, and the end flag of the streaming return can be used to determine that all answers are completed.
- # Bot 返回给用户的消息内容,支持增量返回。如果工作流绑定了消息节点,可能会存在多 answer 场景,此时可以用流式返回的结束标志来判断所有 answer 完成。
- answer = "answer"
- # Intermediate results of the function (function call) called during the Bot conversation process.
- # Bot 对话过程中调用函数(function call)的中间结果。
- function_call = "function_call"
- # Results returned after calling the tool (function call).
- # 调用工具 (function call)后返回的结果。
- tool_output = "tool_output"
- # Results returned after calling the tool (function call).
- # 调用工具 (function call)后返回的结果。
- tool_response = "tool_response"
- # If the user question suggestion switch is turned on in the Bot configuration, the reply content related to the recommended questions will be returned.
- # 如果在 Bot 上配置打开了用户问题建议开关,则会返回推荐问题相关的回复内容。不支持在请求中作为入参。
- follow_up = "follow_up"
- # In the scenario of multiple answers, the server will return a verbose package, and the corresponding content is in JSON format. content.msg_type = generate_answer_finish represents that all answers have been replied to.
- # 多 answer 场景下,服务端会返回一个 verbose 包,对应的 content 为 JSON 格式,content.msg_type =generate_answer_finish 代表全部 answer 回复完成。不支持在请求中作为入参。
- verbose = "verbose"
-
-
-class MessageContentType(str, Enum):
- # Text.
- # 文本。
- text = "text"
- # Multimodal content, that is, a combination of text and files, or a combination of text and images.
- # 多模态内容,即文本和文件的组合、文本和图片的组合。
- object_string = "object_string"
- # message card. This enum value only appears in the interface response and is not supported as an input parameter.
- # 卡片。此枚举值仅在接口响应中出现,不支持作为入参。
- card = "card"
-
-
-class MessageObjectStringType(str, Enum):
- """
- The content type of the multimodal message.
- """
-
- text = "text"
- file = "file"
- image = "image"
-
-
-class MessageObjectString(CozeModel):
- # The content type of the multimodal message.
- # 多模态消息内容类型
- type: MessageObjectStringType
- # Text content. Required when type is text.
- # 文本内容。
- text: str
- # The ID of the file or image content.
- # 在 type 为 file 或 image 时,file_id 和 file_url 应至少指定一个。
- file_id: str
- # The online address of the file or image content.
Must be a valid address that is publicly accessible.
- # file_id or file_url must be specified when type is file or image.
- # 文件或图片内容的在线地址。必须是可公共访问的有效地址。
- # 在 type 为 file 或 image 时,file_id 和 file_url 应至少指定一个。
- file_url: str
-
-
-class Message(CozeModel):
- # The entity that sent this message.
- role: MessageRole
- # The type of message.
- # type: # MessageType
- # The content of the message. It supports various types of content, including plain text, multimodal (a mix of text, images, and files), message cards, and more.
- # 消息的内容,支持纯文本、多模态(文本、图片、文件混合输入)、卡片等多种类型的内容。
- content: str
- # The type of message content.
- # 消息内容的类型
- content_type: MessageContentType
- # Additional information when creating a message, and this additional information will also be returned when retrieving messages.
- # Custom key-value pairs should be specified in Map object format, with a length of 16 key-value pairs. The length of the key should be between 1 and 64 characters, and the length of the value should be between 1 and 512 characters.
- # 创建消息时的附加消息,获取消息时也会返回此附加消息。
- # 自定义键值对,应指定为 Map 对象格式。长度为 16 对键值对,其中键(key)的长度范围为 1~64 个字符,值(value)的长度范围为 1~512 个字符。
- meta_data: Optional[Dict[str, str]] = None
-
- @staticmethod
- def user_text_message(content: str, meta_data: Optional[Dict[str, str]] = None) -> "Message":
- return Message(
- role=MessageRole.user,
- type=MessageType.question,
- content=content,
- content_type=MessageContentType.text,
- meta_data=meta_data,
- )
-
- @staticmethod
- def assistant_text_message(content: str, meta_data: Optional[Dict[str, str]] = None) -> "Message":
- return Message(
- role=MessageRole.assistant,
- type=MessageType.answer,
- content=content,
- content_type=MessageContentType.text,
- meta_data=meta_data,
- )
-
-
class Conversation(CozeModel):
id: str
created_at: int
diff --git a/cozepy/coze.py b/cozepy/coze.py
index a4cbe44..2c9b0d0 100644
--- a/cozepy/coze.py
+++ b/cozepy/coze.py
@@ -8,6 +8,7 @@
from .bot import BotClient
from .workspace import WorkspaceClient
from .conversation import ConversationClient
+ from .chat import ChatClient
class Coze(object):
@@ -24,6 +25,7 @@ def __init__(
self._bot = None
self._workspace = None
self._conversation = None
+ self._chat = None
@property
def bot(self) -> "BotClient":
@@ -48,3 +50,11 @@ def conversation(self) -> "ConversationClient":
self._conversation = ConversationClient(self._base_url, self._auth, self._requester)
return self._conversation
+
+ @property
+ def chat(self) -> "ChatClient":
+ if not self._chat:
+ from cozepy.chat import ChatClient
+
+ self._chat = ChatClient(self._base_url, self._auth, self._requester)
+ return self._chat
diff --git a/cozepy/model.py b/cozepy/model.py
index 351e068..509a6ec 100644
--- a/cozepy/model.py
+++ b/cozepy/model.py
@@ -1,4 +1,5 @@
-from typing import TypeVar, Generic, List
+from enum import Enum
+from typing import TypeVar, Generic, List, Optional, Dict
from pydantic import BaseModel, ConfigDict
@@ -49,3 +50,170 @@ def __repr__(self):
return (
f"NumberPaged(items={self.items}, page_num={self.page_num}, page_size={self.page_size}, total={self.total})"
)
+
+
+class MessageRole(str, Enum):
+ # Indicates that the content of the message is sent by the user.
+ user = "user"
+ # Indicates that the content of the message is sent by the bot.
+ assistant = "assistant"
+
+
+class MessageType(str, Enum):
+ # User input content.
+ # 用户输入内容。
+ question = "question"
+ # The message content returned by the Bot to the user, supporting incremental return. If the workflow is bound to a message node, there may be multiple answer scenarios, and the end flag of the streaming return can be used to determine that all answers are completed.
+ # Bot 返回给用户的消息内容,支持增量返回。如果工作流绑定了消息节点,可能会存在多 answer 场景,此时可以用流式返回的结束标志来判断所有 answer 完成。
+ answer = "answer"
+ # Intermediate results of the function (function call) called during the Bot conversation process.
+ # Bot 对话过程中调用函数(function call)的中间结果。
+ function_call = "function_call"
+ # Results returned after calling the tool (function call).
+ # 调用工具 (function call)后返回的结果。
+ tool_output = "tool_output"
+ # Results returned after calling the tool (function call).
+ # 调用工具 (function call)后返回的结果。
+ tool_response = "tool_response"
+ # If the user question suggestion switch is turned on in the Bot configuration, the reply content related to the recommended questions will be returned.
+ # 如果在 Bot 上配置打开了用户问题建议开关,则会返回推荐问题相关的回复内容。不支持在请求中作为入参。
+ follow_up = "follow_up"
+ # In the scenario of multiple answers, the server will return a verbose package, and the corresponding content is in JSON format. content.msg_type = generate_answer_finish represents that all answers have been replied to.
+ # 多 answer 场景下,服务端会返回一个 verbose 包,对应的 content 为 JSON 格式,content.msg_type =generate_answer_finish 代表全部 answer 回复完成。不支持在请求中作为入参。
+ verbose = "verbose"
+
+
+class MessageContentType(str, Enum):
+ # Text.
+ # 文本。
+ text = "text"
+ # Multimodal content, that is, a combination of text and files, or a combination of text and images.
+ # 多模态内容,即文本和文件的组合、文本和图片的组合。
+ object_string = "object_string"
+ # message card. This enum value only appears in the interface response and is not supported as an input parameter.
+ # 卡片。此枚举值仅在接口响应中出现,不支持作为入参。
+ card = "card"
+
+
+class MessageObjectStringType(str, Enum):
+ """
+ The content type of the multimodal message.
+ """
+
+ text = "text"
+ file = "file"
+ image = "image"
+
+
+class MessageObjectString(CozeModel):
+ # The content type of the multimodal message.
+ # 多模态消息内容类型
+ type: MessageObjectStringType
+ # Text content. Required when type is text.
+ # 文本内容。
+ text: str
+ # The ID of the file or image content.
+ # 在 type 为 file 或 image 时,file_id 和 file_url 应至少指定一个。
+ file_id: str
+ # The online address of the file or image content.
Must be a valid address that is publicly accessible.
+ # file_id or file_url must be specified when type is file or image.
+ # 文件或图片内容的在线地址。必须是可公共访问的有效地址。
+ # 在 type 为 file 或 image 时,file_id 和 file_url 应至少指定一个。
+ file_url: str
+
+
+class Message(CozeModel):
+ # The entity that sent this message.
+ role: MessageRole
+ # The type of message.
+ type: MessageType
+ # The content of the message. It supports various types of content, including plain text, multimodal (a mix of text, images, and files), message cards, and more.
+ # 消息的内容,支持纯文本、多模态(文本、图片、文件混合输入)、卡片等多种类型的内容。
+ content: str
+ # The type of message content.
+ # 消息内容的类型
+ content_type: MessageContentType
+ # Additional information when creating a message, and this additional information will also be returned when retrieving messages.
+ # Custom key-value pairs should be specified in Map object format, with a length of 16 key-value pairs. The length of the key should be between 1 and 64 characters, and the length of the value should be between 1 and 512 characters.
+ # 创建消息时的附加消息,获取消息时也会返回此附加消息。
+ # 自定义键值对,应指定为 Map 对象格式。长度为 16 对键值对,其中键(key)的长度范围为 1~64 个字符,值(value)的长度范围为 1~512 个字符。
+ meta_data: Optional[Dict[str, str]] = None
+
+ id: str = None
+ conversation_id: str = None
+ bot_id: str = None
+ chat_id: str = None
+ created_at: int = None
+ updated_at: int = None
+
+ @staticmethod
+ def user_text_message(content: str, meta_data: Optional[Dict[str, str]] = None) -> "Message":
+ return Message(
+ role=MessageRole.user,
+ type=MessageType.question,
+ content=content,
+ content_type=MessageContentType.text,
+ meta_data=meta_data,
+ )
+
+ @staticmethod
+ def assistant_text_message(content: str, meta_data: Optional[Dict[str, str]] = None) -> "Message":
+ return Message(
+ role=MessageRole.assistant,
+ type=MessageType.answer,
+ content=content,
+ content_type=MessageContentType.text,
+ meta_data=meta_data,
+ )
+
+
+class ChatStatus(str, Enum):
+ """
+ The running status of the session
+ """
+
+ # The session has been created.
+ created = "created"
+
+ # The Bot is processing.
+ in_progress = "in_progress"
+
+ # The Bot has finished processing, and the session has ended.
+ completed = "completed"
+
+ # The session has failed.
+ failed = "failed"
+
+ # The session is interrupted and requires further processing.
+ requires_action = "requires_action"
+
+
+class Chat(CozeModel):
+ # The ID of the chat.
+ id: str
+ # The ID of the conversation.
+ conversation_id: str
+ # The ID of the bot.
+ bot_id: str
+ # Indicates the create time of the chat. The value format is Unix timestamp in seconds.
+ created_at: Optional[int] = None
+ # Indicates the end time of the chat. The value format is Unix timestamp in seconds.
+ completed_at: Optional[int] = None
+ # Indicates the failure time of the chat. The value format is Unix timestamp in seconds.
+ failed_at: Optional[int] = None
+ # Additional information when creating a message, and this additional information will also be returned when retrieving messages.
+ # Custom key-value pairs should be specified in Map object format, with a length of 16 key-value pairs. The length of the key should be between 1 and 64 characters, and the length of the value should be between 1 and 512 characters.
+ meta_data: Optional[Dict[str, str]] = None
+ # When the chat encounters an exception, this field returns detailed error information, including:
+ # Code: The error code. An integer type. 0 indicates success, other values indicate failure.
+ # Msg: The error message. A string type.
+
+ # The running status of the session. The values are:
+ # created: The session has been created.
+ # in_progress: The Bot is processing.
+ # completed: The Bot has finished processing, and the session has ended.
+ # failed: The session has failed.
+ # requires_action: The session is interrupted and requires further processing.
+ status: ChatStatus
+
+ # Details of the information needed for execution.
diff --git a/cozepy/request.py b/cozepy/request.py
index c93a865..eedad80 100644
--- a/cozepy/request.py
+++ b/cozepy/request.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Tuple, Optional
+from typing import TYPE_CHECKING, Tuple, Optional, Iterator, Union
import requests
from requests import Response
@@ -36,7 +36,8 @@ def request(
params: dict = None,
headers: dict = None,
body: dict = None,
- ) -> T:
+ stream: bool = False,
+ ) -> Union[T, Iterator[bytes]]:
"""
Send a request to the server.
"""
@@ -44,7 +45,9 @@ def request(
headers = {}
if self._auth:
self._auth.authentication(headers)
- r = requests.request(method, url, params=params, headers=headers, json=body)
+ r = requests.request(method, url, params=params, headers=headers, json=body, stream=stream)
+ if stream:
+ return r.iter_lines()
code, msg, data = self.__parse_requests_code_msg(r)
diff --git a/tests/test_bot.py b/tests/test_bot.py
index f6a2e03..4a3f404 100644
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -8,8 +8,6 @@ class TestBotClient(TestCase):
def test_list_published_bots_v1(self):
space_id = os.getenv("SPACE_ID_1").strip()
token = os.getenv("COZE_TOKEN").strip()
- for i in token:
- print("token", i)
auth = TokenAuth(token)
cli = Coze(auth=auth, base_url=COZE_CN_BASE_URL)
diff --git a/tests/test_chat.py b/tests/test_chat.py
new file mode 100644
index 0000000..9227d25
--- /dev/null
+++ b/tests/test_chat.py
@@ -0,0 +1,41 @@
+import os
+
+from cozepy import TokenAuth, Coze, COZE_CN_BASE_URL, Message, ChatIterator, Event
+from cozepy.auth import _random_hex
+
+
+def test_chat_v3_not_stream():
+ token = os.getenv("COZE_TOKEN").strip()
+ bot_id = os.getenv("COZE_BOT_ID_TRANSLATE").strip()
+
+ auth = TokenAuth(token)
+ cli = Coze(auth=auth, base_url=COZE_CN_BASE_URL)
+
+ chat = cli.chat.chat_v3(
+ bot_id=bot_id,
+ user_id=_random_hex(10),
+ additional_messages=[Message.user_text_message("Hi, how are you?")],
+ stream=False,
+ )
+ assert chat is not None
+ assert chat.id != ""
+
+
+def test_chat_v3_stream():
+ token = os.getenv("COZE_TOKEN").strip()
+ bot_id = os.getenv("COZE_BOT_ID_TRANSLATE").strip()
+
+ auth = TokenAuth(token)
+ cli = Coze(auth=auth, base_url=COZE_CN_BASE_URL)
+
+ chat_iter: ChatIterator = cli.chat.chat_v3(
+ bot_id=bot_id,
+ user_id=_random_hex(10),
+ additional_messages=[Message.user_text_message("Hi, how are you?")],
+ stream=True,
+ )
+ for item in chat_iter:
+ assert item is not None
+ assert item.event != ""
+ if item.event == Event.conversation_message_delta:
+ assert item.message.content != ""