diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 36a790557..79c860e6b 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -1,7 +1,9 @@ +import json + from flask_restful import Resource, marshal_with, reqparse from flask_restful.inputs import int_range from sqlalchemy.orm import Session -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import BadRequest, NotFound import services from controllers.service_api import api @@ -15,6 +17,7 @@ from fields.conversation_fields import ( simple_conversation_fields, ) from fields.conversation_variable_fields import ( + conversation_variable_fields, conversation_variable_infinite_scroll_pagination_fields, ) from libs.helper import uuid_value @@ -120,7 +123,41 @@ class ConversationVariablesApi(Resource): raise NotFound("Conversation Not Exists.") +class ConversationVariableDetailApi(Resource): + @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) + @marshal_with(conversation_variable_fields) + def put(self, app_model: App, end_user: EndUser, c_id, variable_id): + """Update a conversation variable's value""" + app_mode = AppMode.value_of(app_model.mode) + if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: + raise NotChatAppError() + + conversation_id = str(c_id) + variable_id = str(variable_id) + + parser = reqparse.RequestParser() + parser.add_argument("value", required=True, location="json") + args = parser.parse_args() + + try: + return ConversationService.update_conversation_variable( + app_model, conversation_id, variable_id, end_user, json.loads(args["value"]) + ) + except services.errors.conversation.ConversationNotExistsError: + raise NotFound("Conversation Not Exists.") + except services.errors.conversation.ConversationVariableNotExistsError: + raise NotFound("Conversation Variable Not Exists.") + except services.errors.conversation.ConversationVariableTypeMismatchError as e: + raise BadRequest(str(e)) + + api.add_resource(ConversationRenameApi, "/conversations//name", endpoint="conversation_name") api.add_resource(ConversationApi, "/conversations") api.add_resource(ConversationDetailApi, "/conversations/", endpoint="conversation_detail") api.add_resource(ConversationVariablesApi, "/conversations//variables", endpoint="conversation_variables") +api.add_resource( + ConversationVariableDetailApi, + "/conversations//variables/", + endpoint="conversation_variable_detail", + methods=["PUT"], +) diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index 206c832a2..692a3639c 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -1,12 +1,15 @@ from collections.abc import Callable, Sequence -from typing import Optional, Union +from typing import Any, Optional, Union from sqlalchemy import asc, desc, func, or_, select from sqlalchemy.orm import Session from core.app.entities.app_invoke_entities import InvokeFrom from core.llm_generator.llm_generator import LLMGenerator +from core.variables.types import SegmentType +from core.workflow.nodes.variable_assigner.common.impl import conversation_variable_updater_factory from extensions.ext_database import db +from factories import variable_factory from libs.datetime_utils import naive_utc_now from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import ConversationVariable @@ -15,6 +18,7 @@ from models.model import App, Conversation, EndUser, Message from services.errors.conversation import ( ConversationNotExistsError, ConversationVariableNotExistsError, + ConversationVariableTypeMismatchError, LastConversationNotExistsError, ) from services.errors.message import MessageNotExistsError @@ -220,3 +224,82 @@ class ConversationService: ] return InfiniteScrollPagination(variables, limit, has_more) + + @classmethod + def update_conversation_variable( + cls, + app_model: App, + conversation_id: str, + variable_id: str, + user: Optional[Union[Account, EndUser]], + new_value: Any, + ) -> dict: + """ + Update a conversation variable's value. + + Args: + app_model: The app model + conversation_id: The conversation ID + variable_id: The variable ID to update + user: The user (Account or EndUser) + new_value: The new value for the variable + + Returns: + Dictionary containing the updated variable information + + Raises: + ConversationNotExistsError: If the conversation doesn't exist + ConversationVariableNotExistsError: If the variable doesn't exist + ConversationVariableTypeMismatchError: If the new value type doesn't match the variable's expected type + """ + # Verify conversation exists and user has access + conversation = cls.get_conversation(app_model, conversation_id, user) + + # Get the existing conversation variable + stmt = ( + select(ConversationVariable) + .where(ConversationVariable.app_id == app_model.id) + .where(ConversationVariable.conversation_id == conversation.id) + .where(ConversationVariable.id == variable_id) + ) + + with Session(db.engine) as session: + existing_variable = session.scalar(stmt) + if not existing_variable: + raise ConversationVariableNotExistsError() + + # Convert existing variable to Variable object + current_variable = existing_variable.to_variable() + + # Validate that the new value type matches the expected variable type + expected_type = SegmentType(current_variable.value_type) + if not expected_type.is_valid(new_value): + inferred_type = SegmentType.infer_segment_type(new_value) + raise ConversationVariableTypeMismatchError( + f"Type mismatch: variable '{current_variable.name}' expects {expected_type.value}, " + f"but got {inferred_type.value if inferred_type else 'unknown'} type" + ) + + # Create updated variable with new value only, preserving everything else + updated_variable_dict = { + "id": current_variable.id, + "name": current_variable.name, + "description": current_variable.description, + "value_type": current_variable.value_type, + "value": new_value, + "selector": current_variable.selector, + } + + updated_variable = variable_factory.build_conversation_variable_from_mapping(updated_variable_dict) + + # Use the conversation variable updater to persist the changes + updater = conversation_variable_updater_factory() + updater.update(conversation_id, updated_variable) + updater.flush() + + # Return the updated variable data + return { + "created_at": existing_variable.created_at, + "updated_at": naive_utc_now(), # Update timestamp + **updated_variable.model_dump(), + } diff --git a/api/services/errors/conversation.py b/api/services/errors/conversation.py index f8051e341..a123f99b5 100644 --- a/api/services/errors/conversation.py +++ b/api/services/errors/conversation.py @@ -15,3 +15,7 @@ class ConversationCompletedError(Exception): class ConversationVariableNotExistsError(BaseServiceError): pass + + +class ConversationVariableTypeMismatchError(BaseServiceError): + pass diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index bafcb1f99..ba698bdfd 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -1011,6 +1011,121 @@ Chat applications support session persistence, allowing previous chat history to --- + + + + Update the value of a specific conversation variable. This endpoint allows you to modify the value of a variable that was captured during the conversation while preserving its name, type, and description. + + ### Path Parameters + + + + The ID of the conversation containing the variable to update. + + + The ID of the variable to update. + + + + ### Request Body + + + + The new value for the variable. Must match the variable's expected type (string, number, object, etc.). + + + The user identifier, defined by the developer, must ensure uniqueness within the application. + + + + ### Response + + Returns the updated variable object with: + - `id` (string) Variable ID + - `name` (string) Variable name + - `value_type` (string) Variable type (string, number, object, etc.) + - `value` (any) Updated variable value + - `description` (string) Variable description + - `created_at` (int) Creation timestamp + - `updated_at` (int) Last update timestamp + + ### Errors + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, Value type doesn't match variable's expected type + - 404, `conversation_not_exists`, Conversation not found + - 404, `conversation_variable_not_exists`, Variable not found + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: 'String Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "New string value", + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Number Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Object Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "Customer name extracted from the conversation", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + + 特定の会話変数の値を更新します。このエンドポイントは、名前、型、説明を保持しながら、会話中にキャプチャされた変数の値を変更することを可能にします。 + + ### パスパラメータ + + + + 更新する変数を含む会話のID。 + + + 更新する変数のID。 + + + + ### リクエストボディ + + + + 変数の新しい値。変数の期待される型(文字列、数値、オブジェクトなど)と一致する必要があります。 + + + ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。 + + + + ### レスポンス + + 以下を含む更新された変数オブジェクトを返します: + - `id` (string) 変数ID + - `name` (string) 変数名 + - `value_type` (string) 変数型(文字列、数値、オブジェクトなど) + - `value` (any) 更新された変数値 + - `description` (string) 変数の説明 + - `created_at` (int) 作成タイムスタンプ + - `updated_at` (int) 最終更新タイムスタンプ + + ### エラー + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 値の型が変数の期待される型と一致しません + - 404, `conversation_not_exists`, 会話が見つかりません + - 404, `conversation_variable_not_exists`, 変数が見つかりません + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '文字列値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新しい文字列値", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'オブジェクト値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "会話から抽出された顧客名", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + + 更新特定对话变量的值。此端点允许您修改在对话过程中捕获的变量值,同时保留其名称、类型和描述。 + + ### 路径参数 + + + + 包含要更新变量的对话ID。 + + + 要更新的变量ID。 + + + + ### 请求体 + + + + 变量的新值。必须匹配变量的预期类型(字符串、数字、对象等)。 + + + 用户标识符,由开发人员定义的规则,在应用程序内必须唯一。 + + + + ### 响应 + + 返回包含以下内容的更新变量对象: + - `id` (string) 变量ID + - `name` (string) 变量名称 + - `value_type` (string) 变量类型(字符串、数字、对象等) + - `value` (any) 更新后的变量值 + - `description` (string) 变量描述 + - `created_at` (int) 创建时间戳 + - `updated_at` (int) 最后更新时间戳 + + ### 错误 + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 值类型与变量的预期类型不匹配 + - 404, `conversation_not_exists`, 对话不存在 + - 404, `conversation_variable_not_exists`, 变量不存在 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '字符串值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新的字符串值", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数字值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: '对象值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "客户名称(从对话中提取)", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + + Update the value of a specific conversation variable. This endpoint allows you to modify the value of a variable that was captured during the conversation while preserving its name, type, and description. + + ### Path Parameters + + + + The ID of the conversation containing the variable to update. + + + The ID of the variable to update. + + + + ### Request Body + + + + The new value for the variable. Must match the variable's expected type (string, number, object, etc.). + + + The user identifier, defined by the developer, must ensure uniqueness within the application. + + + + ### Response + + Returns the updated variable object with: + - `id` (string) Variable ID + - `name` (string) Variable name + - `value_type` (string) Variable type (string, number, object, etc.) + - `value` (any) Updated variable value + - `description` (string) Variable description + - `created_at` (int) Creation timestamp + - `updated_at` (int) Last update timestamp + + ### Errors + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, Value type doesn't match variable's expected type + - 404, `conversation_not_exists`, Conversation not found + - 404, `conversation_variable_not_exists`, Variable not found + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: 'String Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "New string value", + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Number Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'Object Value' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "Customer name extracted from the conversation", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + + 特定の会話変数の値を更新します。このエンドポイントは、名前、型、説明を保持しながら、会話中にキャプチャされた変数の値を変更することを可能にします。 + + ### パスパラメータ + + + + 更新する変数を含む会話のID。 + + + 更新する変数のID。 + + + + ### リクエストボディ + + + + 変数の新しい値。変数の期待される型(文字列、数値、オブジェクトなど)と一致する必要があります。 + + + ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。 + + + + ### レスポンス + + 以下を含む更新された変数オブジェクトを返します: + - `id` (string) 変数ID + - `name` (string) 変数名 + - `value_type` (string) 変数型(文字列、数値、オブジェクトなど) + - `value` (any) 更新された変数値 + - `description` (string) 変数の説明 + - `created_at` (int) 作成タイムスタンプ + - `updated_at` (int) 最終更新タイムスタンプ + + ### エラー + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 値の型が変数の期待される型と一致しません + - 404, `conversation_not_exists`, 会話が見つかりません + - 404, `conversation_variable_not_exists`, 変数が見つかりません + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '文字列値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新しい文字列値", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: 'オブジェクト値' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "会話から抽出された顧客名", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- + + + + 更新特定对话变量的值。此端点允许您修改在对话过程中捕获的变量值,同时保留其名称、类型和描述。 + + ### 路径参数 + + + + 包含要更新变量的对话ID。 + + + 要更新的变量ID。 + + + + ### 请求体 + + + + 变量的新值。必须匹配变量的预期类型(字符串、数字、对象等)。 + + + 用户标识符,由开发人员定义的规则,在应用程序内必须唯一。 + + + + ### 响应 + + 返回包含以下内容的更新变量对象: + - `id` (string) 变量ID + - `name` (string) 变量名称 + - `value_type` (string) 变量类型(字符串、数字、对象等) + - `value` (any) 更新后的变量值 + - `description` (string) 变量描述 + - `created_at` (int) 创建时间戳 + - `updated_at` (int) 最后更新时间戳 + + ### 错误 + - 400, `Type mismatch: variable expects {expected_type}, but got {actual_type} type`, 值类型与变量的预期类型不匹配 + - 404, `conversation_not_exists`, 对话不存在 + - 404, `conversation_variable_not_exists`, 变量不存在 + + + + + + + ```bash {{ title: 'cURL' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "Updated Value", + "user": "abc-123" + }' + ``` + + + + + ```bash {{ title: '字符串值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": "新的字符串值", + "user": "abc-123" + }' + ``` + + ```bash {{ title: '数字值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": 42, + "user": "abc-123" + }' + ``` + + ```bash {{ title: '对象值' }} + curl -X PUT '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables/{variable_id}' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "value": {"product": "Widget", "quantity": 10, "price": 29.99}, + "user": "abc-123" + }' + ``` + + + + ```json {{ title: 'Response' }} + { + "id": "variable-uuid-1", + "name": "customer_name", + "value_type": "string", + "value": "Updated Value", + "description": "客户名称(从对话中提取)", + "created_at": 1650000000000, + "updated_at": 1650000001000 + } + ``` + + + + +--- +