feat: fix i18n missing keys and merge upstream/main (#24615)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
Signed-off-by: Yongtao Huang <yongtaoh2022@gmail.com>
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Signed-off-by: zhanluxianshen <zhanluxianshen@163.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: GuanMu <ballmanjq@gmail.com>
Co-authored-by: Davide Delbianco <davide.delbianco@outlook.com>
Co-authored-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Co-authored-by: kenwoodjw <blackxin55+@gmail.com>
Co-authored-by: Yongtao Huang <yongtaoh2022@gmail.com>
Co-authored-by: Yongtao Huang <99629139+hyongtao-db@users.noreply.github.com>
Co-authored-by: Qiang Lee <18018968632@163.com>
Co-authored-by: 李强04 <liqiang04@gaotu.cn>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Matri Qi <matrixdom@126.com>
Co-authored-by: huayaoyue6 <huayaoyue@163.com>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: znn <jubinkumarsoni@gmail.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: Muke Wang <shaodwaaron@gmail.com>
Co-authored-by: wangmuke <wangmuke@kingsware.cn>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: quicksand <quicksandzn@gmail.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Eric Guo <eric.guocz@gmail.com>
Co-authored-by: Zhedong Cen <cenzhedong2@126.com>
Co-authored-by: jiangbo721 <jiangbo721@163.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: hjlarry <25834719+hjlarry@users.noreply.github.com>
Co-authored-by: lxsummer <35754229+lxjustdoit@users.noreply.github.com>
Co-authored-by: 湛露先生 <zhanluxianshen@163.com>
Co-authored-by: Guangdong Liu <liugddx@gmail.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Yessenia-d <yessenia.contact@gmail.com>
Co-authored-by: huangzhuo1949 <167434202+huangzhuo1949@users.noreply.github.com>
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
Co-authored-by: 17hz <0x149527@gmail.com>
Co-authored-by: Amy <1530140574@qq.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Nite Knite <nkCoding@gmail.com>
Co-authored-by: Yeuoly <45712896+Yeuoly@users.noreply.github.com>
Co-authored-by: Petrus Han <petrus.hanks@gmail.com>
Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com>
Co-authored-by: Kalo Chin <frog.beepers.0n@icloud.com>
Co-authored-by: Ujjwal Maurya <ujjwalsbx@gmail.com>
Co-authored-by: Maries <xh001x@hotmail.com>
This commit is contained in:
lyzno1
2025-08-27 15:07:28 +08:00
committed by GitHub
parent a63d1e87b1
commit 5bbf685035
625 changed files with 23778 additions and 10693 deletions

View File

@ -564,3 +564,7 @@ QUEUE_MONITOR_THRESHOLD=200
QUEUE_MONITOR_ALERT_EMAILS=
# Monitor interval in minutes, default is 30 minutes
QUEUE_MONITOR_INTERVAL=30
# Swagger UI configuration
SWAGGER_UI_ENABLED=true
SWAGGER_UI_PATH=/swagger-ui.html

View File

@ -43,6 +43,7 @@ select = [
"S302", # suspicious-marshal-usage, disallow use of `marshal` module
"S311", # suspicious-non-cryptographic-random-usage
"G001", # don't use str format to logging messages
"G003", # don't use + in logging messages
"G004", # don't use f-strings to format logging messages
]

View File

@ -80,7 +80,7 @@
1. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.
```bash
uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin,workflow_storage
uv run celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation
```
Addition, if you want to debug the celery scheduled tasks, you can use the following command in another terminal:
@ -97,8 +97,16 @@ uv run celery -A app.celery beat
uv sync --dev
```
1. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
1. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`, more can check [Claude.md](../CLAUDE.md)
```bash
uv run -P api bash dev/pytest/pytest_all_tests.sh
uv run pytest # Run all tests
uv run pytest tests/unit_tests/ # Unit tests only
uv run pytest tests/integration_tests/ # Integration tests
# Code quality
../dev/reformat # Run all formatters and linters
uv run ruff check --fix ./ # Fix linting issues
uv run ruff format ./ # Format code
uv run mypy . # Type checking
```

View File

@ -5,6 +5,8 @@ from configs import dify_config
from contexts.wrapper import RecyclableContextVar
from dify_app import DifyApp
logger = logging.getLogger(__name__)
# ----------------------------
# Application Factory Function
@ -32,7 +34,7 @@ def create_app() -> DifyApp:
initialize_extensions(app)
end_time = time.perf_counter()
if dify_config.DEBUG:
logging.info("Finished create_app (%s ms)", round((end_time - start_time) * 1000, 2))
logger.info("Finished create_app (%s ms)", round((end_time - start_time) * 1000, 2))
return app
@ -93,14 +95,14 @@ def initialize_extensions(app: DifyApp):
is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
if not is_enabled:
if dify_config.DEBUG:
logging.info("Skipped %s", short_name)
logger.info("Skipped %s", short_name)
continue
start_time = time.perf_counter()
ext.init_app(app)
end_time = time.perf_counter()
if dify_config.DEBUG:
logging.info("Loaded %s (%s ms)", short_name, round((end_time - start_time) * 1000, 2))
logger.info("Loaded %s (%s ms)", short_name, round((end_time - start_time) * 1000, 2))
def create_migrations_app():

11
api/child_class.py Normal file
View File

@ -0,0 +1,11 @@
from tests.integration_tests.utils.parent_class import ParentClass
class ChildClass(ParentClass):
"""Test child class for module import helper tests"""
def __init__(self, name):
super().__init__(name)
def get_name(self):
return f"Child: {self.name}"

View File

@ -38,6 +38,8 @@ from services.plugin.data_migration import PluginDataMigration
from services.plugin.plugin_migration import PluginMigration
from tasks.remove_app_and_related_data_task import delete_draft_variables_batch
logger = logging.getLogger(__name__)
@click.command("reset-password", help="Reset the account password.")
@click.option("--email", prompt=True, help="Account email to reset password for")
@ -685,7 +687,7 @@ def upgrade_db():
click.echo(click.style("Database migration successful!", fg="green"))
except Exception:
logging.exception("Failed to execute database migration")
logger.exception("Failed to execute database migration")
finally:
lock.release()
else:
@ -733,7 +735,7 @@ where sites.id is null limit 1000"""
except Exception:
failed_app_ids.append(app_id)
click.echo(click.style(f"Failed to fix missing site for app {app_id}", fg="red"))
logging.exception("Failed to fix app related site missing issue, app_id: %s", app_id)
logger.exception("Failed to fix app related site missing issue, app_id: %s", app_id)
continue
if not processed_count:

View File

@ -1,4 +1,4 @@
from typing import Annotated, Literal, Optional
from typing import Literal, Optional
from pydantic import (
AliasChoices,
@ -976,6 +976,18 @@ class WorkflowLogConfig(BaseSettings):
)
class SwaggerUIConfig(BaseSettings):
SWAGGER_UI_ENABLED: bool = Field(
description="Whether to enable Swagger UI in api module",
default=True,
)
SWAGGER_UI_PATH: str = Field(
description="Swagger UI page path in api module",
default="/swagger-ui.html",
)
class FeatureConfig(
# place the configs in alphabet order
AppExecutionConfig,
@ -1007,6 +1019,7 @@ class FeatureConfig(
WorkspaceConfig,
LoginConfig,
AccountConfig,
SwaggerUIConfig,
# hosted services config
HostedServiceConfig,
CeleryBeatConfig,

View File

@ -215,6 +215,7 @@ class DatabaseConfig(BaseSettings):
"pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING,
"connect_args": connect_args,
"pool_use_lifo": self.SQLALCHEMY_POOL_USE_LIFO,
"pool_reset_on_return": None,
}

View File

@ -1,4 +1,4 @@
from flask_restful import fields
from flask_restx import Api, Namespace, fields
from libs.helper import AppIconUrlField
@ -10,6 +10,12 @@ parameters__system_parameters = {
"workflow_file_upload_limit": fields.Integer,
}
def build_system_parameters_model(api_or_ns: Api | Namespace):
"""Build the system parameters model for the API or Namespace."""
return api_or_ns.model("SystemParameters", parameters__system_parameters)
parameters_fields = {
"opening_statement": fields.String,
"suggested_questions": fields.Raw,
@ -25,6 +31,14 @@ parameters_fields = {
"system_parameters": fields.Nested(parameters__system_parameters),
}
def build_parameters_model(api_or_ns: Api | Namespace):
"""Build the parameters model for the API or Namespace."""
copied_fields = parameters_fields.copy()
copied_fields["system_parameters"] = fields.Nested(build_system_parameters_model(api_or_ns))
return api_or_ns.model("Parameters", copied_fields)
site_fields = {
"title": fields.String,
"chat_color_theme": fields.String,
@ -41,3 +55,8 @@ site_fields = {
"show_workflow_steps": fields.Boolean,
"use_icon_as_answer_icon": fields.Boolean,
}
def build_site_model(api_or_ns: Api | Namespace):
"""Build the site model for the API or Namespace."""
return api_or_ns.model("Site", site_fields)

View File

@ -85,7 +85,6 @@ from .datasets import (
external,
hit_testing,
metadata,
upload_file,
website,
)

View File

@ -1,7 +1,7 @@
from functools import wraps
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound, Unauthorized

View File

@ -1,8 +1,8 @@
from typing import Any, Optional
import flask_restful
import flask_restx
from flask_login import current_user
from flask_restful import Resource, fields, marshal_with
from flask_restx import Resource, fields, marshal_with
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
@ -40,7 +40,7 @@ def _get_resource(resource_id, tenant_id, resource_model):
).scalar_one_or_none()
if resource is None:
flask_restful.abort(404, message=f"{resource_model.__name__} not found.")
flask_restx.abort(404, message=f"{resource_model.__name__} not found.")
return resource
@ -81,7 +81,7 @@ class BaseApiKeyListResource(Resource):
)
if current_key_count >= self.max_keys:
flask_restful.abort(
flask_restx.abort(
400,
message=f"Cannot create more than {self.max_keys} API keys for this resource type.",
code="max_keys_exceeded",
@ -126,7 +126,7 @@ class BaseApiKeyResource(Resource):
)
if key is None:
flask_restful.abort(404, message="API key not found")
flask_restx.abort(404, message="API key not found")
db.session.query(ApiToken).where(ApiToken.id == api_key_id).delete()
db.session.commit()

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model

View File

@ -2,7 +2,7 @@ from typing import Literal
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal, marshal_with, reqparse
from flask_restx import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden
from controllers.common.errors import NoFileUploadedError, TooManyFilesError

View File

@ -2,7 +2,7 @@ import uuid
from typing import cast
from flask_login import current_user
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
from flask_restx import Resource, inputs, marshal, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, Forbidden, abort

View File

@ -1,7 +1,7 @@
from typing import cast
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden

View File

@ -1,7 +1,7 @@
import logging
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import InternalServerError
import services
@ -31,6 +31,8 @@ from services.errors.audio import (
UnsupportedAudioTypeServiceError,
)
logger = logging.getLogger(__name__)
class ChatMessageAudioApi(Resource):
@setup_required
@ -49,7 +51,7 @@ class ChatMessageAudioApi(Resource):
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except NoAudioUploadedServiceError:
raise NoAudioUploadedError()
@ -70,7 +72,7 @@ class ChatMessageAudioApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("Failed to handle post request to ChatMessageAudioApi")
logger.exception("Failed to handle post request to ChatMessageAudioApi")
raise InternalServerError()
@ -97,7 +99,7 @@ class ChatMessageTextApi(Resource):
)
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except NoAudioUploadedServiceError:
raise NoAudioUploadedError()
@ -118,7 +120,7 @@ class ChatMessageTextApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("Failed to handle post request to ChatMessageTextApi")
logger.exception("Failed to handle post request to ChatMessageTextApi")
raise InternalServerError()
@ -160,7 +162,7 @@ class TextModesApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("Failed to handle get request to TextModesApi")
logger.exception("Failed to handle get request to TextModesApi")
raise InternalServerError()

View File

@ -2,7 +2,7 @@ import logging
import flask_login
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
@ -34,6 +34,8 @@ from models.model import AppMode
from services.app_generate_service import AppGenerateService
from services.errors.llm import InvokeRateLimitError
logger = logging.getLogger(__name__)
# define completion message api for user
class CompletionMessageApi(Resource):
@ -67,7 +69,7 @@ class CompletionMessageApi(Resource):
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -80,7 +82,7 @@ class CompletionMessageApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -134,7 +136,7 @@ class ChatMessageApi(Resource):
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -149,7 +151,7 @@ class ChatMessageApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()

View File

@ -2,8 +2,8 @@ from datetime import datetime
import pytz # pip install pytz
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import Resource, marshal_with, reqparse
from flask_restx.inputs import int_range
from sqlalchemy import func, or_
from sqlalchemy.orm import joinedload
from werkzeug.exceptions import Forbidden, NotFound
@ -24,6 +24,8 @@ from libs.helper import DatetimeString
from libs.login import login_required
from models import Conversation, EndUser, Message, MessageAnnotation
from models.model import AppMode
from services.conversation_service import ConversationService
from services.errors.conversation import ConversationNotExistsError
class CompletionConversationApi(Resource):
@ -46,7 +48,9 @@ class CompletionConversationApi(Resource):
parser.add_argument("limit", type=int_range(1, 100), default=20, location="args")
args = parser.parse_args()
query = db.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.mode == "completion")
query = db.select(Conversation).where(
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
)
if args["keyword"]:
query = query.join(Message, Message.conversation_id == Conversation.id).where(
@ -119,18 +123,11 @@ class CompletionConversationDetailApi(Resource):
raise Forbidden()
conversation_id = str(conversation_id)
conversation = (
db.session.query(Conversation)
.where(Conversation.id == conversation_id, Conversation.app_id == app_model.id)
.first()
)
if not conversation:
try:
ConversationService.delete(app_model, conversation_id, current_user)
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
conversation.is_deleted = True
db.session.commit()
return {"result": "success"}, 204
@ -171,7 +168,7 @@ class ChatConversationApi(Resource):
.subquery()
)
query = db.select(Conversation).where(Conversation.app_id == app_model.id)
query = db.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
if args["keyword"]:
keyword_filter = f"%{args['keyword']}%"
@ -284,18 +281,11 @@ class ChatConversationDetailApi(Resource):
raise Forbidden()
conversation_id = str(conversation_id)
conversation = (
db.session.query(Conversation)
.where(Conversation.id == conversation_id, Conversation.app_id == app_model.id)
.first()
)
if not conversation:
try:
ConversationService.delete(app_model, conversation_id, current_user)
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
conversation.is_deleted = True
db.session.commit()
return {"result": "success"}, 204

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session

View File

@ -1,7 +1,7 @@
from collections.abc import Sequence
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.app.error import (

View File

@ -2,7 +2,7 @@ import json
from enum import StrEnum
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from werkzeug.exceptions import NotFound
from controllers.console import api

View File

@ -1,8 +1,8 @@
import logging
from flask_login import current_user
from flask_restful import Resource, fields, marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import Resource, fields, marshal_with, reqparse
from flask_restx.inputs import int_range
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
from controllers.console import api
@ -33,6 +33,8 @@ from services.errors.conversation import ConversationNotExistsError
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
from services.message_service import MessageService
logger = logging.getLogger(__name__)
class ChatMessageListApi(Resource):
message_infinite_scroll_pagination_fields = {
@ -215,7 +217,7 @@ class MessageSuggestedQuestionApi(Resource):
except SuggestedQuestionsAfterAnswerDisabledError:
raise AppSuggestedQuestionsAfterAnswerDisabledError()
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
return {"data": questions}

View File

@ -3,7 +3,7 @@ from typing import cast
from flask import request
from flask_login import current_user
from flask_restful import Resource
from flask_restx import Resource
from controllers.console import api
from controllers.console.app.wraps import get_app_model

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import BadRequest
from controllers.console import api

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound
from constants.languages import supported_language

View File

@ -5,7 +5,7 @@ import pytz
import sqlalchemy as sa
from flask import jsonify
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model

View File

@ -4,7 +4,7 @@ from collections.abc import Sequence
from typing import cast
from flask import abort, request
from flask_restful import Resource, inputs, marshal_with, reqparse
from flask_restx import Resource, inputs, marshal_with, reqparse
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
@ -72,6 +72,7 @@ class DraftWorkflowApi(Resource):
Get draft workflow
"""
# The role of the current user in the ta table must be admin, owner, or editor
assert isinstance(current_user, Account)
if not current_user.is_editor:
raise Forbidden()
@ -94,6 +95,7 @@ class DraftWorkflowApi(Resource):
Sync draft workflow
"""
# The role of the current user in the ta table must be admin, owner, or editor
assert isinstance(current_user, Account)
if not current_user.is_editor:
raise Forbidden()
@ -171,6 +173,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
Run draft workflow
"""
# The role of the current user in the ta table must be admin, owner, or editor
assert isinstance(current_user, Account)
if not current_user.is_editor:
raise Forbidden()
@ -205,7 +208,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -218,13 +221,12 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
"""
Run draft workflow iteration node
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, location="json")
args = parser.parse_args()
@ -242,7 +244,7 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -256,11 +258,10 @@ class WorkflowDraftRunIterationNodeApi(Resource):
Run draft workflow iteration node
"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, location="json")
@ -279,7 +280,7 @@ class WorkflowDraftRunIterationNodeApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -292,12 +293,12 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
"""
Run draft workflow loop node
"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, location="json")
@ -316,7 +317,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -329,12 +330,12 @@ class WorkflowDraftRunLoopNodeApi(Resource):
"""
Run draft workflow loop node
"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, location="json")
@ -353,7 +354,7 @@ class WorkflowDraftRunLoopNodeApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -366,12 +367,12 @@ class DraftWorkflowRunApi(Resource):
"""
Run draft workflow
"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
@ -405,6 +406,9 @@ class WorkflowTaskStopApi(Resource):
"""
Stop workflow task
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
@ -424,12 +428,12 @@ class DraftWorkflowNodeRunApi(Resource):
"""
Run draft workflow node
"""
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
@ -472,6 +476,9 @@ class PublishedWorkflowApi(Resource):
"""
Get published workflow
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
@ -491,13 +498,12 @@ class PublishedWorkflowApi(Resource):
"""
Publish workflow
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("marked_name", type=str, required=False, default="", location="json")
parser.add_argument("marked_comment", type=str, required=False, default="", location="json")
@ -541,6 +547,9 @@ class DefaultBlockConfigsApi(Resource):
"""
Get default block config
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
@ -559,13 +568,12 @@ class DefaultBlockConfigApi(Resource):
"""
Get default block config
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("q", type=str, location="args")
args = parser.parse_args()
@ -595,13 +603,12 @@ class ConvertToWorkflowApi(Resource):
Convert expert mode of chatbot app to workflow mode
Convert Completion App to Workflow App
"""
if not isinstance(current_user, Account):
raise Forbidden()
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
if request.data:
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=False, nullable=True, location="json")
@ -645,6 +652,9 @@ class PublishedAllWorkflowApi(Resource):
"""
Get published workflows
"""
if not isinstance(current_user, Account):
raise Forbidden()
if not current_user.is_editor:
raise Forbidden()
@ -693,13 +703,12 @@ class WorkflowByIdApi(Resource):
"""
Update workflow attributes
"""
if not isinstance(current_user, Account):
raise Forbidden()
# Check permission
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("marked_name", type=str, required=False, location="json")
parser.add_argument("marked_comment", type=str, required=False, location="json")
@ -750,13 +759,12 @@ class WorkflowByIdApi(Resource):
"""
Delete workflow
"""
if not isinstance(current_user, Account):
raise Forbidden()
# Check permission
if not current_user.is_editor:
raise Forbidden()
if not isinstance(current_user, Account):
raise Forbidden()
workflow_service = WorkflowService()
# Create a session and manage the transaction

View File

@ -1,6 +1,6 @@
from dateutil.parser import isoparse
from flask_restful import Resource, marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import Resource, marshal_with, reqparse
from flask_restx.inputs import int_range
from sqlalchemy.orm import Session
from controllers.console import api

View File

@ -2,7 +2,7 @@ import logging
from typing import Any, NoReturn
from flask import Response
from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse
from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqparse
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
@ -21,6 +21,7 @@ from factories.file_factory import build_from_mapping, build_from_mappings
from factories.variable_factory import build_segment_with_type
from libs.login import current_user, login_required
from models import App, AppMode, db
from models.account import Account
from models.workflow import WorkflowDraftVariable
from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService
from services.workflow_service import WorkflowService
@ -135,6 +136,7 @@ def _api_prerequisite(f):
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def wrapper(*args, **kwargs):
assert isinstance(current_user, Account)
if not current_user.is_editor:
raise Forbidden()
return f(*args, **kwargs)

View File

@ -1,8 +1,8 @@
from typing import cast
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import Resource, marshal_with, reqparse
from flask_restx.inputs import int_range
from controllers.console import api
from controllers.console.app.wraps import get_app_model

View File

@ -5,7 +5,7 @@ import pytz
import sqlalchemy as sa
from flask import jsonify
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model

View File

@ -6,9 +6,11 @@ from controllers.console.app.error import AppNotFoundError
from extensions.ext_database import db
from libs.login import current_user
from models import App, AppMode
from models.account import Account
def _load_app_model(app_id: str) -> Optional[App]:
assert isinstance(current_user, Account)
app_model = (
db.session.query(App)
.where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from constants.languages import supported_language
from controllers.console import api

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api

View File

@ -3,7 +3,7 @@ import logging
import requests
from flask import current_app, redirect, request
from flask_login import current_user
from flask_restful import Resource
from flask_restx import Resource
from werkzeug.exceptions import Forbidden
from configs import dify_config
@ -13,6 +13,8 @@ from libs.oauth_data_source import NotionOAuth
from ..wraps import account_initialization_required, setup_required
logger = logging.getLogger(__name__)
def get_oauth_providers():
with current_app.app_context():
@ -80,7 +82,7 @@ class OAuthDataSourceBinding(Resource):
try:
oauth_provider.get_access_token(code)
except requests.exceptions.HTTPError as e:
logging.exception(
logger.exception(
"An error occurred during the OAuthCallback process with %s: %s", provider, e.response.text
)
return {"error": "OAuth data source process failed"}, 400
@ -103,7 +105,7 @@ class OAuthDataSourceSync(Resource):
try:
oauth_provider.sync_data_source(binding_id)
except requests.exceptions.HTTPError as e:
logging.exception(
logger.exception(
"An error occurred during the OAuthCallback process with %s: %s", provider, e.response.text
)
return {"error": "OAuth data source process failed"}, 400

View File

@ -55,6 +55,12 @@ class EmailOrPasswordMismatchError(BaseHTTPException):
code = 400
class AuthenticationFailedError(BaseHTTPException):
error_code = "authentication_failed"
description = "Invalid email or password."
code = 401
class EmailPasswordLoginLimitError(BaseHTTPException):
error_code = "email_code_login_limit"
description = "Too many incorrect password attempts. Please try again later."

View File

@ -2,7 +2,7 @@ import base64
import secrets
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session

View File

@ -2,15 +2,15 @@ from typing import cast
import flask_login
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
import services
from configs import dify_config
from constants.languages import languages
from controllers.console import api
from controllers.console.auth.error import (
AuthenticationFailedError,
EmailCodeError,
EmailOrPasswordMismatchError,
EmailPasswordLoginLimitError,
InvalidEmailError,
InvalidTokenError,
@ -79,7 +79,7 @@ class LoginApi(Resource):
raise AccountBannedError()
except services.errors.account.AccountPasswordError:
AccountService.add_login_error_rate_limit(args["email"])
raise EmailOrPasswordMismatchError()
raise AuthenticationFailedError()
except services.errors.account.AccountNotFoundError:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
@ -132,6 +132,7 @@ class ResetPasswordSendEmailApi(Resource):
account = AccountService.get_user_through_email(args["email"])
except AccountRegisterError as are:
raise AccountInFreezeError()
if account is None:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
@ -221,7 +222,7 @@ class EmailCodeLoginApi(Resource):
email=user_email, name=user_email, interface_language=languages[0]
)
except WorkSpaceNotAllowedCreateError:
return NotAllowedCreateWorkspace()
raise NotAllowedCreateWorkspace()
except AccountRegisterError as are:
raise AccountInFreezeError()
except WorkspacesLimitExceededError:

View File

@ -3,7 +3,7 @@ from typing import Optional
import requests
from flask import current_app, redirect, request
from flask_restful import Resource
from flask_restx import Resource
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import Unauthorized
@ -24,6 +24,8 @@ from services.feature_service import FeatureService
from .. import api
logger = logging.getLogger(__name__)
def get_oauth_providers():
with current_app.app_context():
@ -80,7 +82,7 @@ class OAuthCallback(Resource):
user_info = oauth_provider.get_user_info(token)
except requests.exceptions.RequestException as e:
error_text = e.response.text if e.response else str(e)
logging.exception("An error occurred during the OAuth process with %s: %s", provider, error_text)
logger.exception("An error occurred during the OAuth process with %s: %s", provider, error_text)
return {"error": "OAuth process failed"}, 400
if invite_token and RegisterService.is_valid_invite_token(invite_token):

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required

View File

@ -1,6 +1,6 @@
from flask import request
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from libs.helper import extract_remote_ip
from libs.login import login_required

View File

@ -2,7 +2,7 @@ import json
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound

View File

@ -1,7 +1,7 @@
import flask_restful
import flask_restx
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal, marshal_with, reqparse
from flask_restx import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound
import services
@ -553,7 +553,7 @@ class DatasetIndexingStatusApi(Resource):
}
documents_status.append(marshal(document_dict, document_status_fields))
data = {"data": documents_status}
return data
return data, 200
class DatasetApiKeyApi(Resource):
@ -589,7 +589,7 @@ class DatasetApiKeyApi(Resource):
)
if current_key_count >= self.max_keys:
flask_restful.abort(
flask_restx.abort(
400,
message=f"Cannot create more than {self.max_keys} API keys for this resource type.",
code="max_keys_exceeded",
@ -629,7 +629,7 @@ class DatasetApiDeleteApi(Resource):
)
if key is None:
flask_restful.abort(404, message="API key not found")
flask_restx.abort(404, message="API key not found")
db.session.query(ApiToken).where(ApiToken.id == api_key_id).delete()
db.session.commit()

View File

@ -4,7 +4,7 @@ from typing import Literal, cast
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal, marshal_with, reqparse
from flask_restx import Resource, marshal, marshal_with, reqparse
from sqlalchemy import asc, desc, select
from werkzeug.exceptions import Forbidden, NotFound
@ -54,6 +54,8 @@ from models import Dataset, DatasetProcessRule, Document, DocumentSegment, Uploa
from services.dataset_service import DatasetService, DocumentService
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
logger = logging.getLogger(__name__)
class DocumentResource(Resource):
def get_document(self, dataset_id: str, document_id: str) -> Document:
@ -468,25 +470,11 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
return {"tokens": 0, "total_price": 0, "currency": "USD", "total_segments": 0, "preview": []}, 200
data_process_rule = documents[0].dataset_process_rule
data_process_rule_dict = data_process_rule.to_dict()
info_list = []
extract_settings = []
for document in documents:
if document.indexing_status in {"completed", "error"}:
raise DocumentAlreadyFinishedError()
data_source_info = document.data_source_info_dict
# format document files info
if data_source_info and "upload_file_id" in data_source_info:
file_id = data_source_info["upload_file_id"]
info_list.append(file_id)
# format document notion info
elif (
data_source_info and "notion_workspace_id" in data_source_info and "notion_page_id" in data_source_info
):
pages = []
page = {"page_id": data_source_info["notion_page_id"], "type": data_source_info["type"]}
pages.append(page)
notion_info = {"workspace_id": data_source_info["notion_workspace_id"], "pages": pages}
info_list.append(notion_info)
if document.data_source_type == "upload_file":
file_id = data_source_info["upload_file_id"]
@ -966,7 +954,7 @@ class DocumentRetryApi(DocumentResource):
raise DocumentAlreadyFinishedError()
retry_documents.append(document)
except Exception:
logging.exception("Failed to retry document, document id: %s", document_id)
logger.exception("Failed to retry document, document id: %s", document_id)
continue
# retry document
DocumentService.retry_document(dataset_id, retry_documents)

View File

@ -2,7 +2,7 @@ import uuid
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal, reqparse
from flask_restx import Resource, marshal, reqparse
from sqlalchemy import select
from werkzeug.exceptions import Forbidden, NotFound
@ -584,7 +584,12 @@ class ChildChunkUpdateApi(Resource):
child_chunk_id = str(child_chunk_id)
child_chunk = (
db.session.query(ChildChunk)
.where(ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id)
.where(
ChildChunk.id == str(child_chunk_id),
ChildChunk.tenant_id == current_user.current_tenant_id,
ChildChunk.segment_id == segment.id,
ChildChunk.document_id == document_id,
)
.first()
)
if not child_chunk:
@ -633,7 +638,12 @@ class ChildChunkUpdateApi(Resource):
child_chunk_id = str(child_chunk_id)
child_chunk = (
db.session.query(ChildChunk)
.where(ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id)
.where(
ChildChunk.id == str(child_chunk_id),
ChildChunk.tenant_id == current_user.current_tenant_id,
ChildChunk.segment_id == segment.id,
ChildChunk.document_id == document_id,
)
.first()
)
if not child_chunk:

View File

@ -1,6 +1,6 @@
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal, reqparse
from flask_restx import Resource, marshal, reqparse
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services

View File

@ -1,4 +1,4 @@
from flask_restful import Resource
from flask_restx import Resource
from controllers.console import api
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase

View File

@ -1,7 +1,7 @@
import logging
from flask_login import current_user
from flask_restful import marshal, reqparse
from flask_restx import marshal, reqparse
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services.dataset_service
@ -23,6 +23,8 @@ from fields.hit_testing_fields import hit_testing_record_fields
from services.dataset_service import DatasetService
from services.hit_testing_service import HitTestingService
logger = logging.getLogger(__name__)
class DatasetsHitTestingBase:
@staticmethod
@ -81,5 +83,5 @@ class DatasetsHitTestingBase:
except ValueError as e:
raise ValueError(str(e))
except Exception as e:
logging.exception("Hit testing failed.")
logger.exception("Hit testing failed.")
raise InternalServerError(str(e))

View File

@ -1,7 +1,7 @@
from typing import Literal
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from werkzeug.exceptions import NotFound
from controllers.console import api

View File

@ -1,62 +0,0 @@
from flask_login import current_user
from flask_restful import Resource
from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.wraps import (
account_initialization_required,
setup_required,
)
from core.file import helpers as file_helpers
from extensions.ext_database import db
from models.dataset import Dataset
from models.model import UploadFile
from services.dataset_service import DocumentService
class UploadFileApi(Resource):
@setup_required
@account_initialization_required
def get(self, dataset_id, document_id):
"""Get upload file."""
# check dataset
dataset_id = str(dataset_id)
dataset = (
db.session.query(Dataset)
.filter(Dataset.tenant_id == current_user.current_tenant_id, Dataset.id == dataset_id)
.first()
)
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check upload file
if document.data_source_type != "upload_file":
raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.")
data_source_info = document.data_source_info_dict
if data_source_info and "upload_file_id" in data_source_info:
file_id = data_source_info["upload_file_id"]
upload_file = db.session.query(UploadFile).where(UploadFile.id == file_id).first()
if not upload_file:
raise NotFound("UploadFile not found.")
else:
raise ValueError("Upload file id not found in document data source info.")
url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
return {
"id": upload_file.id,
"name": upload_file.name,
"size": upload_file.size,
"extension": upload_file.extension,
"url": url,
"download_url": f"{url}&as_attachment=true",
"mime_type": upload_file.mime_type,
"created_by": upload_file.created_by,
"created_at": upload_file.created_at.timestamp(),
}, 200
api.add_resource(UploadFileApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/upload-file")

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console import api
from controllers.console.datasets.error import WebsiteCrawlError

View File

@ -26,6 +26,8 @@ from services.errors.audio import (
UnsupportedAudioTypeServiceError,
)
logger = logging.getLogger(__name__)
class ChatAudioApi(InstalledAppResource):
def post(self, installed_app):
@ -38,7 +40,7 @@ class ChatAudioApi(InstalledAppResource):
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except NoAudioUploadedServiceError:
raise NoAudioUploadedError()
@ -59,13 +61,13 @@ class ChatAudioApi(InstalledAppResource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
class ChatTextApi(InstalledAppResource):
def post(self, installed_app):
from flask_restful import reqparse
from flask_restx import reqparse
app_model = installed_app.app
try:
@ -83,7 +85,7 @@ class ChatTextApi(InstalledAppResource):
response = AudioService.transcript_tts(app_model=app_model, text=text, voice=voice, message_id=message_id)
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except NoAudioUploadedServiceError:
raise NoAudioUploadedError()
@ -104,5 +106,5 @@ class ChatTextApi(InstalledAppResource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()

View File

@ -1,7 +1,7 @@
import logging
from flask_login import current_user
from flask_restful import reqparse
from flask_restx import reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
@ -32,6 +32,8 @@ from models.model import AppMode
from services.app_generate_service import AppGenerateService
from services.errors.llm import InvokeRateLimitError
logger = logging.getLogger(__name__)
# define completion api for user
class CompletionApi(InstalledAppResource):
@ -65,7 +67,7 @@ class CompletionApi(InstalledAppResource):
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -78,7 +80,7 @@ class CompletionApi(InstalledAppResource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -125,7 +127,7 @@ class ChatApi(InstalledAppResource):
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -140,7 +142,7 @@ class ChatApi(InstalledAppResource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()

View File

@ -1,6 +1,6 @@
from flask_login import current_user
from flask_restful import marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import marshal_with, reqparse
from flask_restx.inputs import int_range
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound

View File

@ -3,7 +3,7 @@ from typing import Any
from flask import request
from flask_login import current_user
from flask_restful import Resource, inputs, marshal_with, reqparse
from flask_restx import Resource, inputs, marshal_with, reqparse
from sqlalchemy import and_
from werkzeug.exceptions import BadRequest, Forbidden, NotFound

View File

@ -1,8 +1,8 @@
import logging
from flask_login import current_user
from flask_restful import marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import marshal_with, reqparse
from flask_restx.inputs import int_range
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.console.app.error import (
@ -35,6 +35,8 @@ from services.errors.message import (
)
from services.message_service import MessageService
logger = logging.getLogger(__name__)
class MessageListApi(InstalledAppResource):
@marshal_with(message_infinite_scroll_pagination_fields)
@ -126,7 +128,7 @@ class MessageMoreLikeThisApi(InstalledAppResource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -158,7 +160,7 @@ class MessageSuggestedQuestionApi(InstalledAppResource):
except InvokeError as e:
raise CompletionRequestError(e.description)
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
return {"data": questions}

View File

@ -1,4 +1,4 @@
from flask_restful import marshal_with
from flask_restx import marshal_with
from controllers.common import fields
from controllers.console import api

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource, fields, marshal_with, reqparse
from flask_restx import Resource, fields, marshal_with, reqparse
from constants.languages import languages
from controllers.console import api

View File

@ -1,6 +1,6 @@
from flask_login import current_user
from flask_restful import fields, marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import fields, marshal_with, reqparse
from flask_restx.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.console import api

View File

@ -1,6 +1,6 @@
import logging
from flask_restful import reqparse
from flask_restx import reqparse
from werkzeug.exceptions import InternalServerError
from controllers.console.app.error import (
@ -43,7 +43,7 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json")
parser.add_argument("files", type=list, required=False, location="json")
args = parser.parse_args()
assert current_user is not None
try:
response = AppGenerateService.generate(
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True
@ -63,7 +63,7 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@ -76,6 +76,7 @@ class InstalledAppWorkflowTaskStopApi(InstalledAppResource):
app_mode = AppMode.value_of(app_model.mode)
if app_mode != AppMode.WORKFLOW:
raise NotWorkflowAppError()
assert current_user is not None
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)

View File

@ -1,7 +1,7 @@
from functools import wraps
from flask_login import current_user
from flask_restful import Resource
from flask_restx import Resource
from werkzeug.exceptions import NotFound
from controllers.console.explore.error import AppAccessDeniedError

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from constants import HIDDEN_VALUE
from controllers.console import api

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource
from flask_restx import Resource
from libs.login import login_required
from services.feature_service import FeatureService

View File

@ -2,7 +2,7 @@ from typing import Literal
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal_with
from flask_restx import Resource, marshal_with
from werkzeug.exceptions import Forbidden
import services

View File

@ -1,7 +1,7 @@
import os
from flask import session
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session

View File

@ -1,4 +1,4 @@
from flask_restful import Resource
from flask_restx import Resource
from controllers.console import api

View File

@ -3,7 +3,7 @@ from typing import cast
import httpx
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
import services
from controllers.common import helpers

View File

@ -1,5 +1,5 @@
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from configs import dify_config
from libs.helper import StrLen, email, extract_remote_ip

View File

@ -1,11 +1,11 @@
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restx import Resource, marshal_with, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
from fields.tag_fields import tag_fields
from fields.tag_fields import dataset_tag_fields
from libs.login import login_required
from models.model import Tag
from services.tag_service import TagService
@ -21,7 +21,7 @@ class TagListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@marshal_with(tag_fields)
@marshal_with(dataset_tag_fields)
def get(self):
tag_type = request.args.get("type", type=str, default="")
keyword = request.args.get("keyword", default=None, type=str)

View File

@ -2,13 +2,15 @@ import json
import logging
import requests
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from packaging import version
from configs import dify_config
from . import api
logger = logging.getLogger(__name__)
class VersionApi(Resource):
def get(self):
@ -34,7 +36,7 @@ class VersionApi(Resource):
try:
response = requests.get(check_update_url, {"current_version": args.get("current_version")}, timeout=(3, 10))
except Exception as error:
logging.warning("Check update version error: %s.", str(error))
logger.warning("Check update version error: %s.", str(error))
result["version"] = args.get("current_version")
return result
@ -55,7 +57,7 @@ def _has_new_version(*, latest_version: str, current_version: str) -> bool:
# Compare versions
return latest > current
except version.InvalidVersion:
logging.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version)
logger.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version)
return False

View File

@ -3,7 +3,7 @@ from datetime import datetime
import pytz
from flask import request
from flask_login import current_user
from flask_restful import Resource, fields, marshal_with, reqparse
from flask_restx import Resource, fields, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource
from flask_restx import Resource
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required

View File

@ -1,5 +1,5 @@
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
@ -6,7 +6,7 @@ from controllers.console.wraps import account_initialization_required, setup_req
from core.model_runtime.entities.model_entities import ModelType
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from libs.login import current_user, login_required
from models.account import TenantAccountRole
from models.account import Account, TenantAccountRole
from services.model_load_balancing_service import ModelLoadBalancingService
@ -15,10 +15,12 @@ class LoadBalancingCredentialsValidateApi(Resource):
@login_required
@account_initialization_required
def post(self, provider: str):
assert isinstance(current_user, Account)
if not TenantAccountRole.is_privileged_role(current_user.current_role):
raise Forbidden()
tenant_id = current_user.current_tenant_id
assert tenant_id is not None
parser = reqparse.RequestParser()
parser.add_argument("model", type=str, required=True, nullable=False, location="json")
@ -64,10 +66,12 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
@login_required
@account_initialization_required
def post(self, provider: str, config_id: str):
assert isinstance(current_user, Account)
if not TenantAccountRole.is_privileged_role(current_user.current_role):
raise Forbidden()
tenant_id = current_user.current_tenant_id
assert tenant_id is not None
parser = reqparse.RequestParser()
parser.add_argument("model", type=str, required=True, nullable=False, location="json")

View File

@ -2,7 +2,7 @@ from urllib import parse
from flask import request
from flask_login import current_user
from flask_restful import Resource, abort, marshal_with, reqparse
from flask_restx import Resource, abort, marshal_with, reqparse
import services
from configs import dify_config
@ -54,7 +54,7 @@ class MemberInviteEmailApi(Resource):
@cloud_edition_billing_resource_check("members")
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("emails", type=str, required=True, location="json", action="append")
parser.add_argument("emails", type=list, required=True, location="json")
parser.add_argument("role", type=str, required=True, default="admin", location="json")
parser.add_argument("language", type=str, required=False, location="json")
args = parser.parse_args()

View File

@ -2,7 +2,7 @@ import io
from flask import send_file
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
@ -10,6 +10,7 @@ from controllers.console.wraps import account_initialization_required, setup_req
from core.model_runtime.entities.model_entities import ModelType
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import StrLen, uuid_value
from libs.login import login_required
from services.billing_service import BillingService
from services.model_provider_service import ModelProviderService
@ -45,12 +46,109 @@ class ModelProviderCredentialApi(Resource):
@account_initialization_required
def get(self, provider: str):
tenant_id = current_user.current_tenant_id
# if credential_id is not provided, return current used credential
parser = reqparse.RequestParser()
parser.add_argument("credential_id", type=uuid_value, required=False, nullable=True, location="args")
args = parser.parse_args()
model_provider_service = ModelProviderService()
credentials = model_provider_service.get_provider_credentials(tenant_id=tenant_id, provider=provider)
credentials = model_provider_service.get_provider_credential(
tenant_id=tenant_id, provider=provider, credential_id=args.get("credential_id")
)
return {"credentials": credentials}
@setup_required
@login_required
@account_initialization_required
def post(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
args = parser.parse_args()
model_provider_service = ModelProviderService()
try:
model_provider_service.create_provider_credential(
tenant_id=current_user.current_tenant_id,
provider=provider,
credentials=args["credentials"],
credential_name=args["name"],
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
return {"result": "success"}, 201
@setup_required
@login_required
@account_initialization_required
def put(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("credential_id", type=uuid_value, required=True, nullable=False, location="json")
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
args = parser.parse_args()
model_provider_service = ModelProviderService()
try:
model_provider_service.update_provider_credential(
tenant_id=current_user.current_tenant_id,
provider=provider,
credentials=args["credentials"],
credential_id=args["credential_id"],
credential_name=args["name"],
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
return {"result": "success"}
@setup_required
@login_required
@account_initialization_required
def delete(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("credential_id", type=uuid_value, required=True, nullable=False, location="json")
args = parser.parse_args()
model_provider_service = ModelProviderService()
model_provider_service.remove_provider_credential(
tenant_id=current_user.current_tenant_id, provider=provider, credential_id=args["credential_id"]
)
return {"result": "success"}, 204
class ModelProviderCredentialSwitchApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("credential_id", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
service = ModelProviderService()
service.switch_active_provider_credential(
tenant_id=current_user.current_tenant_id,
provider=provider,
credential_id=args["credential_id"],
)
return {"result": "success"}
class ModelProviderValidateApi(Resource):
@setup_required
@ -69,7 +167,7 @@ class ModelProviderValidateApi(Resource):
error = ""
try:
model_provider_service.provider_credentials_validate(
model_provider_service.validate_provider_credentials(
tenant_id=tenant_id, provider=provider, credentials=args["credentials"]
)
except CredentialsValidateFailedError as ex:
@ -84,42 +182,6 @@ class ModelProviderValidateApi(Resource):
return response
class ModelProviderApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
args = parser.parse_args()
model_provider_service = ModelProviderService()
try:
model_provider_service.save_provider_credentials(
tenant_id=current_user.current_tenant_id, provider=provider, credentials=args["credentials"]
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
return {"result": "success"}, 201
@setup_required
@login_required
@account_initialization_required
def delete(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
model_provider_service = ModelProviderService()
model_provider_service.remove_provider_credentials(tenant_id=current_user.current_tenant_id, provider=provider)
return {"result": "success"}, 204
class ModelProviderIconApi(Resource):
"""
Get model provider icon
@ -187,8 +249,10 @@ class ModelProviderPaymentCheckoutUrlApi(Resource):
api.add_resource(ModelProviderListApi, "/workspaces/current/model-providers")
api.add_resource(ModelProviderCredentialApi, "/workspaces/current/model-providers/<path:provider>/credentials")
api.add_resource(
ModelProviderCredentialSwitchApi, "/workspaces/current/model-providers/<path:provider>/credentials/switch"
)
api.add_resource(ModelProviderValidateApi, "/workspaces/current/model-providers/<path:provider>/credentials/validate")
api.add_resource(ModelProviderApi, "/workspaces/current/model-providers/<path:provider>")
api.add_resource(
PreferredProviderTypeUpdateApi, "/workspaces/current/model-providers/<path:provider>/preferred-provider-type"

View File

@ -1,7 +1,7 @@
import logging
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
@ -9,10 +9,13 @@ from controllers.console.wraps import account_initialization_required, setup_req
from core.model_runtime.entities.model_entities import ModelType
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.utils.encoders import jsonable_encoder
from libs.helper import StrLen, uuid_value
from libs.login import login_required
from services.model_load_balancing_service import ModelLoadBalancingService
from services.model_provider_service import ModelProviderService
logger = logging.getLogger(__name__)
class DefaultModelApi(Resource):
@setup_required
@ -72,7 +75,7 @@ class DefaultModelApi(Resource):
model=model_setting["model"],
)
except Exception as ex:
logging.exception(
logger.exception(
"Failed to update default model, model type: %s, model: %s",
model_setting["model_type"],
model_setting.get("model"),
@ -98,6 +101,7 @@ class ModelProviderModelApi(Resource):
@login_required
@account_initialization_required
def post(self, provider: str):
# To save the model's load balance configs
if not current_user.is_admin_or_owner:
raise Forbidden()
@ -113,22 +117,26 @@ class ModelProviderModelApi(Resource):
choices=[mt.value for mt in ModelType],
location="json",
)
parser.add_argument("credentials", type=dict, required=False, nullable=True, location="json")
parser.add_argument("load_balancing", type=dict, required=False, nullable=True, location="json")
parser.add_argument("config_from", type=str, required=False, nullable=True, location="json")
parser.add_argument("credential_id", type=uuid_value, required=False, nullable=True, location="json")
args = parser.parse_args()
if args.get("config_from", "") == "custom-model":
if not args.get("credential_id"):
raise ValueError("credential_id is required when configuring a custom-model")
service = ModelProviderService()
service.switch_active_custom_model_credential(
tenant_id=current_user.current_tenant_id,
provider=provider,
model_type=args["model_type"],
model=args["model"],
credential_id=args["credential_id"],
)
model_load_balancing_service = ModelLoadBalancingService()
if (
"load_balancing" in args
and args["load_balancing"]
and "enabled" in args["load_balancing"]
and args["load_balancing"]["enabled"]
):
if "configs" not in args["load_balancing"]:
raise ValueError("invalid load balancing configs")
if "load_balancing" in args and args["load_balancing"] and "configs" in args["load_balancing"]:
# save load balancing configs
model_load_balancing_service.update_load_balancing_configs(
tenant_id=tenant_id,
@ -136,37 +144,17 @@ class ModelProviderModelApi(Resource):
model=args["model"],
model_type=args["model_type"],
configs=args["load_balancing"]["configs"],
config_from=args.get("config_from", ""),
)
# enable load balancing
model_load_balancing_service.enable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args["model"], model_type=args["model_type"]
)
else:
# disable load balancing
model_load_balancing_service.disable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args["model"], model_type=args["model_type"]
)
if args.get("config_from", "") != "predefined-model":
model_provider_service = ModelProviderService()
try:
model_provider_service.save_model_credentials(
tenant_id=tenant_id,
provider=provider,
model=args["model"],
model_type=args["model_type"],
credentials=args["credentials"],
)
except CredentialsValidateFailedError as ex:
logging.exception(
"Failed to save model credentials, tenant_id: %s, model: %s, model_type: %s",
tenant_id,
args.get("model"),
args.get("model_type"),
)
raise ValueError(str(ex))
if args.get("load_balancing", {}).get("enabled"):
model_load_balancing_service.enable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args["model"], model_type=args["model_type"]
)
else:
model_load_balancing_service.disable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args["model"], model_type=args["model_type"]
)
return {"result": "success"}, 200
@ -192,7 +180,7 @@ class ModelProviderModelApi(Resource):
args = parser.parse_args()
model_provider_service = ModelProviderService()
model_provider_service.remove_model_credentials(
model_provider_service.remove_model(
tenant_id=tenant_id, provider=provider, model=args["model"], model_type=args["model_type"]
)
@ -216,11 +204,17 @@ class ModelProviderModelCredentialApi(Resource):
choices=[mt.value for mt in ModelType],
location="args",
)
parser.add_argument("config_from", type=str, required=False, nullable=True, location="args")
parser.add_argument("credential_id", type=uuid_value, required=False, nullable=True, location="args")
args = parser.parse_args()
model_provider_service = ModelProviderService()
credentials = model_provider_service.get_model_credentials(
tenant_id=tenant_id, provider=provider, model_type=args["model_type"], model=args["model"]
current_credential = model_provider_service.get_model_credential(
tenant_id=tenant_id,
provider=provider,
model_type=args["model_type"],
model=args["model"],
credential_id=args.get("credential_id"),
)
model_load_balancing_service = ModelLoadBalancingService()
@ -228,10 +222,173 @@ class ModelProviderModelCredentialApi(Resource):
tenant_id=tenant_id, provider=provider, model=args["model"], model_type=args["model_type"]
)
return {
"credentials": credentials,
"load_balancing": {"enabled": is_load_balancing_enabled, "configs": load_balancing_configs},
}
if args.get("config_from", "") == "predefined-model":
available_credentials = model_provider_service.provider_manager.get_provider_available_credentials(
tenant_id=tenant_id, provider_name=provider
)
else:
model_type = ModelType.value_of(args["model_type"]).to_origin_model_type()
available_credentials = model_provider_service.provider_manager.get_provider_model_available_credentials(
tenant_id=tenant_id, provider_name=provider, model_type=model_type, model_name=args["model"]
)
return jsonable_encoder(
{
"credentials": current_credential.get("credentials") if current_credential else {},
"current_credential_id": current_credential.get("current_credential_id")
if current_credential
else None,
"current_credential_name": current_credential.get("current_credential_name")
if current_credential
else None,
"load_balancing": {"enabled": is_load_balancing_enabled, "configs": load_balancing_configs},
"available_credentials": available_credentials,
}
)
@setup_required
@login_required
@account_initialization_required
def post(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("model", type=str, required=True, nullable=False, location="json")
parser.add_argument(
"model_type",
type=str,
required=True,
nullable=False,
choices=[mt.value for mt in ModelType],
location="json",
)
parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
args = parser.parse_args()
tenant_id = current_user.current_tenant_id
model_provider_service = ModelProviderService()
try:
model_provider_service.create_model_credential(
tenant_id=tenant_id,
provider=provider,
model=args["model"],
model_type=args["model_type"],
credentials=args["credentials"],
credential_name=args["name"],
)
except CredentialsValidateFailedError as ex:
logger.exception(
"Failed to save model credentials, tenant_id: %s, model: %s, model_type: %s",
tenant_id,
args.get("model"),
args.get("model_type"),
)
raise ValueError(str(ex))
return {"result": "success"}, 201
@setup_required
@login_required
@account_initialization_required
def put(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("model", type=str, required=True, nullable=False, location="json")
parser.add_argument(
"model_type",
type=str,
required=True,
nullable=False,
choices=[mt.value for mt in ModelType],
location="json",
)
parser.add_argument("credential_id", type=uuid_value, required=True, nullable=False, location="json")
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
args = parser.parse_args()
model_provider_service = ModelProviderService()
try:
model_provider_service.update_model_credential(
tenant_id=current_user.current_tenant_id,
provider=provider,
model_type=args["model_type"],
model=args["model"],
credentials=args["credentials"],
credential_id=args["credential_id"],
credential_name=args["name"],
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
return {"result": "success"}
@setup_required
@login_required
@account_initialization_required
def delete(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("model", type=str, required=True, nullable=False, location="json")
parser.add_argument(
"model_type",
type=str,
required=True,
nullable=False,
choices=[mt.value for mt in ModelType],
location="json",
)
parser.add_argument("credential_id", type=uuid_value, required=True, nullable=False, location="json")
args = parser.parse_args()
model_provider_service = ModelProviderService()
model_provider_service.remove_model_credential(
tenant_id=current_user.current_tenant_id,
provider=provider,
model_type=args["model_type"],
model=args["model"],
credential_id=args["credential_id"],
)
return {"result": "success"}, 204
class ModelProviderModelCredentialSwitchApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self, provider: str):
if not current_user.is_admin_or_owner:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("model", type=str, required=True, nullable=False, location="json")
parser.add_argument(
"model_type",
type=str,
required=True,
nullable=False,
choices=[mt.value for mt in ModelType],
location="json",
)
parser.add_argument("credential_id", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
service = ModelProviderService()
service.add_model_credential_to_model_list(
tenant_id=current_user.current_tenant_id,
provider=provider,
model_type=args["model_type"],
model=args["model"],
credential_id=args["credential_id"],
)
return {"result": "success"}
class ModelProviderModelEnableApi(Resource):
@ -314,7 +471,7 @@ class ModelProviderModelValidateApi(Resource):
error = ""
try:
model_provider_service.model_credentials_validate(
model_provider_service.validate_model_credentials(
tenant_id=tenant_id,
provider=provider,
model=args["model"],
@ -379,6 +536,10 @@ api.add_resource(
api.add_resource(
ModelProviderModelCredentialApi, "/workspaces/current/model-providers/<path:provider>/models/credentials"
)
api.add_resource(
ModelProviderModelCredentialSwitchApi,
"/workspaces/current/model-providers/<path:provider>/models/credentials/switch",
)
api.add_resource(
ModelProviderModelValidateApi, "/workspaces/current/model-providers/<path:provider>/models/credentials/validate"
)

View File

@ -2,7 +2,7 @@ import io
from flask import request, send_file
from flask_login import current_user
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden
from configs import dify_config

View File

@ -3,7 +3,7 @@ from urllib.parse import urlparse
from flask import make_response, redirect, request, send_file
from flask_login import current_user
from flask_restful import (
from flask_restx import (
Resource,
reqparse,
)
@ -95,7 +95,6 @@ class ToolBuiltinProviderInfoApi(Resource):
def get(self, provider):
user = current_user
user_id = user.id
tenant_id = user.current_tenant_id
return jsonable_encoder(BuiltinToolManageService.get_builtin_tool_provider_info(tenant_id, provider))

View File

@ -2,7 +2,7 @@ import logging
from flask import request
from flask_login import current_user
from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse
from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqparse
from sqlalchemy import select
from werkzeug.exceptions import Unauthorized
@ -31,6 +31,9 @@ from services.feature_service import FeatureService
from services.file_service import FileService
from services.workspace_service import WorkspaceService
logger = logging.getLogger(__name__)
provider_fields = {
"provider_name": fields.String,
"provider_type": fields.String,
@ -120,7 +123,7 @@ class TenantApi(Resource):
@marshal_with(tenant_fields)
def get(self):
if request.path == "/info":
logging.warning("Deprecated URL /info was used.")
logger.warning("Deprecated URL /info was used.")
tenant = current_user.current_tenant

View File

@ -1,9 +1,20 @@
from flask import Blueprint
from flask_restx import Namespace
from libs.external_api import ExternalApi
bp = Blueprint("files", __name__)
api = ExternalApi(bp)
bp = Blueprint("files", __name__, url_prefix="/files")
api = ExternalApi(
bp,
version="1.0",
title="Files API",
description="API for file operations including upload and preview",
doc="/docs", # Enable Swagger UI at /files/docs
)
files_ns = Namespace("files", description="File operations", path="/")
from . import image_preview, tool_files, upload
api.add_namespace(files_ns)

View File

@ -1,16 +1,17 @@
from urllib.parse import quote
from flask import Response, request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import NotFound
import services
from controllers.common.errors import UnsupportedFileTypeError
from controllers.files import api
from controllers.files import files_ns
from services.account_service import TenantService
from services.file_service import FileService
@files_ns.route("/<uuid:file_id>/image-preview")
class ImagePreviewApi(Resource):
"""
Deprecated
@ -39,6 +40,7 @@ class ImagePreviewApi(Resource):
return Response(generator, mimetype=mimetype)
@files_ns.route("/<uuid:file_id>/file-preview")
class FilePreviewApi(Resource):
def get(self, file_id):
file_id = str(file_id)
@ -94,6 +96,7 @@ class FilePreviewApi(Resource):
return response
@files_ns.route("/workspaces/<uuid:workspace_id>/webapp-logo")
class WorkspaceWebappLogoApi(Resource):
def get(self, workspace_id):
workspace_id = str(workspace_id)
@ -112,8 +115,3 @@ class WorkspaceWebappLogoApi(Resource):
raise UnsupportedFileTypeError()
return Response(generator, mimetype=mimetype)
api.add_resource(ImagePreviewApi, "/files/<uuid:file_id>/image-preview")
api.add_resource(FilePreviewApi, "/files/<uuid:file_id>/file-preview")
api.add_resource(WorkspaceWebappLogoApi, "/files/workspaces/<uuid:workspace_id>/webapp-logo")

View File

@ -1,17 +1,18 @@
from urllib.parse import quote
from flask import Response
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import Forbidden, NotFound
from controllers.common.errors import UnsupportedFileTypeError
from controllers.files import api
from controllers.files import files_ns
from core.tools.signature import verify_tool_file_signature
from core.tools.tool_file_manager import ToolFileManager
from models import db as global_db
class ToolFilePreviewApi(Resource):
@files_ns.route("/tools/<uuid:file_id>.<string:extension>")
class ToolFileApi(Resource):
def get(self, file_id, extension):
file_id = str(file_id)
@ -52,6 +53,3 @@ class ToolFilePreviewApi(Resource):
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
return response
api.add_resource(ToolFilePreviewApi, "/files/tools/<uuid:file_id>.<string:extension>")

View File

@ -1,7 +1,9 @@
from mimetypes import guess_extension
from typing import Optional
from flask import request
from flask_restful import Resource, marshal_with
from flask_restx import Resource, reqparse
from flask_restx.api import HTTPStatus
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden
import services
@ -10,39 +12,76 @@ from controllers.common.errors import (
UnsupportedFileTypeError,
)
from controllers.console.wraps import setup_required
from controllers.files import api
from controllers.files import files_ns
from controllers.inner_api.plugin.wraps import get_user
from core.file.helpers import verify_plugin_file_signature
from core.tools.tool_file_manager import ToolFileManager
from fields.file_fields import file_fields
from fields.file_fields import build_file_model
# Define parser for both documentation and validation
upload_parser = reqparse.RequestParser()
upload_parser.add_argument("file", location="files", type=FileStorage, required=True, help="File to upload")
upload_parser.add_argument(
"timestamp", type=str, required=True, location="args", help="Unix timestamp for signature verification"
)
upload_parser.add_argument(
"nonce", type=str, required=True, location="args", help="Random string for signature verification"
)
upload_parser.add_argument(
"sign", type=str, required=True, location="args", help="HMAC signature for request validation"
)
upload_parser.add_argument("tenant_id", type=str, required=True, location="args", help="Tenant identifier")
upload_parser.add_argument("user_id", type=str, required=False, location="args", help="User identifier")
@files_ns.route("/upload/for-plugin")
class PluginUploadFileApi(Resource):
@setup_required
@marshal_with(file_fields)
@files_ns.expect(upload_parser)
@files_ns.doc("upload_plugin_file")
@files_ns.doc(description="Upload a file for plugin usage with signature verification")
@files_ns.doc(
responses={
201: "File uploaded successfully",
400: "Invalid request parameters",
403: "Forbidden - Invalid signature or missing parameters",
413: "File too large",
415: "Unsupported file type",
}
)
@files_ns.marshal_with(build_file_model(files_ns), code=HTTPStatus.CREATED)
def post(self):
# get file from request
file = request.files["file"]
"""Upload a file for plugin usage.
timestamp = request.args.get("timestamp")
nonce = request.args.get("nonce")
sign = request.args.get("sign")
tenant_id = request.args.get("tenant_id")
if not tenant_id:
raise Forbidden("Invalid request.")
Accepts a file upload with signature verification for security.
The file must be accompanied by valid timestamp, nonce, and signature parameters.
user_id = request.args.get("user_id")
Returns:
dict: File metadata including ID, URLs, and properties
int: HTTP status code (201 for success)
Raises:
Forbidden: Invalid signature or missing required parameters
FileTooLargeError: File exceeds size limit
UnsupportedFileTypeError: File type not supported
"""
# Parse and validate all arguments
args = upload_parser.parse_args()
file: FileStorage = args["file"]
timestamp: str = args["timestamp"]
nonce: str = args["nonce"]
sign: str = args["sign"]
tenant_id: str = args["tenant_id"]
user_id: Optional[str] = args.get("user_id")
user = get_user(tenant_id, user_id)
filename = file.filename
mimetype = file.mimetype
filename: Optional[str] = file.filename
mimetype: Optional[str] = file.mimetype
if not filename or not mimetype:
raise Forbidden("Invalid request.")
if not timestamp or not nonce or not sign:
raise Forbidden("Invalid request.")
if not verify_plugin_file_signature(
filename=filename,
mimetype=mimetype,
@ -88,6 +127,3 @@ class PluginUploadFileApi(Resource):
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
api.add_resource(PluginUploadFileApi, "/files/upload/for-plugin")

View File

@ -1,10 +1,23 @@
from flask import Blueprint
from flask_restx import Namespace
from libs.external_api import ExternalApi
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
api = ExternalApi(bp)
api = ExternalApi(
bp,
version="1.0",
title="Inner API",
description="Internal APIs for enterprise features, billing, and plugin communication",
doc="/docs", # Enable Swagger UI at /inner/api/docs
)
# Create namespace
inner_api_ns = Namespace("inner_api", description="Internal API operations", path="/")
from . import mail
from .plugin import plugin
from .workspace import workspace
api.add_namespace(inner_api_ns)

View File

@ -1,7 +1,7 @@
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console.wraps import setup_required
from controllers.inner_api import api
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import billing_inner_api_only, enterprise_inner_api_only
from tasks.mail_inner_task import send_inner_email_task
@ -26,13 +26,45 @@ class BaseMail(Resource):
return {"message": "success"}, 200
@inner_api_ns.route("/enterprise/mail")
class EnterpriseMail(BaseMail):
method_decorators = [setup_required, enterprise_inner_api_only]
@inner_api_ns.doc("send_enterprise_mail")
@inner_api_ns.doc(description="Send internal email for enterprise features")
@inner_api_ns.expect(_mail_parser)
@inner_api_ns.doc(
responses={200: "Email sent successfully", 401: "Unauthorized - invalid API key", 404: "Service not available"}
)
def post(self):
"""Send internal email for enterprise features.
This endpoint allows sending internal emails for enterprise-specific
notifications and communications.
Returns:
dict: Success message with status code 200
"""
return super().post()
@inner_api_ns.route("/billing/mail")
class BillingMail(BaseMail):
method_decorators = [setup_required, billing_inner_api_only]
@inner_api_ns.doc("send_billing_mail")
@inner_api_ns.doc(description="Send internal email for billing notifications")
@inner_api_ns.expect(_mail_parser)
@inner_api_ns.doc(
responses={200: "Email sent successfully", 401: "Unauthorized - invalid API key", 404: "Service not available"}
)
def post(self):
"""Send internal email for billing notifications.
api.add_resource(EnterpriseMail, "/enterprise/mail")
api.add_resource(BillingMail, "/billing/mail")
This endpoint allows sending internal emails for billing-related
notifications and alerts.
Returns:
dict: Success message with status code 200
"""
return super().post()

View File

@ -1,7 +1,7 @@
from flask_restful import Resource
from flask_restx import Resource
from controllers.console.wraps import setup_required
from controllers.inner_api import api
from controllers.inner_api import inner_api_ns
from controllers.inner_api.plugin.wraps import get_user_tenant, plugin_data
from controllers.inner_api.wraps import plugin_inner_api_only
from core.file.helpers import get_signed_file_url_for_plugin
@ -35,11 +35,21 @@ from models.account import Account, Tenant
from models.model import EndUser
@inner_api_ns.route("/invoke/llm")
class PluginInvokeLLMApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeLLM)
@inner_api_ns.doc("plugin_invoke_llm")
@inner_api_ns.doc(description="Invoke LLM models through plugin interface")
@inner_api_ns.doc(
responses={
200: "LLM invocation successful (streaming response)",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeLLM):
def generator():
response = PluginModelBackwardsInvocation.invoke_llm(user_model.id, tenant_model, payload)
@ -48,11 +58,21 @@ class PluginInvokeLLMApi(Resource):
return length_prefixed_response(0xF, generator())
@inner_api_ns.route("/invoke/llm/structured-output")
class PluginInvokeLLMWithStructuredOutputApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeLLMWithStructuredOutput)
@inner_api_ns.doc("plugin_invoke_llm_structured")
@inner_api_ns.doc(description="Invoke LLM models with structured output through plugin interface")
@inner_api_ns.doc(
responses={
200: "LLM structured output invocation successful (streaming response)",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeLLMWithStructuredOutput):
def generator():
response = PluginModelBackwardsInvocation.invoke_llm_with_structured_output(
@ -63,11 +83,21 @@ class PluginInvokeLLMWithStructuredOutputApi(Resource):
return length_prefixed_response(0xF, generator())
@inner_api_ns.route("/invoke/text-embedding")
class PluginInvokeTextEmbeddingApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeTextEmbedding)
@inner_api_ns.doc("plugin_invoke_text_embedding")
@inner_api_ns.doc(description="Invoke text embedding models through plugin interface")
@inner_api_ns.doc(
responses={
200: "Text embedding successful",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTextEmbedding):
try:
return jsonable_encoder(
@ -83,11 +113,17 @@ class PluginInvokeTextEmbeddingApi(Resource):
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
@inner_api_ns.route("/invoke/rerank")
class PluginInvokeRerankApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeRerank)
@inner_api_ns.doc("plugin_invoke_rerank")
@inner_api_ns.doc(description="Invoke rerank models through plugin interface")
@inner_api_ns.doc(
responses={200: "Rerank successful", 401: "Unauthorized - invalid API key", 404: "Service not available"}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeRerank):
try:
return jsonable_encoder(
@ -103,11 +139,21 @@ class PluginInvokeRerankApi(Resource):
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
@inner_api_ns.route("/invoke/tts")
class PluginInvokeTTSApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeTTS)
@inner_api_ns.doc("plugin_invoke_tts")
@inner_api_ns.doc(description="Invoke text-to-speech models through plugin interface")
@inner_api_ns.doc(
responses={
200: "TTS invocation successful (streaming response)",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTTS):
def generator():
response = PluginModelBackwardsInvocation.invoke_tts(
@ -120,11 +166,17 @@ class PluginInvokeTTSApi(Resource):
return length_prefixed_response(0xF, generator())
@inner_api_ns.route("/invoke/speech2text")
class PluginInvokeSpeech2TextApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeSpeech2Text)
@inner_api_ns.doc("plugin_invoke_speech2text")
@inner_api_ns.doc(description="Invoke speech-to-text models through plugin interface")
@inner_api_ns.doc(
responses={200: "Speech2Text successful", 401: "Unauthorized - invalid API key", 404: "Service not available"}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeSpeech2Text):
try:
return jsonable_encoder(
@ -140,11 +192,17 @@ class PluginInvokeSpeech2TextApi(Resource):
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
@inner_api_ns.route("/invoke/moderation")
class PluginInvokeModerationApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeModeration)
@inner_api_ns.doc("plugin_invoke_moderation")
@inner_api_ns.doc(description="Invoke moderation models through plugin interface")
@inner_api_ns.doc(
responses={200: "Moderation successful", 401: "Unauthorized - invalid API key", 404: "Service not available"}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeModeration):
try:
return jsonable_encoder(
@ -160,11 +218,21 @@ class PluginInvokeModerationApi(Resource):
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
@inner_api_ns.route("/invoke/tool")
class PluginInvokeToolApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeTool)
@inner_api_ns.doc("plugin_invoke_tool")
@inner_api_ns.doc(description="Invoke tools through plugin interface")
@inner_api_ns.doc(
responses={
200: "Tool invocation successful (streaming response)",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeTool):
def generator():
return PluginToolBackwardsInvocation.convert_to_event_stream(
@ -182,11 +250,21 @@ class PluginInvokeToolApi(Resource):
return length_prefixed_response(0xF, generator())
@inner_api_ns.route("/invoke/parameter-extractor")
class PluginInvokeParameterExtractorNodeApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeParameterExtractorNode)
@inner_api_ns.doc("plugin_invoke_parameter_extractor")
@inner_api_ns.doc(description="Invoke parameter extractor node through plugin interface")
@inner_api_ns.doc(
responses={
200: "Parameter extraction successful",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeParameterExtractorNode):
try:
return jsonable_encoder(
@ -205,11 +283,21 @@ class PluginInvokeParameterExtractorNodeApi(Resource):
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
@inner_api_ns.route("/invoke/question-classifier")
class PluginInvokeQuestionClassifierNodeApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeQuestionClassifierNode)
@inner_api_ns.doc("plugin_invoke_question_classifier")
@inner_api_ns.doc(description="Invoke question classifier node through plugin interface")
@inner_api_ns.doc(
responses={
200: "Question classification successful",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeQuestionClassifierNode):
try:
return jsonable_encoder(
@ -228,11 +316,21 @@ class PluginInvokeQuestionClassifierNodeApi(Resource):
return jsonable_encoder(BaseBackwardsInvocationResponse(error=str(e)))
@inner_api_ns.route("/invoke/app")
class PluginInvokeAppApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeApp)
@inner_api_ns.doc("plugin_invoke_app")
@inner_api_ns.doc(description="Invoke application through plugin interface")
@inner_api_ns.doc(
responses={
200: "App invocation successful (streaming response)",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeApp):
response = PluginAppBackwardsInvocation.invoke_app(
app_id=payload.app_id,
@ -248,11 +346,21 @@ class PluginInvokeAppApi(Resource):
return length_prefixed_response(0xF, PluginAppBackwardsInvocation.convert_to_event_stream(response))
@inner_api_ns.route("/invoke/encrypt")
class PluginInvokeEncryptApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeEncrypt)
@inner_api_ns.doc("plugin_invoke_encrypt")
@inner_api_ns.doc(description="Encrypt or decrypt data through plugin interface")
@inner_api_ns.doc(
responses={
200: "Encryption/decryption successful",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeEncrypt):
"""
encrypt or decrypt data
@ -265,11 +373,21 @@ class PluginInvokeEncryptApi(Resource):
return BaseBackwardsInvocationResponse(error=str(e)).model_dump()
@inner_api_ns.route("/invoke/summary")
class PluginInvokeSummaryApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestInvokeSummary)
@inner_api_ns.doc("plugin_invoke_summary")
@inner_api_ns.doc(description="Invoke summary functionality through plugin interface")
@inner_api_ns.doc(
responses={
200: "Summary generation successful",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeSummary):
try:
return BaseBackwardsInvocationResponse(
@ -285,40 +403,43 @@ class PluginInvokeSummaryApi(Resource):
return BaseBackwardsInvocationResponse(error=str(e)).model_dump()
@inner_api_ns.route("/upload/file/request")
class PluginUploadFileRequestApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestRequestUploadFile)
@inner_api_ns.doc("plugin_upload_file_request")
@inner_api_ns.doc(description="Request signed URL for file upload through plugin interface")
@inner_api_ns.doc(
responses={
200: "Signed URL generated successfully",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestRequestUploadFile):
# generate signed url
url = get_signed_file_url_for_plugin(payload.filename, payload.mimetype, tenant_model.id, user_model.id)
return BaseBackwardsInvocationResponse(data={"url": url}).model_dump()
@inner_api_ns.route("/fetch/app/info")
class PluginFetchAppInfoApi(Resource):
@setup_required
@plugin_inner_api_only
@get_user_tenant
@plugin_data(payload_type=RequestFetchAppInfo)
@inner_api_ns.doc("plugin_fetch_app_info")
@inner_api_ns.doc(description="Fetch application information through plugin interface")
@inner_api_ns.doc(
responses={
200: "App information retrieved successfully",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestFetchAppInfo):
return BaseBackwardsInvocationResponse(
data=PluginAppBackwardsInvocation.fetch_app_info(payload.app_id, tenant_model.id)
).model_dump()
api.add_resource(PluginInvokeLLMApi, "/invoke/llm")
api.add_resource(PluginInvokeLLMWithStructuredOutputApi, "/invoke/llm/structured-output")
api.add_resource(PluginInvokeTextEmbeddingApi, "/invoke/text-embedding")
api.add_resource(PluginInvokeRerankApi, "/invoke/rerank")
api.add_resource(PluginInvokeTTSApi, "/invoke/tts")
api.add_resource(PluginInvokeSpeech2TextApi, "/invoke/speech2text")
api.add_resource(PluginInvokeModerationApi, "/invoke/moderation")
api.add_resource(PluginInvokeToolApi, "/invoke/tool")
api.add_resource(PluginInvokeParameterExtractorNodeApi, "/invoke/parameter-extractor")
api.add_resource(PluginInvokeQuestionClassifierNodeApi, "/invoke/question-classifier")
api.add_resource(PluginInvokeAppApi, "/invoke/app")
api.add_resource(PluginInvokeEncryptApi, "/invoke/encrypt")
api.add_resource(PluginInvokeSummaryApi, "/invoke/summary")
api.add_resource(PluginUploadFileRequestApi, "/upload/file/request")
api.add_resource(PluginFetchAppInfoApi, "/fetch/app/info")

View File

@ -4,7 +4,7 @@ from typing import Optional
from flask import current_app, request
from flask_login import user_logged_in
from flask_restful import reqparse
from flask_restx import reqparse
from pydantic import BaseModel
from sqlalchemy.orm import Session

View File

@ -1,9 +1,9 @@
import json
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from controllers.console.wraps import setup_required
from controllers.inner_api import api
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import enterprise_inner_api_only
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
@ -11,9 +11,19 @@ from models.account import Account
from services.account_service import TenantService
@inner_api_ns.route("/enterprise/workspace")
class EnterpriseWorkspace(Resource):
@setup_required
@enterprise_inner_api_only
@inner_api_ns.doc("create_enterprise_workspace")
@inner_api_ns.doc(description="Create a new enterprise workspace with owner assignment")
@inner_api_ns.doc(
responses={
200: "Workspace created successfully",
401: "Unauthorized - invalid API key",
404: "Owner account not found or service not available",
}
)
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=True, location="json")
@ -44,9 +54,19 @@ class EnterpriseWorkspace(Resource):
}
@inner_api_ns.route("/enterprise/workspace/ownerless")
class EnterpriseWorkspaceNoOwnerEmail(Resource):
@setup_required
@enterprise_inner_api_only
@inner_api_ns.doc("create_enterprise_workspace_ownerless")
@inner_api_ns.doc(description="Create a new enterprise workspace without initial owner assignment")
@inner_api_ns.doc(
responses={
200: "Workspace created successfully",
401: "Unauthorized - invalid API key",
404: "Service not available",
}
)
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=True, location="json")
@ -71,7 +91,3 @@ class EnterpriseWorkspaceNoOwnerEmail(Resource):
"message": "enterprise workspace created.",
"tenant": resp,
}
api.add_resource(EnterpriseWorkspace, "/enterprise/workspace")
api.add_resource(EnterpriseWorkspaceNoOwnerEmail, "/enterprise/workspace/ownerless")

View File

@ -1,8 +1,20 @@
from flask import Blueprint
from flask_restx import Namespace
from libs.external_api import ExternalApi
bp = Blueprint("mcp", __name__, url_prefix="/mcp")
api = ExternalApi(bp)
api = ExternalApi(
bp,
version="1.0",
title="MCP API",
description="API for Model Context Protocol operations",
doc="/docs", # Enable Swagger UI at /mcp/docs
)
mcp_ns = Namespace("mcp", description="MCP operations", path="/")
from . import mcp
api.add_namespace(mcp_ns)

View File

@ -1,8 +1,10 @@
from flask_restful import Resource, reqparse
from typing import Optional, Union
from flask_restx import Resource, reqparse
from pydantic import ValidationError
from controllers.console.app.mcp_server import AppMCPServerStatus
from controllers.mcp import api
from controllers.mcp import mcp_ns
from core.app.app_config.entities import VariableEntity
from core.mcp import types
from core.mcp.server.streamable_http import MCPServerStreamableHTTPRequestHandler
@ -13,22 +15,58 @@ from libs import helper
from models.model import App, AppMCPServer, AppMode
def int_or_str(value):
"""Validate that a value is either an integer or string."""
if isinstance(value, (int, str)):
return value
else:
return None
# Define parser for both documentation and validation
mcp_request_parser = reqparse.RequestParser()
mcp_request_parser.add_argument(
"jsonrpc", type=str, required=True, location="json", help="JSON-RPC version (should be '2.0')"
)
mcp_request_parser.add_argument("method", type=str, required=True, location="json", help="The method to invoke")
mcp_request_parser.add_argument("params", type=dict, required=False, location="json", help="Parameters for the method")
mcp_request_parser.add_argument(
"id", type=int_or_str, required=False, location="json", help="Request ID for tracking responses"
)
@mcp_ns.route("/server/<string:server_code>/mcp")
class MCPAppApi(Resource):
def post(self, server_code):
def int_or_str(value):
if isinstance(value, (int, str)):
return value
else:
return None
@mcp_ns.expect(mcp_request_parser)
@mcp_ns.doc("handle_mcp_request")
@mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server")
@mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"})
@mcp_ns.doc(
responses={
200: "MCP response successfully processed",
400: "Invalid MCP request or parameters",
404: "Server or app not found",
}
)
def post(self, server_code: str):
"""Handle MCP requests for a specific server.
parser = reqparse.RequestParser()
parser.add_argument("jsonrpc", type=str, required=True, location="json")
parser.add_argument("method", type=str, required=True, location="json")
parser.add_argument("params", type=dict, required=False, location="json")
parser.add_argument("id", type=int_or_str, required=False, location="json")
args = parser.parse_args()
Processes JSON-RPC formatted requests according to the Model Context Protocol specification.
Validates the server status and associated app before processing the request.
request_id = args.get("id")
Args:
server_code: Unique identifier for the MCP server
Returns:
dict: JSON-RPC response from the MCP handler
Raises:
ValidationError: Invalid request format or parameters
"""
# Parse and validate all arguments
args = mcp_request_parser.parse_args()
request_id: Optional[Union[int, str]] = args.get("id")
server = db.session.query(AppMCPServer).where(AppMCPServer.server_code == server_code).first()
if not server:
@ -99,6 +137,3 @@ class MCPAppApi(Resource):
mcp_server_handler = MCPServerStreamableHTTPRequestHandler(app, request, converted_user_input_form)
response = mcp_server_handler.handle()
return helper.compact_generate_response(response)
api.add_resource(MCPAppApi, "/server/<string:server_code>/mcp")

View File

@ -1,11 +1,23 @@
from flask import Blueprint
from flask_restx import Namespace
from libs.external_api import ExternalApi
bp = Blueprint("service_api", __name__, url_prefix="/v1")
api = ExternalApi(bp)
api = ExternalApi(
bp,
version="1.0",
title="Service API",
description="API for application services",
doc="/docs", # Enable Swagger UI at /v1/docs
)
service_api_ns = Namespace("service_api", description="Service operations", path="/")
from . import index
from .app import annotation, app, audio, completion, conversation, file, file_preview, message, site, workflow
from .dataset import dataset, document, hit_testing, metadata, segment, upload_file
from .workspace import models
api.add_namespace(service_api_ns)

View File

@ -1,28 +1,52 @@
from typing import Literal
from flask import request
from flask_restful import Resource, marshal, marshal_with, reqparse
from flask_restx import Api, Namespace, Resource, fields, reqparse
from flask_restx.api import HTTPStatus
from werkzeug.exceptions import Forbidden
from controllers.service_api import api
from controllers.service_api import service_api_ns
from controllers.service_api.wraps import validate_app_token
from extensions.ext_redis import redis_client
from fields.annotation_fields import (
annotation_fields,
)
from fields.annotation_fields import annotation_fields, build_annotation_model
from libs.login import current_user
from models.account import Account
from models.model import App
from services.annotation_service import AppAnnotationService
# Define parsers for annotation API
annotation_create_parser = reqparse.RequestParser()
annotation_create_parser.add_argument("question", required=True, type=str, location="json", help="Annotation question")
annotation_create_parser.add_argument("answer", required=True, type=str, location="json", help="Annotation answer")
annotation_reply_action_parser = reqparse.RequestParser()
annotation_reply_action_parser.add_argument(
"score_threshold", required=True, type=float, location="json", help="Score threshold for annotation matching"
)
annotation_reply_action_parser.add_argument(
"embedding_provider_name", required=True, type=str, location="json", help="Embedding provider name"
)
annotation_reply_action_parser.add_argument(
"embedding_model_name", required=True, type=str, location="json", help="Embedding model name"
)
@service_api_ns.route("/apps/annotation-reply/<string:action>")
class AnnotationReplyActionApi(Resource):
@service_api_ns.expect(annotation_reply_action_parser)
@service_api_ns.doc("annotation_reply_action")
@service_api_ns.doc(description="Enable or disable annotation reply feature")
@service_api_ns.doc(params={"action": "Action to perform: 'enable' or 'disable'"})
@service_api_ns.doc(
responses={
200: "Action completed successfully",
401: "Unauthorized - invalid API token",
}
)
@validate_app_token
def post(self, app_model: App, action: Literal["enable", "disable"]):
parser = reqparse.RequestParser()
parser.add_argument("score_threshold", required=True, type=float, location="json")
parser.add_argument("embedding_provider_name", required=True, type=str, location="json")
parser.add_argument("embedding_model_name", required=True, type=str, location="json")
args = parser.parse_args()
"""Enable or disable annotation reply feature."""
args = annotation_reply_action_parser.parse_args()
if action == "enable":
result = AppAnnotationService.enable_app_annotation(args, app_model.id)
elif action == "disable":
@ -30,9 +54,21 @@ class AnnotationReplyActionApi(Resource):
return result, 200
@service_api_ns.route("/apps/annotation-reply/<string:action>/status/<uuid:job_id>")
class AnnotationReplyActionStatusApi(Resource):
@service_api_ns.doc("get_annotation_reply_action_status")
@service_api_ns.doc(description="Get the status of an annotation reply action job")
@service_api_ns.doc(params={"action": "Action type", "job_id": "Job ID"})
@service_api_ns.doc(
responses={
200: "Job status retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Job not found",
}
)
@validate_app_token
def get(self, app_model: App, job_id, action):
"""Get the status of an annotation reply action job."""
job_id = str(job_id)
app_annotation_job_key = f"{action}_app_annotation_job_{str(job_id)}"
cache_result = redis_client.get(app_annotation_job_key)
@ -48,60 +84,114 @@ class AnnotationReplyActionStatusApi(Resource):
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
# Define annotation list response model
annotation_list_fields = {
"data": fields.List(fields.Nested(annotation_fields)),
"has_more": fields.Boolean,
"limit": fields.Integer,
"total": fields.Integer,
"page": fields.Integer,
}
def build_annotation_list_model(api_or_ns: Api | Namespace):
"""Build the annotation list model for the API or Namespace."""
copied_annotation_list_fields = annotation_list_fields.copy()
copied_annotation_list_fields["data"] = fields.List(fields.Nested(build_annotation_model(api_or_ns)))
return api_or_ns.model("AnnotationList", copied_annotation_list_fields)
@service_api_ns.route("/apps/annotations")
class AnnotationListApi(Resource):
@service_api_ns.doc("list_annotations")
@service_api_ns.doc(description="List annotations for the application")
@service_api_ns.doc(
responses={
200: "Annotations retrieved successfully",
401: "Unauthorized - invalid API token",
}
)
@validate_app_token
@service_api_ns.marshal_with(build_annotation_list_model(service_api_ns))
def get(self, app_model: App):
"""List annotations for the application."""
page = request.args.get("page", default=1, type=int)
limit = request.args.get("limit", default=20, type=int)
keyword = request.args.get("keyword", default="", type=str)
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_model.id, page, limit, keyword)
response = {
"data": marshal(annotation_list, annotation_fields),
return {
"data": annotation_list,
"has_more": len(annotation_list) == limit,
"limit": limit,
"total": total,
"page": page,
}
return response, 200
@service_api_ns.expect(annotation_create_parser)
@service_api_ns.doc("create_annotation")
@service_api_ns.doc(description="Create a new annotation")
@service_api_ns.doc(
responses={
201: "Annotation created successfully",
401: "Unauthorized - invalid API token",
}
)
@validate_app_token
@marshal_with(annotation_fields)
@service_api_ns.marshal_with(build_annotation_model(service_api_ns), code=HTTPStatus.CREATED)
def post(self, app_model: App):
parser = reqparse.RequestParser()
parser.add_argument("question", required=True, type=str, location="json")
parser.add_argument("answer", required=True, type=str, location="json")
args = parser.parse_args()
"""Create a new annotation."""
args = annotation_create_parser.parse_args()
annotation = AppAnnotationService.insert_app_annotation_directly(args, app_model.id)
return annotation
return annotation, 201
@service_api_ns.route("/apps/annotations/<uuid:annotation_id>")
class AnnotationUpdateDeleteApi(Resource):
@service_api_ns.expect(annotation_create_parser)
@service_api_ns.doc("update_annotation")
@service_api_ns.doc(description="Update an existing annotation")
@service_api_ns.doc(params={"annotation_id": "Annotation ID"})
@service_api_ns.doc(
responses={
200: "Annotation updated successfully",
401: "Unauthorized - invalid API token",
403: "Forbidden - insufficient permissions",
404: "Annotation not found",
}
)
@validate_app_token
@marshal_with(annotation_fields)
@service_api_ns.marshal_with(build_annotation_model(service_api_ns))
def put(self, app_model: App, annotation_id):
"""Update an existing annotation."""
assert isinstance(current_user, Account)
if not current_user.is_editor:
raise Forbidden()
annotation_id = str(annotation_id)
parser = reqparse.RequestParser()
parser.add_argument("question", required=True, type=str, location="json")
parser.add_argument("answer", required=True, type=str, location="json")
args = parser.parse_args()
args = annotation_create_parser.parse_args()
annotation = AppAnnotationService.update_app_annotation_directly(args, app_model.id, annotation_id)
return annotation
@service_api_ns.doc("delete_annotation")
@service_api_ns.doc(description="Delete an annotation")
@service_api_ns.doc(params={"annotation_id": "Annotation ID"})
@service_api_ns.doc(
responses={
204: "Annotation deleted successfully",
401: "Unauthorized - invalid API token",
403: "Forbidden - insufficient permissions",
404: "Annotation not found",
}
)
@validate_app_token
def delete(self, app_model: App, annotation_id):
"""Delete an annotation."""
assert isinstance(current_user, Account)
if not current_user.is_editor:
raise Forbidden()
annotation_id = str(annotation_id)
AppAnnotationService.delete_app_annotation(app_model.id, annotation_id)
return {"result": "success"}, 204
api.add_resource(AnnotationReplyActionApi, "/apps/annotation-reply/<string:action>")
api.add_resource(AnnotationReplyActionStatusApi, "/apps/annotation-reply/<string:action>/status/<uuid:job_id>")
api.add_resource(AnnotationListApi, "/apps/annotations")
api.add_resource(AnnotationUpdateDeleteApi, "/apps/annotations/<uuid:annotation_id>")

View File

@ -1,7 +1,7 @@
from flask_restful import Resource, marshal_with
from flask_restx import Resource
from controllers.common import fields
from controllers.service_api import api
from controllers.common.fields import build_parameters_model
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import AppUnavailableError
from controllers.service_api.wraps import validate_app_token
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
@ -9,13 +9,26 @@ from models.model import App, AppMode
from services.app_service import AppService
@service_api_ns.route("/parameters")
class AppParameterApi(Resource):
"""Resource for app variables."""
@service_api_ns.doc("get_app_parameters")
@service_api_ns.doc(description="Retrieve application input parameters and configuration")
@service_api_ns.doc(
responses={
200: "Parameters retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Application not found",
}
)
@validate_app_token
@marshal_with(fields.parameters_fields)
@service_api_ns.marshal_with(build_parameters_model(service_api_ns))
def get(self, app_model: App):
"""Retrieve app parameters."""
"""Retrieve app parameters.
Returns the input form parameters and configuration for the application.
"""
if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
workflow = app_model.workflow
if workflow is None:
@ -35,17 +48,43 @@ class AppParameterApi(Resource):
return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form)
@service_api_ns.route("/meta")
class AppMetaApi(Resource):
@service_api_ns.doc("get_app_meta")
@service_api_ns.doc(description="Get application metadata")
@service_api_ns.doc(
responses={
200: "Metadata retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Application not found",
}
)
@validate_app_token
def get(self, app_model: App):
"""Get app meta"""
"""Get app metadata.
Returns metadata about the application including configuration and settings.
"""
return AppService().get_app_meta(app_model)
@service_api_ns.route("/info")
class AppInfoApi(Resource):
@service_api_ns.doc("get_app_info")
@service_api_ns.doc(description="Get basic application information")
@service_api_ns.doc(
responses={
200: "Application info retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Application not found",
}
)
@validate_app_token
def get(self, app_model: App):
"""Get app information"""
"""Get app information.
Returns basic information about the application including name, description, tags, and mode.
"""
tags = [tag.name for tag in app_model.tags]
return {
"name": app_model.name,
@ -54,8 +93,3 @@ class AppInfoApi(Resource):
"mode": app_model.mode,
"author_name": app_model.author_name,
}
api.add_resource(AppParameterApi, "/parameters")
api.add_resource(AppMetaApi, "/meta")
api.add_resource(AppInfoApi, "/info")

View File

@ -1,11 +1,11 @@
import logging
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import InternalServerError
import services
from controllers.service_api import api
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import (
AppUnavailableError,
AudioTooLargeError,
@ -29,10 +29,29 @@ from services.errors.audio import (
UnsupportedAudioTypeServiceError,
)
logger = logging.getLogger(__name__)
@service_api_ns.route("/audio-to-text")
class AudioApi(Resource):
@service_api_ns.doc("audio_to_text")
@service_api_ns.doc(description="Convert audio to text using speech-to-text")
@service_api_ns.doc(
responses={
200: "Audio successfully transcribed",
400: "Bad request - no audio or invalid audio",
401: "Unauthorized - invalid API token",
413: "Audio file too large",
415: "Unsupported audio type",
500: "Internal server error",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM))
def post(self, app_model: App, end_user: EndUser):
"""Convert audio to text using speech-to-text.
Accepts an audio file upload and returns the transcribed text.
"""
file = request.files["file"]
try:
@ -40,7 +59,7 @@ class AudioApi(Resource):
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except NoAudioUploadedServiceError:
raise NoAudioUploadedError()
@ -61,20 +80,39 @@ class AudioApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
# Define parser for text-to-audio API
text_to_audio_parser = reqparse.RequestParser()
text_to_audio_parser.add_argument("message_id", type=str, required=False, location="json", help="Message ID")
text_to_audio_parser.add_argument("voice", type=str, location="json", help="Voice to use for TTS")
text_to_audio_parser.add_argument("text", type=str, location="json", help="Text to convert to audio")
text_to_audio_parser.add_argument("streaming", type=bool, location="json", help="Enable streaming response")
@service_api_ns.route("/text-to-audio")
class TextApi(Resource):
@service_api_ns.expect(text_to_audio_parser)
@service_api_ns.doc("text_to_audio")
@service_api_ns.doc(description="Convert text to audio using text-to-speech")
@service_api_ns.doc(
responses={
200: "Text successfully converted to audio",
400: "Bad request - invalid parameters",
401: "Unauthorized - invalid API token",
500: "Internal server error",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
def post(self, app_model: App, end_user: EndUser):
"""Convert text to audio using text-to-speech.
Converts the provided text to audio using the specified voice.
"""
try:
parser = reqparse.RequestParser()
parser.add_argument("message_id", type=str, required=False, location="json")
parser.add_argument("voice", type=str, location="json")
parser.add_argument("text", type=str, location="json")
parser.add_argument("streaming", type=bool, location="json")
args = parser.parse_args()
args = text_to_audio_parser.parse_args()
message_id = args.get("message_id", None)
text = args.get("text", None)
@ -85,7 +123,7 @@ class TextApi(Resource):
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except NoAudioUploadedServiceError:
raise NoAudioUploadedError()
@ -106,9 +144,5 @@ class TextApi(Resource):
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
api.add_resource(AudioApi, "/audio-to-text")
api.add_resource(TextApi, "/text-to-audio")

View File

@ -1,11 +1,11 @@
import logging
from flask import request
from flask_restful import Resource, reqparse
from flask_restx import Resource, reqparse
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services
from controllers.service_api import api
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import (
AppUnavailableError,
CompletionRequestError,
@ -33,21 +33,71 @@ from services.app_generate_service import AppGenerateService
from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
logger = logging.getLogger(__name__)
# Define parser for completion API
completion_parser = reqparse.RequestParser()
completion_parser.add_argument(
"inputs", type=dict, required=True, location="json", help="Input parameters for completion"
)
completion_parser.add_argument("query", type=str, location="json", default="", help="The query string")
completion_parser.add_argument("files", type=list, required=False, location="json", help="List of file attachments")
completion_parser.add_argument(
"response_mode", type=str, choices=["blocking", "streaming"], location="json", help="Response mode"
)
completion_parser.add_argument(
"retriever_from", type=str, required=False, default="dev", location="json", help="Retriever source"
)
# Define parser for chat API
chat_parser = reqparse.RequestParser()
chat_parser.add_argument("inputs", type=dict, required=True, location="json", help="Input parameters for chat")
chat_parser.add_argument("query", type=str, required=True, location="json", help="The chat query")
chat_parser.add_argument("files", type=list, required=False, location="json", help="List of file attachments")
chat_parser.add_argument(
"response_mode", type=str, choices=["blocking", "streaming"], location="json", help="Response mode"
)
chat_parser.add_argument("conversation_id", type=uuid_value, location="json", help="Existing conversation ID")
chat_parser.add_argument(
"retriever_from", type=str, required=False, default="dev", location="json", help="Retriever source"
)
chat_parser.add_argument(
"auto_generate_name",
type=bool,
required=False,
default=True,
location="json",
help="Auto generate conversation name",
)
chat_parser.add_argument("workflow_id", type=str, required=False, location="json", help="Workflow ID for advanced chat")
@service_api_ns.route("/completion-messages")
class CompletionApi(Resource):
@service_api_ns.expect(completion_parser)
@service_api_ns.doc("create_completion")
@service_api_ns.doc(description="Create a completion for the given prompt")
@service_api_ns.doc(
responses={
200: "Completion created successfully",
400: "Bad request - invalid parameters",
401: "Unauthorized - invalid API token",
404: "Conversation not found",
500: "Internal server error",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser):
"""Create a completion for the given prompt.
This endpoint generates a completion based on the provided inputs and query.
Supports both blocking and streaming response modes.
"""
if app_model.mode != "completion":
raise AppUnavailableError()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, required=True, location="json")
parser.add_argument("query", type=str, location="json", default="")
parser.add_argument("files", type=list, required=False, location="json")
parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json")
parser.add_argument("retriever_from", type=str, required=False, default="dev", location="json")
args = parser.parse_args()
args = completion_parser.parse_args()
external_trace_id = get_external_trace_id(request)
if external_trace_id:
args["external_trace_id"] = external_trace_id
@ -71,7 +121,7 @@ class CompletionApi(Resource):
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -84,13 +134,25 @@ class CompletionApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@service_api_ns.route("/completion-messages/<string:task_id>/stop")
class CompletionStopApi(Resource):
@service_api_ns.doc("stop_completion")
@service_api_ns.doc(description="Stop a running completion task")
@service_api_ns.doc(params={"task_id": "The ID of the task to stop"})
@service_api_ns.doc(
responses={
200: "Task stopped successfully",
401: "Unauthorized - invalid API token",
404: "Task not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser, task_id):
def post(self, app_model: App, end_user: EndUser, task_id: str):
"""Stop a running completion task."""
if app_model.mode != "completion":
raise AppUnavailableError()
@ -99,23 +161,33 @@ class CompletionStopApi(Resource):
return {"result": "success"}, 200
@service_api_ns.route("/chat-messages")
class ChatApi(Resource):
@service_api_ns.expect(chat_parser)
@service_api_ns.doc("create_chat_message")
@service_api_ns.doc(description="Send a message in a chat conversation")
@service_api_ns.doc(
responses={
200: "Message sent successfully",
400: "Bad request - invalid parameters or workflow issues",
401: "Unauthorized - invalid API token",
404: "Conversation or workflow not found",
429: "Rate limit exceeded",
500: "Internal server error",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser):
"""Send a message in a chat conversation.
This endpoint handles chat messages for chat, agent chat, and advanced chat applications.
Supports conversation management and both blocking and streaming response modes.
"""
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument("inputs", type=dict, required=True, location="json")
parser.add_argument("query", type=str, required=True, location="json")
parser.add_argument("files", type=list, required=False, location="json")
parser.add_argument("response_mode", type=str, choices=["blocking", "streaming"], location="json")
parser.add_argument("conversation_id", type=uuid_value, location="json")
parser.add_argument("retriever_from", type=str, required=False, default="dev", location="json")
parser.add_argument("auto_generate_name", type=bool, required=False, default=True, location="json")
parser.add_argument("workflow_id", type=str, required=False, location="json")
args = parser.parse_args()
args = chat_parser.parse_args()
external_trace_id = get_external_trace_id(request)
if external_trace_id:
@ -140,7 +212,7 @@ class ChatApi(Resource):
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -155,13 +227,25 @@ class ChatApi(Resource):
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
logger.exception("internal server error.")
raise InternalServerError()
@service_api_ns.route("/chat-messages/<string:task_id>/stop")
class ChatStopApi(Resource):
@service_api_ns.doc("stop_chat_message")
@service_api_ns.doc(description="Stop a running chat message generation")
@service_api_ns.doc(params={"task_id": "The ID of the task to stop"})
@service_api_ns.doc(
responses={
200: "Task stopped successfully",
401: "Unauthorized - invalid API token",
404: "Task not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
def post(self, app_model: App, end_user: EndUser, task_id):
def post(self, app_model: App, end_user: EndUser, task_id: str):
"""Stop a running chat message generation."""
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
@ -169,9 +253,3 @@ class ChatStopApi(Resource):
AppQueueManager.set_stop_flag(task_id, InvokeFrom.SERVICE_API, end_user.id)
return {"result": "success"}, 200
api.add_resource(CompletionApi, "/completion-messages")
api.add_resource(CompletionStopApi, "/completion-messages/<string:task_id>/stop")
api.add_resource(ChatApi, "/chat-messages")
api.add_resource(ChatStopApi, "/chat-messages/<string:task_id>/stop")

View File

@ -1,48 +1,97 @@
from flask_restful import Resource, marshal_with, reqparse
from flask_restful.inputs import int_range
from flask_restx import Resource, reqparse
from flask_restx.inputs import int_range
from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, NotFound
import services
from controllers.service_api import api
from controllers.service_api import service_api_ns
from controllers.service_api.app.error import NotChatAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db
from fields.conversation_fields import (
conversation_delete_fields,
conversation_infinite_scroll_pagination_fields,
simple_conversation_fields,
build_conversation_delete_model,
build_conversation_infinite_scroll_pagination_model,
build_simple_conversation_model,
)
from fields.conversation_variable_fields import (
conversation_variable_fields,
conversation_variable_infinite_scroll_pagination_fields,
build_conversation_variable_infinite_scroll_pagination_model,
build_conversation_variable_model,
)
from libs.helper import uuid_value
from models.model import App, AppMode, EndUser
from services.conversation_service import ConversationService
# Define parsers for conversation APIs
conversation_list_parser = reqparse.RequestParser()
conversation_list_parser.add_argument(
"last_id", type=uuid_value, location="args", help="Last conversation ID for pagination"
)
conversation_list_parser.add_argument(
"limit",
type=int_range(1, 100),
required=False,
default=20,
location="args",
help="Number of conversations to return",
)
conversation_list_parser.add_argument(
"sort_by",
type=str,
choices=["created_at", "-created_at", "updated_at", "-updated_at"],
required=False,
default="-updated_at",
location="args",
help="Sort order for conversations",
)
conversation_rename_parser = reqparse.RequestParser()
conversation_rename_parser.add_argument("name", type=str, required=False, location="json", help="New conversation name")
conversation_rename_parser.add_argument(
"auto_generate", type=bool, required=False, default=False, location="json", help="Auto-generate conversation name"
)
conversation_variables_parser = reqparse.RequestParser()
conversation_variables_parser.add_argument(
"last_id", type=uuid_value, location="args", help="Last variable ID for pagination"
)
conversation_variables_parser.add_argument(
"limit", type=int_range(1, 100), required=False, default=20, location="args", help="Number of variables to return"
)
conversation_variable_update_parser = reqparse.RequestParser()
# using lambda is for passing the already-typed value without modification
# if no lambda, it will be converted to string
# the string cannot be converted using json.loads
conversation_variable_update_parser.add_argument(
"value", required=True, location="json", type=lambda x: x, help="New value for the conversation variable"
)
@service_api_ns.route("/conversations")
class ConversationApi(Resource):
@service_api_ns.expect(conversation_list_parser)
@service_api_ns.doc("list_conversations")
@service_api_ns.doc(description="List all conversations for the current user")
@service_api_ns.doc(
responses={
200: "Conversations retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Last conversation not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
@marshal_with(conversation_infinite_scroll_pagination_fields)
@service_api_ns.marshal_with(build_conversation_infinite_scroll_pagination_model(service_api_ns))
def get(self, app_model: App, end_user: EndUser):
"""List all conversations for the current user.
Supports pagination using last_id and limit parameters.
"""
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument("last_id", type=uuid_value, location="args")
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
parser.add_argument(
"sort_by",
type=str,
choices=["created_at", "-created_at", "updated_at", "-updated_at"],
required=False,
default="-updated_at",
location="args",
)
args = parser.parse_args()
args = conversation_list_parser.parse_args()
try:
with Session(db.engine) as session:
@ -59,10 +108,22 @@ class ConversationApi(Resource):
raise NotFound("Last Conversation Not Exists.")
@service_api_ns.route("/conversations/<uuid:c_id>")
class ConversationDetailApi(Resource):
@service_api_ns.doc("delete_conversation")
@service_api_ns.doc(description="Delete a specific conversation")
@service_api_ns.doc(params={"c_id": "Conversation ID"})
@service_api_ns.doc(
responses={
204: "Conversation deleted successfully",
401: "Unauthorized - invalid API token",
404: "Conversation not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(conversation_delete_fields)
@service_api_ns.marshal_with(build_conversation_delete_model(service_api_ns), code=204)
def delete(self, app_model: App, end_user: EndUser, c_id):
"""Delete a specific conversation."""
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
@ -76,20 +137,30 @@ class ConversationDetailApi(Resource):
return {"result": "success"}, 204
@service_api_ns.route("/conversations/<uuid:c_id>/name")
class ConversationRenameApi(Resource):
@service_api_ns.expect(conversation_rename_parser)
@service_api_ns.doc("rename_conversation")
@service_api_ns.doc(description="Rename a conversation or auto-generate a name")
@service_api_ns.doc(params={"c_id": "Conversation ID"})
@service_api_ns.doc(
responses={
200: "Conversation renamed successfully",
401: "Unauthorized - invalid API token",
404: "Conversation not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(simple_conversation_fields)
@service_api_ns.marshal_with(build_simple_conversation_model(service_api_ns))
def post(self, app_model: App, end_user: EndUser, c_id):
"""Rename a conversation or auto-generate a name."""
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)
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=False, location="json")
parser.add_argument("auto_generate", type=bool, required=False, default=False, location="json")
args = parser.parse_args()
args = conversation_rename_parser.parse_args()
try:
return ConversationService.rename(app_model, conversation_id, end_user, args["name"], args["auto_generate"])
@ -97,10 +168,26 @@ class ConversationRenameApi(Resource):
raise NotFound("Conversation Not Exists.")
@service_api_ns.route("/conversations/<uuid:c_id>/variables")
class ConversationVariablesApi(Resource):
@service_api_ns.expect(conversation_variables_parser)
@service_api_ns.doc("list_conversation_variables")
@service_api_ns.doc(description="List all variables for a conversation")
@service_api_ns.doc(params={"c_id": "Conversation ID"})
@service_api_ns.doc(
responses={
200: "Variables retrieved successfully",
401: "Unauthorized - invalid API token",
404: "Conversation not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY))
@marshal_with(conversation_variable_infinite_scroll_pagination_fields)
@service_api_ns.marshal_with(build_conversation_variable_infinite_scroll_pagination_model(service_api_ns))
def get(self, app_model: App, end_user: EndUser, c_id):
"""List all variables for a conversation.
Conversational variables are only available for chat applications.
"""
# conversational variable only for chat app
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
@ -108,10 +195,7 @@ class ConversationVariablesApi(Resource):
conversation_id = str(c_id)
parser = reqparse.RequestParser()
parser.add_argument("last_id", type=uuid_value, location="args")
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args()
args = conversation_variables_parser.parse_args()
try:
return ConversationService.get_conversational_variable(
@ -121,11 +205,28 @@ class ConversationVariablesApi(Resource):
raise NotFound("Conversation Not Exists.")
@service_api_ns.route("/conversations/<uuid:c_id>/variables/<uuid:variable_id>")
class ConversationVariableDetailApi(Resource):
@service_api_ns.expect(conversation_variable_update_parser)
@service_api_ns.doc("update_conversation_variable")
@service_api_ns.doc(description="Update a conversation variable's value")
@service_api_ns.doc(params={"c_id": "Conversation ID", "variable_id": "Variable ID"})
@service_api_ns.doc(
responses={
200: "Variable updated successfully",
400: "Bad request - type mismatch",
401: "Unauthorized - invalid API token",
404: "Conversation or variable not found",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(conversation_variable_fields)
@service_api_ns.marshal_with(build_conversation_variable_model(service_api_ns))
def put(self, app_model: App, end_user: EndUser, c_id, variable_id):
"""Update a conversation variable's value"""
"""Update a conversation variable's value.
Allows updating the value of a specific conversation variable.
The value must match the variable's expected type.
"""
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
@ -133,12 +234,7 @@ class ConversationVariableDetailApi(Resource):
conversation_id = str(c_id)
variable_id = str(variable_id)
parser = reqparse.RequestParser()
# using lambda is for passing the already-typed value without modification
# if no lambda, it will be converted to string
# the string cannot be converted using json.loads
parser.add_argument("value", required=True, location="json", type=lambda x: x)
args = parser.parse_args()
args = conversation_variable_update_parser.parse_args()
try:
return ConversationService.update_conversation_variable(
@ -150,15 +246,3 @@ class ConversationVariableDetailApi(Resource):
raise NotFound("Conversation Variable Not Exists.")
except services.errors.conversation.ConversationVariableTypeMismatchError as e:
raise BadRequest(str(e))
api.add_resource(ConversationRenameApi, "/conversations/<uuid:c_id>/name", endpoint="conversation_name")
api.add_resource(ConversationApi, "/conversations")
api.add_resource(ConversationDetailApi, "/conversations/<uuid:c_id>", endpoint="conversation_detail")
api.add_resource(ConversationVariablesApi, "/conversations/<uuid:c_id>/variables", endpoint="conversation_variables")
api.add_resource(
ConversationVariableDetailApi,
"/conversations/<uuid:c_id>/variables/<uuid:variable_id>",
endpoint="conversation_variable_detail",
methods=["PUT"],
)

View File

@ -1,5 +1,6 @@
from flask import request
from flask_restful import Resource, marshal_with
from flask_restx import Resource
from flask_restx.api import HTTPStatus
import services
from controllers.common.errors import (
@ -9,17 +10,33 @@ from controllers.common.errors import (
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.service_api import api
from controllers.service_api import service_api_ns
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from fields.file_fields import file_fields
from fields.file_fields import build_file_model
from models.model import App, EndUser
from services.file_service import FileService
@service_api_ns.route("/files/upload")
class FileApi(Resource):
@service_api_ns.doc("upload_file")
@service_api_ns.doc(description="Upload a file for use in conversations")
@service_api_ns.doc(
responses={
201: "File uploaded successfully",
400: "Bad request - no file or invalid file",
401: "Unauthorized - invalid API token",
413: "File too large",
415: "Unsupported file type",
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM))
@marshal_with(file_fields)
@service_api_ns.marshal_with(build_file_model(service_api_ns), code=HTTPStatus.CREATED)
def post(self, app_model: App, end_user: EndUser):
"""Upload a file for use in conversations.
Accepts a single file upload via multipart/form-data.
"""
# check file
if "file" not in request.files:
raise NoFileUploadedError()
@ -47,6 +64,3 @@ class FileApi(Resource):
raise UnsupportedFileTypeError()
return upload_file, 201
api.add_resource(FileApi, "/files/upload")

Some files were not shown because too many files have changed in this diff Show More