Compare commits
40 Commits
main
...
feat/webap
| Author | SHA1 | Date |
|---|---|---|
|
|
859f3946c7 | 12 months ago |
|
|
eabd34b2ae | 12 months ago |
|
|
777cb06c38 | 12 months ago |
|
|
ede078c522 | 12 months ago |
|
|
a9ea410b22 | 12 months ago |
|
|
fb9e5b4376 | 12 months ago |
|
|
652c603d6a | 12 months ago |
|
|
180606a11b | 12 months ago |
|
|
bc8f741859 | 12 months ago |
|
|
3c3261b42e | 12 months ago |
|
|
6c133dc45c | 12 months ago |
|
|
4e17af5326 | 12 months ago |
|
|
743672f78d | 12 months ago |
|
|
cb31b5e8b2 | 12 months ago |
|
|
50d80f6d71 | 12 months ago |
|
|
88c59f06cb | 12 months ago |
|
|
28eb95276d | 12 months ago |
|
|
b39bd0325d | 12 months ago |
|
|
1845d56234 | 12 months ago |
|
|
543eada1be | 12 months ago |
|
|
c4821a333f | 12 months ago |
|
|
8c78286e5f | 12 months ago |
|
|
bb9ec1a587 | 12 months ago |
|
|
cbcf06ed65 | 12 months ago |
|
|
38097a75da | 12 months ago |
|
|
dc79ec52ea | 12 months ago |
|
|
20ca9c6a3e | 12 months ago |
|
|
6c3804ca49 | 12 months ago |
|
|
a321d39cb4 | 12 months ago |
|
|
25f1352675 | 12 months ago |
|
|
1e5df79e05 | 12 months ago |
|
|
5814b097b8 | 12 months ago |
|
|
abd13cf2f8 | 12 months ago |
|
|
60ce8f6053 | 12 months ago |
|
|
2616f89d46 | 12 months ago |
|
|
99ac7f0e97 | 12 months ago |
|
|
e57bf8c959 | 12 months ago |
|
|
14ddb90525 | 12 months ago |
|
|
2cb36a5b70 | 12 months ago |
|
|
467a1f4063 | 1 year ago |
@ -1,11 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: "\U0001F4A1 Model Providers & Plugins"
|
|
||||||
url: "https://github.com/langgenius/dify-official-plugins/issues/new/choose"
|
|
||||||
about: Report issues with official plugins or model providers, you will need to provide the plugin version and other relevant details.
|
|
||||||
- name: "\U0001F4AC Documentation Issues"
|
|
||||||
url: "https://github.com/langgenius/dify-docs/issues/new"
|
|
||||||
about: Report issues with the documentation, such as typos, outdated information, or missing content. Please provide the specific section and details of the issue.
|
|
||||||
- name: "\U0001F4E7 Discussions"
|
- name: "\U0001F4E7 Discussions"
|
||||||
url: https://github.com/langgenius/dify/discussions/categories/general
|
url: https://github.com/langgenius/dify/discussions/categories/general
|
||||||
about: General discussions and seek help from the community
|
about: General discussions and request help from the community
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
name: autofix.ci
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
autofix:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Use uv to ensure we have the same ruff version in CI and locally.
|
|
||||||
- uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f
|
|
||||||
- run: |
|
|
||||||
cd api
|
|
||||||
uv sync --dev
|
|
||||||
# Fix lint errors
|
|
||||||
uv run ruff check --fix-only .
|
|
||||||
# Format code
|
|
||||||
uv run ruff format .
|
|
||||||
|
|
||||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
|
||||||
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
name: Deploy RAG Dev
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Build and Push API & Web"]
|
|
||||||
branches:
|
|
||||||
- "deploy/rag-dev"
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.event.workflow_run.head_branch == 'deploy/rag-dev'
|
|
||||||
steps:
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@v0.1.8
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.RAG_SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
script: |
|
|
||||||
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
class MatrixoneConfig(BaseModel):
|
|
||||||
"""Matrixone vector database configuration."""
|
|
||||||
|
|
||||||
MATRIXONE_HOST: str = Field(default="localhost", description="Host address of the Matrixone server")
|
|
||||||
MATRIXONE_PORT: int = Field(default=6001, description="Port number of the Matrixone server")
|
|
||||||
MATRIXONE_USER: str = Field(default="dump", description="Username for authenticating with Matrixone")
|
|
||||||
MATRIXONE_PASSWORD: str = Field(default="111", description="Password for authenticating with Matrixone")
|
|
||||||
MATRIXONE_DATABASE: str = Field(default="dify", description="Name of the Matrixone database to connect to")
|
|
||||||
MATRIXONE_METRIC: str = Field(
|
|
||||||
default="l2", description="Distance metric type for vector similarity search (cosine or l2)"
|
|
||||||
)
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
from pydantic import BaseModel, Field
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class PyProjectConfig(BaseModel):
|
|
||||||
version: str = Field(description="Dify version", default="")
|
|
||||||
|
|
||||||
|
|
||||||
class PyProjectTomlConfig(BaseSettings):
|
|
||||||
"""
|
|
||||||
configs in api/pyproject.toml
|
|
||||||
"""
|
|
||||||
|
|
||||||
project: PyProjectConfig = Field(
|
|
||||||
description="configs in the project section of pyproject.toml",
|
|
||||||
default=PyProjectConfig(),
|
|
||||||
)
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
import json
|
|
||||||
from enum import StrEnum
|
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_restful import Resource, marshal_with, reqparse
|
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
|
|
||||||
from controllers.console import api
|
|
||||||
from controllers.console.app.wraps import get_app_model
|
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
|
||||||
from extensions.ext_database import db
|
|
||||||
from fields.app_fields import app_server_fields
|
|
||||||
from libs.login import login_required
|
|
||||||
from models.model import AppMCPServer
|
|
||||||
|
|
||||||
|
|
||||||
class AppMCPServerStatus(StrEnum):
|
|
||||||
ACTIVE = "active"
|
|
||||||
INACTIVE = "inactive"
|
|
||||||
|
|
||||||
|
|
||||||
class AppMCPServerController(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model
|
|
||||||
@marshal_with(app_server_fields)
|
|
||||||
def get(self, app_model):
|
|
||||||
server = db.session.query(AppMCPServer).where(AppMCPServer.app_id == app_model.id).first()
|
|
||||||
return server
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model
|
|
||||||
@marshal_with(app_server_fields)
|
|
||||||
def post(self, app_model):
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise NotFound()
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("description", type=str, required=False, location="json")
|
|
||||||
parser.add_argument("parameters", type=dict, required=True, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
description = args.get("description")
|
|
||||||
if not description:
|
|
||||||
description = app_model.description or ""
|
|
||||||
|
|
||||||
server = AppMCPServer(
|
|
||||||
name=app_model.name,
|
|
||||||
description=description,
|
|
||||||
parameters=json.dumps(args["parameters"], ensure_ascii=False),
|
|
||||||
status=AppMCPServerStatus.ACTIVE,
|
|
||||||
app_id=app_model.id,
|
|
||||||
tenant_id=current_user.current_tenant_id,
|
|
||||||
server_code=AppMCPServer.generate_server_code(16),
|
|
||||||
)
|
|
||||||
db.session.add(server)
|
|
||||||
db.session.commit()
|
|
||||||
return server
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model
|
|
||||||
@marshal_with(app_server_fields)
|
|
||||||
def put(self, app_model):
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise NotFound()
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument("id", type=str, required=True, location="json")
|
|
||||||
parser.add_argument("description", type=str, required=False, location="json")
|
|
||||||
parser.add_argument("parameters", type=dict, required=True, location="json")
|
|
||||||
parser.add_argument("status", type=str, required=False, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
server = db.session.query(AppMCPServer).where(AppMCPServer.id == args["id"]).first()
|
|
||||||
if not server:
|
|
||||||
raise NotFound()
|
|
||||||
|
|
||||||
description = args.get("description")
|
|
||||||
if description is None:
|
|
||||||
pass
|
|
||||||
elif not description:
|
|
||||||
server.description = app_model.description or ""
|
|
||||||
else:
|
|
||||||
server.description = description
|
|
||||||
|
|
||||||
server.parameters = json.dumps(args["parameters"], ensure_ascii=False)
|
|
||||||
if args["status"]:
|
|
||||||
if args["status"] not in [status.value for status in AppMCPServerStatus]:
|
|
||||||
raise ValueError("Invalid status")
|
|
||||||
server.status = args["status"]
|
|
||||||
db.session.commit()
|
|
||||||
return server
|
|
||||||
|
|
||||||
|
|
||||||
class AppMCPServerRefreshController(Resource):
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@marshal_with(app_server_fields)
|
|
||||||
def get(self, server_id):
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise NotFound()
|
|
||||||
server = (
|
|
||||||
db.session.query(AppMCPServer)
|
|
||||||
.where(AppMCPServer.id == server_id)
|
|
||||||
.where(AppMCPServer.tenant_id == current_user.current_tenant_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not server:
|
|
||||||
raise NotFound()
|
|
||||||
server.server_code = AppMCPServer.generate_server_code(16)
|
|
||||||
db.session.commit()
|
|
||||||
return server
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppMCPServerController, "/apps/<uuid:app_id>/server")
|
|
||||||
api.add_resource(AppMCPServerRefreshController, "/apps/<uuid:server_id>/server/refresh")
|
|
||||||
@ -1,426 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import Any, NoReturn
|
|
||||||
|
|
||||||
from flask import Response
|
|
||||||
from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from werkzeug.exceptions import Forbidden
|
|
||||||
|
|
||||||
from controllers.console import api
|
|
||||||
from controllers.console.app.error import (
|
|
||||||
DraftWorkflowNotExist,
|
|
||||||
)
|
|
||||||
from controllers.console.app.wraps import get_app_model
|
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
|
||||||
from controllers.web.error import InvalidArgumentError, NotFoundError
|
|
||||||
from core.variables.segment_group import SegmentGroup
|
|
||||||
from core.variables.segments import ArrayFileSegment, FileSegment, Segment
|
|
||||||
from core.variables.types import SegmentType
|
|
||||||
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
|
||||||
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.workflow import WorkflowDraftVariable
|
|
||||||
from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService
|
|
||||||
from services.workflow_service import WorkflowService
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_values_to_json_serializable_object(value: Segment) -> Any:
|
|
||||||
if isinstance(value, FileSegment):
|
|
||||||
return value.value.model_dump()
|
|
||||||
elif isinstance(value, ArrayFileSegment):
|
|
||||||
return [i.model_dump() for i in value.value]
|
|
||||||
elif isinstance(value, SegmentGroup):
|
|
||||||
return [_convert_values_to_json_serializable_object(i) for i in value.value]
|
|
||||||
else:
|
|
||||||
return value.value
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_var_value(variable: WorkflowDraftVariable) -> Any:
|
|
||||||
value = variable.get_value()
|
|
||||||
# create a copy of the value to avoid affecting the model cache.
|
|
||||||
value = value.model_copy(deep=True)
|
|
||||||
# Refresh the url signature before returning it to client.
|
|
||||||
if isinstance(value, FileSegment):
|
|
||||||
file = value.value
|
|
||||||
file.remote_url = file.generate_url()
|
|
||||||
elif isinstance(value, ArrayFileSegment):
|
|
||||||
files = value.value
|
|
||||||
for file in files:
|
|
||||||
file.remote_url = file.generate_url()
|
|
||||||
return _convert_values_to_json_serializable_object(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_pagination_parser():
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument(
|
|
||||||
"page",
|
|
||||||
type=inputs.int_range(1, 100_000),
|
|
||||||
required=False,
|
|
||||||
default=1,
|
|
||||||
location="args",
|
|
||||||
help="the page of data requested",
|
|
||||||
)
|
|
||||||
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_variable_type(workflow_draft_var: WorkflowDraftVariable) -> str:
|
|
||||||
value_type = workflow_draft_var.value_type
|
|
||||||
return value_type.exposed_type().value
|
|
||||||
|
|
||||||
|
|
||||||
_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS = {
|
|
||||||
"id": fields.String,
|
|
||||||
"type": fields.String(attribute=lambda model: model.get_variable_type()),
|
|
||||||
"name": fields.String,
|
|
||||||
"description": fields.String,
|
|
||||||
"selector": fields.List(fields.String, attribute=lambda model: model.get_selector()),
|
|
||||||
"value_type": fields.String(attribute=_serialize_variable_type),
|
|
||||||
"edited": fields.Boolean(attribute=lambda model: model.edited),
|
|
||||||
"visible": fields.Boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
_WORKFLOW_DRAFT_VARIABLE_FIELDS = dict(
|
|
||||||
_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS,
|
|
||||||
value=fields.Raw(attribute=_serialize_var_value),
|
|
||||||
)
|
|
||||||
|
|
||||||
_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS = {
|
|
||||||
"id": fields.String,
|
|
||||||
"type": fields.String(attribute=lambda _: "env"),
|
|
||||||
"name": fields.String,
|
|
||||||
"description": fields.String,
|
|
||||||
"selector": fields.List(fields.String, attribute=lambda model: model.get_selector()),
|
|
||||||
"value_type": fields.String(attribute=_serialize_variable_type),
|
|
||||||
"edited": fields.Boolean(attribute=lambda model: model.edited),
|
|
||||||
"visible": fields.Boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
_WORKFLOW_DRAFT_ENV_VARIABLE_LIST_FIELDS = {
|
|
||||||
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS)),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_items(var_list: WorkflowDraftVariableList) -> list[WorkflowDraftVariable]:
|
|
||||||
return var_list.variables
|
|
||||||
|
|
||||||
|
|
||||||
_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS = {
|
|
||||||
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS), attribute=_get_items),
|
|
||||||
"total": fields.Raw(),
|
|
||||||
}
|
|
||||||
|
|
||||||
_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS = {
|
|
||||||
"items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_FIELDS), attribute=_get_items),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _api_prerequisite(f):
|
|
||||||
"""Common prerequisites for all draft workflow variable APIs.
|
|
||||||
|
|
||||||
It ensures the following conditions are satisfied:
|
|
||||||
|
|
||||||
- Dify has been property setup.
|
|
||||||
- The request user has logged in and initialized.
|
|
||||||
- The requested app is a workflow or a chat flow.
|
|
||||||
- The request user has the edit permission for the app.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@setup_required
|
|
||||||
@login_required
|
|
||||||
@account_initialization_required
|
|
||||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if not current_user.is_editor:
|
|
||||||
raise Forbidden()
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowVariableCollectionApi(Resource):
|
|
||||||
@_api_prerequisite
|
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
|
||||||
def get(self, app_model: App):
|
|
||||||
"""
|
|
||||||
Get draft workflow
|
|
||||||
"""
|
|
||||||
parser = _create_pagination_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# fetch draft workflow by app_model
|
|
||||||
workflow_service = WorkflowService()
|
|
||||||
workflow_exist = workflow_service.is_workflow_exist(app_model=app_model)
|
|
||||||
if not workflow_exist:
|
|
||||||
raise DraftWorkflowNotExist()
|
|
||||||
|
|
||||||
# fetch draft workflow by app_model
|
|
||||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=session,
|
|
||||||
)
|
|
||||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
|
||||||
app_id=app_model.id,
|
|
||||||
page=args.page,
|
|
||||||
limit=args.limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
return workflow_vars
|
|
||||||
|
|
||||||
@_api_prerequisite
|
|
||||||
def delete(self, app_model: App):
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=db.session(),
|
|
||||||
)
|
|
||||||
draft_var_srv.delete_workflow_variables(app_model.id)
|
|
||||||
db.session.commit()
|
|
||||||
return Response("", 204)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_node_id(node_id: str) -> NoReturn | None:
|
|
||||||
if node_id in [
|
|
||||||
CONVERSATION_VARIABLE_NODE_ID,
|
|
||||||
SYSTEM_VARIABLE_NODE_ID,
|
|
||||||
]:
|
|
||||||
# NOTE(QuantumGhost): While we store the system and conversation variables as node variables
|
|
||||||
# with specific `node_id` in database, we still want to make the API separated. By disallowing
|
|
||||||
# accessing system and conversation variables in `WorkflowDraftNodeVariableListApi`,
|
|
||||||
# we mitigate the risk that user of the API depending on the implementation detail of the API.
|
|
||||||
#
|
|
||||||
# ref: [Hyrum's Law](https://www.hyrumslaw.com/)
|
|
||||||
|
|
||||||
raise InvalidArgumentError(
|
|
||||||
f"invalid node_id, please use correspond api for conversation and system variables, node_id={node_id}",
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class NodeVariableCollectionApi(Resource):
|
|
||||||
@_api_prerequisite
|
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
|
||||||
def get(self, app_model: App, node_id: str):
|
|
||||||
validate_node_id(node_id)
|
|
||||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=session,
|
|
||||||
)
|
|
||||||
node_vars = draft_var_srv.list_node_variables(app_model.id, node_id)
|
|
||||||
|
|
||||||
return node_vars
|
|
||||||
|
|
||||||
@_api_prerequisite
|
|
||||||
def delete(self, app_model: App, node_id: str):
|
|
||||||
validate_node_id(node_id)
|
|
||||||
srv = WorkflowDraftVariableService(db.session())
|
|
||||||
srv.delete_node_variables(app_model.id, node_id)
|
|
||||||
db.session.commit()
|
|
||||||
return Response("", 204)
|
|
||||||
|
|
||||||
|
|
||||||
class VariableApi(Resource):
|
|
||||||
_PATCH_NAME_FIELD = "name"
|
|
||||||
_PATCH_VALUE_FIELD = "value"
|
|
||||||
|
|
||||||
@_api_prerequisite
|
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
|
||||||
def get(self, app_model: App, variable_id: str):
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=db.session(),
|
|
||||||
)
|
|
||||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
|
||||||
if variable is None:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
if variable.app_id != app_model.id:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
return variable
|
|
||||||
|
|
||||||
@_api_prerequisite
|
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
|
||||||
def patch(self, app_model: App, variable_id: str):
|
|
||||||
# Request payload for file types:
|
|
||||||
#
|
|
||||||
# Local File:
|
|
||||||
#
|
|
||||||
# {
|
|
||||||
# "type": "image",
|
|
||||||
# "transfer_method": "local_file",
|
|
||||||
# "url": "",
|
|
||||||
# "upload_file_id": "daded54f-72c7-4f8e-9d18-9b0abdd9f190"
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# Remote File:
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# {
|
|
||||||
# "type": "image",
|
|
||||||
# "transfer_method": "remote_url",
|
|
||||||
# "url": "http://127.0.0.1:5001/files/1602650a-4fe4-423c-85a2-af76c083e3c4/file-preview?timestamp=1750041099&nonce=...&sign=...=",
|
|
||||||
# "upload_file_id": "1602650a-4fe4-423c-85a2-af76c083e3c4"
|
|
||||||
# }
|
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
|
||||||
parser.add_argument(self._PATCH_NAME_FIELD, type=str, required=False, nullable=True, location="json")
|
|
||||||
# Parse 'value' field as-is to maintain its original data structure
|
|
||||||
parser.add_argument(self._PATCH_VALUE_FIELD, type=lambda x: x, required=False, nullable=True, location="json")
|
|
||||||
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=db.session(),
|
|
||||||
)
|
|
||||||
args = parser.parse_args(strict=True)
|
|
||||||
|
|
||||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
|
||||||
if variable is None:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
if variable.app_id != app_model.id:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
|
|
||||||
new_name = args.get(self._PATCH_NAME_FIELD, None)
|
|
||||||
raw_value = args.get(self._PATCH_VALUE_FIELD, None)
|
|
||||||
if new_name is None and raw_value is None:
|
|
||||||
return variable
|
|
||||||
|
|
||||||
new_value = None
|
|
||||||
if raw_value is not None:
|
|
||||||
if variable.value_type == SegmentType.FILE:
|
|
||||||
if not isinstance(raw_value, dict):
|
|
||||||
raise InvalidArgumentError(description=f"expected dict for file, got {type(raw_value)}")
|
|
||||||
raw_value = build_from_mapping(mapping=raw_value, tenant_id=app_model.tenant_id)
|
|
||||||
elif variable.value_type == SegmentType.ARRAY_FILE:
|
|
||||||
if not isinstance(raw_value, list):
|
|
||||||
raise InvalidArgumentError(description=f"expected list for files, got {type(raw_value)}")
|
|
||||||
if len(raw_value) > 0 and not isinstance(raw_value[0], dict):
|
|
||||||
raise InvalidArgumentError(description=f"expected dict for files[0], got {type(raw_value)}")
|
|
||||||
raw_value = build_from_mappings(mappings=raw_value, tenant_id=app_model.tenant_id)
|
|
||||||
new_value = build_segment_with_type(variable.value_type, raw_value)
|
|
||||||
draft_var_srv.update_variable(variable, name=new_name, value=new_value)
|
|
||||||
db.session.commit()
|
|
||||||
return variable
|
|
||||||
|
|
||||||
@_api_prerequisite
|
|
||||||
def delete(self, app_model: App, variable_id: str):
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=db.session(),
|
|
||||||
)
|
|
||||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
|
||||||
if variable is None:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
if variable.app_id != app_model.id:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
draft_var_srv.delete_variable(variable)
|
|
||||||
db.session.commit()
|
|
||||||
return Response("", 204)
|
|
||||||
|
|
||||||
|
|
||||||
class VariableResetApi(Resource):
|
|
||||||
@_api_prerequisite
|
|
||||||
def put(self, app_model: App, variable_id: str):
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=db.session(),
|
|
||||||
)
|
|
||||||
|
|
||||||
workflow_srv = WorkflowService()
|
|
||||||
draft_workflow = workflow_srv.get_draft_workflow(app_model)
|
|
||||||
if draft_workflow is None:
|
|
||||||
raise NotFoundError(
|
|
||||||
f"Draft workflow not found, app_id={app_model.id}",
|
|
||||||
)
|
|
||||||
variable = draft_var_srv.get_variable(variable_id=variable_id)
|
|
||||||
if variable is None:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
if variable.app_id != app_model.id:
|
|
||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
|
||||||
|
|
||||||
resetted = draft_var_srv.reset_variable(draft_workflow, variable)
|
|
||||||
db.session.commit()
|
|
||||||
if resetted is None:
|
|
||||||
return Response("", 204)
|
|
||||||
else:
|
|
||||||
return marshal(resetted, _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList:
|
|
||||||
with Session(bind=db.engine, expire_on_commit=False) as session:
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
|
||||||
session=session,
|
|
||||||
)
|
|
||||||
if node_id == CONVERSATION_VARIABLE_NODE_ID:
|
|
||||||
draft_vars = draft_var_srv.list_conversation_variables(app_model.id)
|
|
||||||
elif node_id == SYSTEM_VARIABLE_NODE_ID:
|
|
||||||
draft_vars = draft_var_srv.list_system_variables(app_model.id)
|
|
||||||
else:
|
|
||||||
draft_vars = draft_var_srv.list_node_variables(app_id=app_model.id, node_id=node_id)
|
|
||||||
return draft_vars
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationVariableCollectionApi(Resource):
|
|
||||||
@_api_prerequisite
|
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
|
||||||
def get(self, app_model: App):
|
|
||||||
# NOTE(QuantumGhost): Prefill conversation variables into the draft variables table
|
|
||||||
# so their IDs can be returned to the caller.
|
|
||||||
workflow_srv = WorkflowService()
|
|
||||||
draft_workflow = workflow_srv.get_draft_workflow(app_model)
|
|
||||||
if draft_workflow is None:
|
|
||||||
raise NotFoundError(description=f"draft workflow not found, id={app_model.id}")
|
|
||||||
draft_var_srv = WorkflowDraftVariableService(db.session())
|
|
||||||
draft_var_srv.prefill_conversation_variable_default_values(draft_workflow)
|
|
||||||
db.session.commit()
|
|
||||||
return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID)
|
|
||||||
|
|
||||||
|
|
||||||
class SystemVariableCollectionApi(Resource):
|
|
||||||
@_api_prerequisite
|
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
|
||||||
def get(self, app_model: App):
|
|
||||||
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentVariableCollectionApi(Resource):
|
|
||||||
@_api_prerequisite
|
|
||||||
def get(self, app_model: App):
|
|
||||||
"""
|
|
||||||
Get draft workflow
|
|
||||||
"""
|
|
||||||
# fetch draft workflow by app_model
|
|
||||||
workflow_service = WorkflowService()
|
|
||||||
workflow = workflow_service.get_draft_workflow(app_model=app_model)
|
|
||||||
if workflow is None:
|
|
||||||
raise DraftWorkflowNotExist()
|
|
||||||
|
|
||||||
env_vars = workflow.environment_variables
|
|
||||||
env_vars_list = []
|
|
||||||
for v in env_vars:
|
|
||||||
env_vars_list.append(
|
|
||||||
{
|
|
||||||
"id": v.id,
|
|
||||||
"type": "env",
|
|
||||||
"name": v.name,
|
|
||||||
"description": v.description,
|
|
||||||
"selector": v.selector,
|
|
||||||
"value_type": v.value_type.exposed_type().value,
|
|
||||||
"value": v.value,
|
|
||||||
# Do not track edited for env vars.
|
|
||||||
"edited": False,
|
|
||||||
"visible": True,
|
|
||||||
"editable": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"items": env_vars_list}
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowVariableCollectionApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/variables",
|
|
||||||
)
|
|
||||||
api.add_resource(NodeVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/variables")
|
|
||||||
api.add_resource(VariableApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>")
|
|
||||||
api.add_resource(VariableResetApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>/reset")
|
|
||||||
|
|
||||||
api.add_resource(ConversationVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/conversation-variables")
|
|
||||||
api.add_resource(SystemVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/system-variables")
|
|
||||||
api.add_resource(EnvironmentVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/environment-variables")
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
from flask import Blueprint
|
|
||||||
|
|
||||||
from libs.external_api import ExternalApi
|
|
||||||
|
|
||||||
bp = Blueprint("mcp", __name__, url_prefix="/mcp")
|
|
||||||
api = ExternalApi(bp)
|
|
||||||
|
|
||||||
from . import mcp
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
from flask_restful import Resource, reqparse
|
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
from controllers.console.app.mcp_server import AppMCPServerStatus
|
|
||||||
from controllers.mcp import api
|
|
||||||
from core.app.app_config.entities import VariableEntity
|
|
||||||
from core.mcp import types
|
|
||||||
from core.mcp.server.streamable_http import MCPServerStreamableHTTPRequestHandler
|
|
||||||
from core.mcp.types import ClientNotification, ClientRequest
|
|
||||||
from core.mcp.utils import create_mcp_error_response
|
|
||||||
from extensions.ext_database import db
|
|
||||||
from libs import helper
|
|
||||||
from models.model import App, AppMCPServer, AppMode
|
|
||||||
|
|
||||||
|
|
||||||
class MCPAppApi(Resource):
|
|
||||||
def post(self, server_code):
|
|
||||||
def int_or_str(value):
|
|
||||||
if isinstance(value, (int, str)):
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
request_id = args.get("id")
|
|
||||||
|
|
||||||
server = db.session.query(AppMCPServer).where(AppMCPServer.server_code == server_code).first()
|
|
||||||
if not server:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "Server Not Found")
|
|
||||||
)
|
|
||||||
|
|
||||||
if server.status != AppMCPServerStatus.ACTIVE:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "Server is not active")
|
|
||||||
)
|
|
||||||
|
|
||||||
app = db.session.query(App).where(App.id == server.app_id).first()
|
|
||||||
if not app:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "App Not Found")
|
|
||||||
)
|
|
||||||
|
|
||||||
if app.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
|
|
||||||
workflow = app.workflow
|
|
||||||
if workflow is None:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "App is unavailable")
|
|
||||||
)
|
|
||||||
|
|
||||||
user_input_form = workflow.user_input_form(to_old_structure=True)
|
|
||||||
else:
|
|
||||||
app_model_config = app.app_model_config
|
|
||||||
if app_model_config is None:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_REQUEST, "App is unavailable")
|
|
||||||
)
|
|
||||||
|
|
||||||
features_dict = app_model_config.to_dict()
|
|
||||||
user_input_form = features_dict.get("user_input_form", [])
|
|
||||||
converted_user_input_form: list[VariableEntity] = []
|
|
||||||
try:
|
|
||||||
for item in user_input_form:
|
|
||||||
variable_type = item.get("type", "") or list(item.keys())[0]
|
|
||||||
variable = item[variable_type]
|
|
||||||
converted_user_input_form.append(
|
|
||||||
VariableEntity(
|
|
||||||
type=variable_type,
|
|
||||||
variable=variable.get("variable"),
|
|
||||||
description=variable.get("description") or "",
|
|
||||||
label=variable.get("label"),
|
|
||||||
required=variable.get("required", False),
|
|
||||||
max_length=variable.get("max_length"),
|
|
||||||
options=variable.get("options") or [],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except ValidationError as e:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_PARAMS, f"Invalid user_input_form: {str(e)}")
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
request: ClientRequest | ClientNotification = ClientRequest.model_validate(args)
|
|
||||||
except ValidationError as e:
|
|
||||||
try:
|
|
||||||
notification = ClientNotification.model_validate(args)
|
|
||||||
request = notification
|
|
||||||
except ValidationError as e:
|
|
||||||
return helper.compact_generate_response(
|
|
||||||
create_mcp_error_response(request_id, types.INVALID_PARAMS, f"Invalid MCP request: {str(e)}")
|
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue