Commit c77029e6 authored by Jyong's avatar Jyong

Merge branch 'main' into feat/dataset-notion-import

parents ff1aadf9 f65a3ad1
#!/usr/bin/env bash
set -eo pipefail
SHA=$(git rev-parse HEAD)
REPO_NAME=langgenius/dify
API_REPO_NAME="${REPO_NAME}-api"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
REFSPEC=$(echo "${GITHUB_HEAD_REF}" | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
PR_NUM=$(echo "${GITHUB_REF}" | sed 's:refs/pull/::' | sed 's:/merge::')
LATEST_TAG="pr-${PR_NUM}"
CACHE_FROM_TAG="latest"
elif [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/tags/::' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="latest"
else
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/heads/::' | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="${REFSPEC}"
fi
if [[ "${REFSPEC}" == "main" ]]; then
LATEST_TAG="latest"
CACHE_FROM_TAG="latest"
fi
echo "Pulling cache image ${API_REPO_NAME}:${CACHE_FROM_TAG}"
if docker pull "${API_REPO_NAME}:${CACHE_FROM_TAG}"; then
API_CACHE_FROM_SCRIPT="--cache-from ${API_REPO_NAME}:${CACHE_FROM_TAG}"
else
echo "WARNING: Failed to pull ${API_REPO_NAME}:${CACHE_FROM_TAG}, disable build image cache."
API_CACHE_FROM_SCRIPT=""
fi
cat<<EOF
Rolling with tags:
- ${API_REPO_NAME}:${SHA}
- ${API_REPO_NAME}:${REFSPEC}
- ${API_REPO_NAME}:${LATEST_TAG}
EOF
#
# Build image
#
cd api
docker build \
${API_CACHE_FROM_SCRIPT} \
--build-arg COMMIT_SHA=${SHA} \
-t "${API_REPO_NAME}:${SHA}" \
-t "${API_REPO_NAME}:${REFSPEC}" \
-t "${API_REPO_NAME}:${LATEST_TAG}" \
--label "sha=${SHA}" \
--label "built_at=$(date)" \
--label "build_actor=${GITHUB_ACTOR}" \
.
# push
docker push --all-tags "${API_REPO_NAME}"
...@@ -5,16 +5,19 @@ on: ...@@ -5,16 +5,19 @@ on:
branches: branches:
- 'main' - 'main'
- 'deploy/dev' - 'deploy/dev'
release:
types: [published]
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: Set up QEMU
uses: actions/checkout@v2 uses: docker/setup-qemu-action@v2
with:
persist-credentials: false - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
...@@ -22,13 +25,29 @@ jobs: ...@@ -22,13 +25,29 @@ jobs:
username: ${{ secrets.DOCKERHUB_USER }} username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image - name: Extract metadata (tags, labels) for Docker
shell: bash id: meta
env: uses: docker/metadata-action@v4
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} with:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} images: langgenius/dify-api
run: | tags: |
/bin/bash .github/workflows/build-api-image.sh type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: "{{defaultContext}}:api"
platforms: linux/amd64,linux/arm64
build-args: |
COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Deploy to server - name: Deploy to server
if: github.ref == 'refs/heads/deploy/dev' if: github.ref == 'refs/heads/deploy/dev'
......
#!/usr/bin/env bash
set -eo pipefail
SHA=$(git rev-parse HEAD)
REPO_NAME=langgenius/dify
WEB_REPO_NAME="${REPO_NAME}-web"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
REFSPEC=$(echo "${GITHUB_HEAD_REF}" | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
PR_NUM=$(echo "${GITHUB_REF}" | sed 's:refs/pull/::' | sed 's:/merge::')
LATEST_TAG="pr-${PR_NUM}"
CACHE_FROM_TAG="latest"
elif [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/tags/::' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="latest"
else
REFSPEC=$(echo "${GITHUB_REF}" | sed 's:refs/heads/::' | sed 's/[^a-zA-Z0-9]/-/g' | head -c 40)
LATEST_TAG="${REFSPEC}"
CACHE_FROM_TAG="${REFSPEC}"
fi
if [[ "${REFSPEC}" == "main" ]]; then
LATEST_TAG="latest"
CACHE_FROM_TAG="latest"
fi
echo "Pulling cache image ${WEB_REPO_NAME}:${CACHE_FROM_TAG}"
if docker pull "${WEB_REPO_NAME}:${CACHE_FROM_TAG}"; then
WEB_CACHE_FROM_SCRIPT="--cache-from ${WEB_REPO_NAME}:${CACHE_FROM_TAG}"
else
echo "WARNING: Failed to pull ${WEB_REPO_NAME}:${CACHE_FROM_TAG}, disable build image cache."
WEB_CACHE_FROM_SCRIPT=""
fi
cat<<EOF
Rolling with tags:
- ${WEB_REPO_NAME}:${SHA}
- ${WEB_REPO_NAME}:${REFSPEC}
- ${WEB_REPO_NAME}:${LATEST_TAG}
EOF
#
# Build image
#
cd web
docker build \
${WEB_CACHE_FROM_SCRIPT} \
--build-arg COMMIT_SHA=${SHA} \
-t "${WEB_REPO_NAME}:${SHA}" \
-t "${WEB_REPO_NAME}:${REFSPEC}" \
-t "${WEB_REPO_NAME}:${LATEST_TAG}" \
--label "sha=${SHA}" \
--label "built_at=$(date)" \
--label "build_actor=${GITHUB_ACTOR}" \
.
docker push --all-tags "${WEB_REPO_NAME}"
\ No newline at end of file
...@@ -5,16 +5,19 @@ on: ...@@ -5,16 +5,19 @@ on:
branches: branches:
- 'main' - 'main'
- 'deploy/dev' - 'deploy/dev'
release:
types: [published]
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
steps: steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - name: Set up QEMU
uses: actions/checkout@v2 uses: docker/setup-qemu-action@v2
with:
persist-credentials: false - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
...@@ -22,13 +25,29 @@ jobs: ...@@ -22,13 +25,29 @@ jobs:
username: ${{ secrets.DOCKERHUB_USER }} username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image - name: Extract metadata (tags, labels) for Docker
shell: bash id: meta
env: uses: docker/metadata-action@v4
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} with:
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} images: langgenius/dify-web
run: | tags: |
/bin/bash .github/workflows/build-web-image.sh type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: "{{defaultContext}}:web"
platforms: linux/amd64,linux/arm64
build-args: |
COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Deploy to server - name: Deploy to server
if: github.ref == 'refs/heads/deploy/dev' if: github.ref == 'refs/heads/deploy/dev'
......
...@@ -130,7 +130,6 @@ dmypy.json ...@@ -130,7 +130,6 @@ dmypy.json
.idea/' .idea/'
.DS_Store .DS_Store
.vscode
# Intellij IDEA Files # Intellij IDEA Files
.idea/ .idea/
......
import datetime import datetime
import json
import random import random
import string import string
...@@ -9,7 +8,7 @@ from libs.password import password_pattern, valid_password, hash_password ...@@ -9,7 +8,7 @@ from libs.password import password_pattern, valid_password, hash_password
from libs.helper import email as email_validate from libs.helper import email as email_validate
from extensions.ext_database import db from extensions.ext_database import db
from models.account import InvitationCode from models.account import InvitationCode
from models.model import Account, AppModelConfig, ApiToken, Site, App, RecommendedApp from models.model import Account
import secrets import secrets
import base64 import base64
...@@ -131,30 +130,7 @@ def generate_upper_string(): ...@@ -131,30 +130,7 @@ def generate_upper_string():
return result return result
@click.command('gen-recommended-apps', help='Number of records to generate')
def generate_recommended_apps():
print('Generating recommended app data...')
apps = App.query.all()
for app in apps:
recommended_app = RecommendedApp(
app_id=app.id,
description={
'en': 'Description for ' + app.name,
'zh': '描述 ' + app.name
},
copyright='Copyright ' + str(random.randint(1990, 2020)),
privacy_policy='https://privacypolicy.example.com',
category=random.choice(['Games', 'News', 'Music', 'Sports']),
position=random.randint(1, 100),
install_count=random.randint(100, 100000)
)
db.session.add(recommended_app)
db.session.commit()
print('Done!')
def register_commands(app): def register_commands(app):
app.cli.add_command(reset_password) app.cli.add_command(reset_password)
app.cli.add_command(reset_email) app.cli.add_command(reset_email)
app.cli.add_command(generate_invitation_codes) app.cli.add_command(generate_invitation_codes)
app.cli.add_command(generate_recommended_apps)
...@@ -78,7 +78,7 @@ class Config: ...@@ -78,7 +78,7 @@ class Config:
self.CONSOLE_URL = get_env('CONSOLE_URL') self.CONSOLE_URL = get_env('CONSOLE_URL')
self.API_URL = get_env('API_URL') self.API_URL = get_env('API_URL')
self.APP_URL = get_env('APP_URL') self.APP_URL = get_env('APP_URL')
self.CURRENT_VERSION = "0.2.0" self.CURRENT_VERSION = "0.3.1"
self.COMMIT_SHA = get_env('COMMIT_SHA') self.COMMIT_SHA = get_env('COMMIT_SHA')
self.EDITION = "SELF_HOSTED" self.EDITION = "SELF_HOSTED"
self.DEPLOY_ENV = get_env('DEPLOY_ENV') self.DEPLOY_ENV = get_env('DEPLOY_ENV')
......
...@@ -5,8 +5,11 @@ from libs.external_api import ExternalApi ...@@ -5,8 +5,11 @@ from libs.external_api import ExternalApi
bp = Blueprint('console', __name__, url_prefix='/console/api') bp = Blueprint('console', __name__, url_prefix='/console/api')
api = ExternalApi(bp) api = ExternalApi(bp)
# Import other controllers
from . import setup, version, apikey, admin
# Import app controllers # Import app controllers
from .app import app, site, explore, completion, model_config, statistic, conversation, message from .app import app, site, completion, model_config, statistic, conversation, message, generator
# Import auth controllers # Import auth controllers
from .auth import login, oauth, data_source_oauth from .auth import login, oauth, data_source_oauth
...@@ -14,7 +17,8 @@ from .auth import login, oauth, data_source_oauth ...@@ -14,7 +17,8 @@ from .auth import login, oauth, data_source_oauth
# Import datasets controllers # Import datasets controllers
from .datasets import datasets, datasets_document, datasets_segments, file, hit_testing, data_source from .datasets import datasets, datasets_document, datasets_segments, file, hit_testing, data_source
# Import other controllers # Import workspace controllers
from . import setup, version, apikey
from .workspace import workspace, members, providers, account from .workspace import workspace, members, providers, account
# Import explore controllers
from .explore import installed_app, recommended_app, completion, conversation, message, parameter, saved_message
import os
from functools import wraps
from flask import request
from flask_restful import Resource, reqparse
from werkzeug.exceptions import NotFound, Unauthorized
from controllers.console import api
from controllers.console.wraps import only_edition_cloud
from extensions.ext_database import db
from models.model import RecommendedApp, App, InstalledApp
def admin_required(view):
@wraps(view)
def decorated(*args, **kwargs):
if not os.getenv('ADMIN_API_KEY'):
raise Unauthorized('API key is invalid.')
auth_header = request.headers.get('Authorization')
if auth_header is None:
raise Unauthorized('Authorization header is missing.')
if ' ' not in auth_header:
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != 'bearer':
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
if os.getenv('ADMIN_API_KEY') != auth_token:
raise Unauthorized('API key is invalid.')
return view(*args, **kwargs)
return decorated
class InsertExploreAppListApi(Resource):
@only_edition_cloud
@admin_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('app_id', type=str, required=True, nullable=False, location='json')
parser.add_argument('desc', type=str, location='json')
parser.add_argument('copyright', type=str, location='json')
parser.add_argument('privacy_policy', type=str, location='json')
parser.add_argument('language', type=str, required=True, nullable=False, choices=['en-US', 'zh-Hans'],
location='json')
parser.add_argument('category', type=str, required=True, nullable=False, location='json')
parser.add_argument('position', type=int, required=True, nullable=False, location='json')
args = parser.parse_args()
app = App.query.filter(App.id == args['app_id']).first()
if not app:
raise NotFound('App not found')
site = app.site
if not site:
desc = args['desc'] if args['desc'] else ''
copy_right = args['copyright'] if args['copyright'] else ''
privacy_policy = args['privacy_policy'] if args['privacy_policy'] else ''
else:
desc = site.description if (site.description if not args['desc'] else args['desc']) else ''
copy_right = site.copyright if (site.copyright if not args['copyright'] else args['copyright']) else ''
privacy_policy = site.privacy_policy \
if (site.privacy_policy if not args['privacy_policy'] else args['privacy_policy']) else ''
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args['app_id']).first()
if not recommended_app:
recommended_app = RecommendedApp(
app_id=app.id,
description=desc,
copyright=copy_right,
privacy_policy=privacy_policy,
language=args['language'],
category=args['category'],
position=args['position']
)
db.session.add(recommended_app)
app.is_public = True
db.session.commit()
return {'result': 'success'}, 201
else:
recommended_app.description = desc
recommended_app.copyright = copy_right
recommended_app.privacy_policy = privacy_policy
recommended_app.language = args['language']
recommended_app.category = args['category']
recommended_app.position = args['position']
app.is_public = True
db.session.commit()
return {'result': 'success'}, 200
class InsertExploreAppApi(Resource):
@only_edition_cloud
@admin_required
def delete(self, app_id):
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == str(app_id)).first()
if not recommended_app:
return {'result': 'success'}, 204
app = App.query.filter(App.id == recommended_app.app_id).first()
if app:
app.is_public = False
installed_apps = InstalledApp.query.filter(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id != InstalledApp.app_owner_tenant_id
).all()
for installed_app in installed_apps:
db.session.delete(installed_app)
db.session.delete(recommended_app)
db.session.commit()
return {'result': 'success'}, 204
api.add_resource(InsertExploreAppListApi, '/admin/insert-explore-apps')
api.add_resource(InsertExploreAppApi, '/admin/insert-explore-apps/<uuid:app_id>')
...@@ -9,18 +9,13 @@ from werkzeug.exceptions import Unauthorized, Forbidden ...@@ -9,18 +9,13 @@ from werkzeug.exceptions import Unauthorized, Forbidden
from constants.model_template import model_templates, demo_model_templates from constants.model_template import model_templates, demo_model_templates
from controllers.console import api from controllers.console import api
from controllers.console.app.error import AppNotFoundError, ProviderNotInitializeError, ProviderQuotaExceededError, \ from controllers.console.app.error import AppNotFoundError
CompletionRequestError, ProviderModelCurrentlyNotSupportError
from controllers.console.setup import setup_required from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required
from core.generator.llm_generator import LLMGenerator
from core.llm.error import ProviderTokenNotInitError, QuotaExceededError, LLMBadRequestError, LLMAPIConnectionError, \
LLMAPIUnavailableError, LLMRateLimitError, LLMAuthorizationError, ModelCurrentlyNotSupportError
from events.app_event import app_was_created, app_was_deleted from events.app_event import app_was_created, app_was_deleted
from libs.helper import TimestampField from libs.helper import TimestampField
from extensions.ext_database import db from extensions.ext_database import db
from models.model import App, AppModelConfig, Site, InstalledApp from models.model import App, AppModelConfig, Site
from services.account_service import TenantService
from services.app_model_config_service import AppModelConfigService from services.app_model_config_service import AppModelConfigService
model_config_fields = { model_config_fields = {
...@@ -478,35 +473,6 @@ class AppExport(Resource): ...@@ -478,35 +473,6 @@ class AppExport(Resource):
pass pass
class IntroductionGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('prompt_template', type=str, required=True, location='json')
args = parser.parse_args()
account = current_user
try:
answer = LLMGenerator.generate_introduction(
account.current_tenant_id,
args['prompt_template']
)
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
return {'introduction': answer}
api.add_resource(AppListApi, '/apps') api.add_resource(AppListApi, '/apps')
api.add_resource(AppTemplateApi, '/app-templates') api.add_resource(AppTemplateApi, '/app-templates')
api.add_resource(AppApi, '/apps/<uuid:app_id>') api.add_resource(AppApi, '/apps/<uuid:app_id>')
...@@ -515,4 +481,3 @@ api.add_resource(AppNameApi, '/apps/<uuid:app_id>/name') ...@@ -515,4 +481,3 @@ api.add_resource(AppNameApi, '/apps/<uuid:app_id>/name')
api.add_resource(AppSiteStatus, '/apps/<uuid:app_id>/site-enable') api.add_resource(AppSiteStatus, '/apps/<uuid:app_id>/site-enable')
api.add_resource(AppApiStatus, '/apps/<uuid:app_id>/api-enable') api.add_resource(AppApiStatus, '/apps/<uuid:app_id>/api-enable')
api.add_resource(AppRateLimit, '/apps/<uuid:app_id>/rate-limit') api.add_resource(AppRateLimit, '/apps/<uuid:app_id>/rate-limit')
api.add_resource(IntroductionGenerateApi, '/introduction-generate')
from flask_login import login_required, current_user
from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.error import ProviderNotInitializeError, ProviderQuotaExceededError, \
CompletionRequestError, ProviderModelCurrentlyNotSupportError
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from core.generator.llm_generator import LLMGenerator
from core.llm.error import ProviderTokenNotInitError, QuotaExceededError, LLMBadRequestError, LLMAPIConnectionError, \
LLMAPIUnavailableError, LLMRateLimitError, LLMAuthorizationError, ModelCurrentlyNotSupportError
class IntroductionGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('prompt_template', type=str, required=True, location='json')
args = parser.parse_args()
account = current_user
try:
answer = LLMGenerator.generate_introduction(
account.current_tenant_id,
args['prompt_template']
)
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
return {'introduction': answer}
class RuleGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('audiences', type=str, required=True, nullable=False, location='json')
parser.add_argument('hoping_to_solve', type=str, required=True, nullable=False, location='json')
args = parser.parse_args()
account = current_user
try:
rules = LLMGenerator.generate_rule_config(
account.current_tenant_id,
args['audiences'],
args['hoping_to_solve']
)
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
return rules
api.add_resource(IntroductionGenerateApi, '/introduction-generate')
api.add_resource(RuleGenerateApi, '/rule-generate')
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from decimal import Decimal
from datetime import datetime from datetime import datetime
import pytz import pytz
...@@ -59,18 +60,20 @@ class DailyConversationStatistic(Resource): ...@@ -59,18 +60,20 @@ class DailyConversationStatistic(Resource):
arg_dict['end'] = end_datetime_utc arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date' sql_query += ' GROUP BY date order by date'
rs = db.session.execute(sql_query, arg_dict)
response_date = [] with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs: for i in rs:
response_date.append({ response_data.append({
'date': str(i.date), 'date': str(i.date),
'conversation_count': i.conversation_count 'conversation_count': i.conversation_count
}) })
return jsonify({ return jsonify({
'data': response_date 'data': response_data
}) })
...@@ -119,18 +122,20 @@ class DailyTerminalsStatistic(Resource): ...@@ -119,18 +122,20 @@ class DailyTerminalsStatistic(Resource):
arg_dict['end'] = end_datetime_utc arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date' sql_query += ' GROUP BY date order by date'
rs = db.session.execute(sql_query, arg_dict)
response_date = [] with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs: for i in rs:
response_date.append({ response_data.append({
'date': str(i.date), 'date': str(i.date),
'terminal_count': i.terminal_count 'terminal_count': i.terminal_count
}) })
return jsonify({ return jsonify({
'data': response_date 'data': response_data
}) })
...@@ -180,12 +185,14 @@ class DailyTokenCostStatistic(Resource): ...@@ -180,12 +185,14 @@ class DailyTokenCostStatistic(Resource):
arg_dict['end'] = end_datetime_utc arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date' sql_query += ' GROUP BY date order by date'
rs = db.session.execute(sql_query, arg_dict)
response_date = [] with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs: for i in rs:
response_date.append({ response_data.append({
'date': str(i.date), 'date': str(i.date),
'token_count': i.token_count, 'token_count': i.token_count,
'total_price': i.total_price, 'total_price': i.total_price,
...@@ -193,10 +200,207 @@ class DailyTokenCostStatistic(Resource): ...@@ -193,10 +200,207 @@ class DailyTokenCostStatistic(Resource):
}) })
return jsonify({ return jsonify({
'data': response_date 'data': response_data
})
class AverageSessionInteractionStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, app_id):
account = current_user
app_id = str(app_id)
app_model = _get_app(app_id, 'chat')
parser = reqparse.RequestParser()
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
args = parser.parse_args()
sql_query = """SELECT date(DATE_TRUNC('day', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
AVG(subquery.message_count) AS interactions
FROM (SELECT m.conversation_id, COUNT(m.id) AS message_count
FROM conversations c
JOIN messages m ON c.id = m.conversation_id
WHERE c.override_model_configs IS NULL AND c.app_id = :app_id"""
arg_dict = {'tz': account.timezone, 'app_id': app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args['start']:
start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M')
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and c.created_at >= :start'
arg_dict['start'] = start_datetime_utc
if args['end']:
end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M')
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and c.created_at < :end'
arg_dict['end'] = end_datetime_utc
sql_query += """
GROUP BY m.conversation_id) subquery
LEFT JOIN conversations c on c.id=subquery.conversation_id
GROUP BY date
ORDER BY date"""
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_data.append({
'date': str(i.date),
'interactions': float(i.interactions.quantize(Decimal('0.01')))
})
return jsonify({
'data': response_data
})
class UserSatisfactionRateStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, app_id):
account = current_user
app_id = str(app_id)
app_model = _get_app(app_id)
parser = reqparse.RequestParser()
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
args = parser.parse_args()
sql_query = '''
SELECT date(DATE_TRUNC('day', m.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
COUNT(m.id) as message_count, COUNT(mf.id) as feedback_count
FROM messages m
LEFT JOIN message_feedbacks mf on mf.message_id=m.id
WHERE m.app_id = :app_id
'''
arg_dict = {'tz': account.timezone, 'app_id': app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args['start']:
start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M')
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and m.created_at >= :start'
arg_dict['start'] = start_datetime_utc
if args['end']:
end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M')
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and m.created_at < :end'
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_data.append({
'date': str(i.date),
'rate': round((i.feedback_count * 1000 / i.message_count) if i.message_count > 0 else 0, 2),
})
return jsonify({
'data': response_data
})
class AverageResponseTimeStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, app_id):
account = current_user
app_id = str(app_id)
app_model = _get_app(app_id, 'completion')
parser = reqparse.RequestParser()
parser.add_argument('start', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
parser.add_argument('end', type=datetime_string('%Y-%m-%d %H:%M'), location='args')
args = parser.parse_args()
sql_query = '''
SELECT date(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
AVG(provider_response_latency) as latency
FROM messages
WHERE app_id = :app_id
'''
arg_dict = {'tz': account.timezone, 'app_id': app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args['start']:
start_datetime = datetime.strptime(args['start'], '%Y-%m-%d %H:%M')
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and created_at >= :start'
arg_dict['start'] = start_datetime_utc
if args['end']:
end_datetime = datetime.strptime(args['end'], '%Y-%m-%d %H:%M')
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += ' and created_at < :end'
arg_dict['end'] = end_datetime_utc
sql_query += ' GROUP BY date order by date'
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
response_data = []
for i in rs:
response_data.append({
'date': str(i.date),
'latency': round(i.latency * 1000, 4)
})
return jsonify({
'data': response_data
}) })
api.add_resource(DailyConversationStatistic, '/apps/<uuid:app_id>/statistics/daily-conversations') api.add_resource(DailyConversationStatistic, '/apps/<uuid:app_id>/statistics/daily-conversations')
api.add_resource(DailyTerminalsStatistic, '/apps/<uuid:app_id>/statistics/daily-end-users') api.add_resource(DailyTerminalsStatistic, '/apps/<uuid:app_id>/statistics/daily-end-users')
api.add_resource(DailyTokenCostStatistic, '/apps/<uuid:app_id>/statistics/token-costs') api.add_resource(DailyTokenCostStatistic, '/apps/<uuid:app_id>/statistics/token-costs')
api.add_resource(AverageSessionInteractionStatistic, '/apps/<uuid:app_id>/statistics/average-session-interactions')
api.add_resource(UserSatisfactionRateStatistic, '/apps/<uuid:app_id>/statistics/user-satisfaction-rate')
api.add_resource(AverageResponseTimeStatistic, '/apps/<uuid:app_id>/statistics/average-response-time')
# -*- coding:utf-8 -*-
import json
import logging
from typing import Generator, Union
from flask import Response, stream_with_context
from flask_login import current_user
from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
from controllers.console import api
from controllers.console.app.error import ConversationCompletedError, AppUnavailableError, ProviderNotInitializeError, \
ProviderQuotaExceededError, ProviderModelCurrentlyNotSupportError, CompletionRequestError
from controllers.console.explore.error import NotCompletionAppError, NotChatAppError
from controllers.console.explore.wraps import InstalledAppResource
from core.conversation_message_task import PubHandler
from core.llm.error import LLMBadRequestError, LLMAPIUnavailableError, LLMAuthorizationError, LLMAPIConnectionError, \
LLMRateLimitError, ProviderTokenNotInitError, QuotaExceededError, ModelCurrentlyNotSupportError
from libs.helper import uuid_value
from services.completion_service import CompletionService
# define completion api for user
class CompletionApi(InstalledAppResource):
def post(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
parser = reqparse.RequestParser()
parser.add_argument('inputs', type=dict, required=True, location='json')
parser.add_argument('query', type=str, location='json')
parser.add_argument('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
try:
response = CompletionService.completion(
app_model=app_model,
user=current_user,
args=args,
from_source='console',
streaming=streaming
)
return compact_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
raise InternalServerError()
class CompletionStopApi(InstalledAppResource):
def post(self, installed_app, task_id):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
PubHandler.stop(current_user, task_id)
return {'result': 'success'}, 200
class ChatApi(InstalledAppResource):
def post(self, installed_app):
app_model = installed_app.app
if app_model.mode != '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('response_mode', type=str, choices=['blocking', 'streaming'], location='json')
parser.add_argument('conversation_id', type=uuid_value, location='json')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
try:
response = CompletionService.completion(
app_model=app_model,
user=current_user,
args=args,
from_source='console',
streaming=streaming
)
return compact_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except ValueError as e:
raise e
except Exception as e:
logging.exception("internal server error.")
raise InternalServerError()
class ChatStopApi(InstalledAppResource):
def post(self, installed_app, task_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
PubHandler.stop(current_user, task_id)
return {'result': 'success'}, 200
def compact_response(response: Union[dict | Generator]) -> Response:
if isinstance(response, dict):
return Response(response=json.dumps(response), status=200, mimetype='application/json')
else:
def generate() -> Generator:
try:
for chunk in response:
yield chunk
except services.errors.conversation.ConversationNotExistsError:
yield "data: " + json.dumps(api.handle_error(NotFound("Conversation Not Exists.")).get_json()) + "\n\n"
except services.errors.conversation.ConversationCompletedError:
yield "data: " + json.dumps(api.handle_error(ConversationCompletedError()).get_json()) + "\n\n"
except services.errors.app_model_config.AppModelConfigBrokenError:
logging.exception("App model config broken.")
yield "data: " + json.dumps(api.handle_error(AppUnavailableError()).get_json()) + "\n\n"
except ProviderTokenNotInitError:
yield "data: " + json.dumps(api.handle_error(ProviderNotInitializeError()).get_json()) + "\n\n"
except QuotaExceededError:
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
except ModelCurrentlyNotSupportError:
yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
except ValueError as e:
yield "data: " + json.dumps(api.handle_error(e).get_json()) + "\n\n"
except Exception:
logging.exception("internal server error.")
yield "data: " + json.dumps(api.handle_error(InternalServerError()).get_json()) + "\n\n"
return Response(stream_with_context(generate()), status=200,
mimetype='text/event-stream')
api.add_resource(CompletionApi, '/installed-apps/<uuid:installed_app_id>/completion-messages', endpoint='installed_app_completion')
api.add_resource(CompletionStopApi, '/installed-apps/<uuid:installed_app_id>/completion-messages/<string:task_id>/stop', endpoint='installed_app_stop_completion')
api.add_resource(ChatApi, '/installed-apps/<uuid:installed_app_id>/chat-messages', endpoint='installed_app_chat_completion')
api.add_resource(ChatStopApi, '/installed-apps/<uuid:installed_app_id>/chat-messages/<string:task_id>/stop', endpoint='installed_app_stop_chat_completion')
# -*- coding:utf-8 -*-
from flask_login import current_user
from flask_restful import fields, reqparse, marshal_with
from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.explore.error import NotChatAppError
from controllers.console.explore.wraps import InstalledAppResource
from libs.helper import TimestampField, uuid_value
from services.conversation_service import ConversationService
from services.errors.conversation import LastConversationNotExistsError, ConversationNotExistsError
from services.web_conversation_service import WebConversationService
conversation_fields = {
'id': fields.String,
'name': fields.String,
'inputs': fields.Raw,
'status': fields.String,
'introduction': fields.String,
'created_at': TimestampField
}
conversation_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
'data': fields.List(fields.Nested(conversation_fields))
}
class ConversationListApi(InstalledAppResource):
@marshal_with(conversation_infinite_scroll_pagination_fields)
def get(self, installed_app):
app_model = installed_app.app
if app_model.mode != '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('pinned', type=str, choices=['true', 'false', None], location='args')
args = parser.parse_args()
pinned = None
if 'pinned' in args and args['pinned'] is not None:
pinned = True if args['pinned'] == 'true' else False
try:
return WebConversationService.pagination_by_last_id(
app_model=app_model,
user=current_user,
last_id=args['last_id'],
limit=args['limit'],
pinned=pinned
)
except LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.")
class ConversationApi(InstalledAppResource):
def delete(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
ConversationService.delete(app_model, conversation_id, current_user)
WebConversationService.unpin(app_model, conversation_id, current_user)
return {"result": "success"}, 204
class ConversationRenameApi(InstalledAppResource):
@marshal_with(conversation_fields)
def post(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
parser = reqparse.RequestParser()
parser.add_argument('name', type=str, required=True, location='json')
args = parser.parse_args()
try:
return ConversationService.rename(app_model, conversation_id, current_user, args['name'])
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
class ConversationPinApi(InstalledAppResource):
def patch(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
try:
WebConversationService.pin(app_model, conversation_id, current_user)
except ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
return {"result": "success"}
class ConversationUnPinApi(InstalledAppResource):
def patch(self, installed_app, c_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
conversation_id = str(c_id)
WebConversationService.unpin(app_model, conversation_id, current_user)
return {"result": "success"}
api.add_resource(ConversationRenameApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/name', endpoint='installed_app_conversation_rename')
api.add_resource(ConversationListApi, '/installed-apps/<uuid:installed_app_id>/conversations', endpoint='installed_app_conversations')
api.add_resource(ConversationApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>', endpoint='installed_app_conversation')
api.add_resource(ConversationPinApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/pin', endpoint='installed_app_conversation_pin')
api.add_resource(ConversationUnPinApi, '/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/unpin', endpoint='installed_app_conversation_unpin')
# -*- coding:utf-8 -*-
from libs.exception import BaseHTTPException
class NotCompletionAppError(BaseHTTPException):
error_code = 'not_completion_app'
description = "Not Completion App"
code = 400
class NotChatAppError(BaseHTTPException):
error_code = 'not_chat_app'
description = "Not Chat App"
code = 400
class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException):
error_code = 'app_suggested_questions_after_answer_disabled'
description = "Function Suggested questions after answer disabled."
code = 403
...@@ -2,12 +2,16 @@ ...@@ -2,12 +2,16 @@
from datetime import datetime from datetime import datetime
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_restful import Resource, reqparse, fields, marshal_with, abort, inputs from flask_restful import Resource, reqparse, fields, marshal_with, inputs
from sqlalchemy import and_ from sqlalchemy import and_
from werkzeug.exceptions import NotFound, Forbidden, BadRequest
from controllers.console import api from controllers.console import api
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db from extensions.ext_database import db
from models.model import Tenant, App, InstalledApp, RecommendedApp from libs.helper import TimestampField
from models.model import App, InstalledApp, RecommendedApp
from services.account_service import TenantService from services.account_service import TenantService
app_fields = { app_fields = {
...@@ -20,42 +24,25 @@ app_fields = { ...@@ -20,42 +24,25 @@ app_fields = {
installed_app_fields = { installed_app_fields = {
'id': fields.String, 'id': fields.String,
'app': fields.Nested(app_fields, attribute='app'), 'app': fields.Nested(app_fields),
'app_owner_tenant_id': fields.String, 'app_owner_tenant_id': fields.String,
'is_pinned': fields.Boolean, 'is_pinned': fields.Boolean,
'last_used_at': fields.DateTime, 'last_used_at': TimestampField,
'editable': fields.Boolean 'editable': fields.Boolean,
'uninstallable': fields.Boolean,
} }
installed_app_list_fields = { installed_app_list_fields = {
'installed_apps': fields.List(fields.Nested(installed_app_fields)) 'installed_apps': fields.List(fields.Nested(installed_app_fields))
} }
recommended_app_fields = {
'app': fields.Nested(app_fields, attribute='app'),
'app_id': fields.String,
'description': fields.String(attribute='description'),
'copyright': fields.String,
'privacy_policy': fields.String,
'category': fields.String,
'position': fields.Integer,
'is_listed': fields.Boolean,
'install_count': fields.Integer,
'installed': fields.Boolean,
'editable': fields.Boolean
}
recommended_app_list_fields = {
'recommended_apps': fields.List(fields.Nested(recommended_app_fields)),
'categories': fields.List(fields.String)
}
class InstalledAppsListApi(Resource):
class InstalledAppsListResource(Resource):
@login_required @login_required
@account_initialization_required
@marshal_with(installed_app_list_fields) @marshal_with(installed_app_list_fields)
def get(self): def get(self):
current_tenant_id = Tenant.query.first().id current_tenant_id = current_user.current_tenant_id
installed_apps = db.session.query(InstalledApp).filter( installed_apps = db.session.query(InstalledApp).filter(
InstalledApp.tenant_id == current_tenant_id InstalledApp.tenant_id == current_tenant_id
).all() ).all()
...@@ -63,30 +50,42 @@ class InstalledAppsListResource(Resource): ...@@ -63,30 +50,42 @@ class InstalledAppsListResource(Resource):
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant) current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
installed_apps = [ installed_apps = [
{ {
**installed_app, 'id': installed_app.id,
'app': installed_app.app,
'app_owner_tenant_id': installed_app.app_owner_tenant_id,
'is_pinned': installed_app.is_pinned,
'last_used_at': installed_app.last_used_at,
"editable": current_user.role in ["owner", "admin"], "editable": current_user.role in ["owner", "admin"],
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id
} }
for installed_app in installed_apps for installed_app in installed_apps
] ]
installed_apps.sort(key=lambda app: (-app.is_pinned, app.last_used_at)) installed_apps.sort(key=lambda app: (-app['is_pinned'], app['last_used_at']
if app['last_used_at'] is not None else datetime.min))
return {'installed_apps': installed_apps} return {'installed_apps': installed_apps}
@login_required @login_required
@account_initialization_required
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('app_id', type=str, required=True, help='Invalid app_id') parser.add_argument('app_id', type=str, required=True, help='Invalid app_id')
args = parser.parse_args() args = parser.parse_args()
current_tenant_id = Tenant.query.first().id
app = App.query.get(args['app_id'])
if app is None:
abort(404, message='App not found')
recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args['app_id']).first() recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args['app_id']).first()
if recommended_app is None: if recommended_app is None:
abort(404, message='App not found') raise NotFound('App not found')
current_tenant_id = current_user.current_tenant_id
app = db.session.query(App).filter(
App.id == args['app_id']
).first()
if app is None:
raise NotFound('App not found')
if not app.is_public: if not app.is_public:
abort(403, message="You can't install a non-public app") raise Forbidden('You can\'t install a non-public app')
installed_app = InstalledApp.query.filter(and_( installed_app = InstalledApp.query.filter(and_(
InstalledApp.app_id == args['app_id'], InstalledApp.app_id == args['app_id'],
...@@ -100,6 +99,7 @@ class InstalledAppsListResource(Resource): ...@@ -100,6 +99,7 @@ class InstalledAppsListResource(Resource):
new_installed_app = InstalledApp( new_installed_app = InstalledApp(
app_id=args['app_id'], app_id=args['app_id'],
tenant_id=current_tenant_id, tenant_id=current_tenant_id,
app_owner_tenant_id=app.tenant_id,
is_pinned=False, is_pinned=False,
last_used_at=datetime.utcnow() last_used_at=datetime.utcnow()
) )
...@@ -109,42 +109,25 @@ class InstalledAppsListResource(Resource): ...@@ -109,42 +109,25 @@ class InstalledAppsListResource(Resource):
return {'message': 'App installed successfully'} return {'message': 'App installed successfully'}
class InstalledAppResource(Resource): class InstalledAppApi(InstalledAppResource):
"""
@login_required update and delete an installed app
def delete(self, installed_app_id): use InstalledAppResource to apply default decorators and get installed_app
"""
installed_app = InstalledApp.query.filter(and_( def delete(self, installed_app):
InstalledApp.id == str(installed_app_id),
InstalledApp.tenant_id == current_user.current_tenant_id
)).first()
if installed_app is None:
abort(404, message='App not found')
if installed_app.app_owner_tenant_id == current_user.current_tenant_id: if installed_app.app_owner_tenant_id == current_user.current_tenant_id:
abort(400, message="You can't uninstall an app owned by the current tenant") raise BadRequest('You can\'t uninstall an app owned by the current tenant')
db.session.delete(installed_app) db.session.delete(installed_app)
db.session.commit() db.session.commit()
return {'result': 'success', 'message': 'App uninstalled successfully'} return {'result': 'success', 'message': 'App uninstalled successfully'}
@login_required def patch(self, installed_app):
def patch(self, installed_app_id):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument('is_pinned', type=inputs.boolean) parser.add_argument('is_pinned', type=inputs.boolean)
args = parser.parse_args() args = parser.parse_args()
current_tenant_id = Tenant.query.first().id
installed_app = InstalledApp.query.filter(and_(
InstalledApp.id == str(installed_app_id),
InstalledApp.tenant_id == current_tenant_id
)).first()
if installed_app is None:
abort(404, message='Installed app not found')
commit_args = False commit_args = False
if 'is_pinned' in args: if 'is_pinned' in args:
installed_app.is_pinned = args['is_pinned'] installed_app.is_pinned = args['is_pinned']
...@@ -156,54 +139,5 @@ class InstalledAppResource(Resource): ...@@ -156,54 +139,5 @@ class InstalledAppResource(Resource):
return {'result': 'success', 'message': 'App info updated successfully'} return {'result': 'success', 'message': 'App info updated successfully'}
class RecommendedAppsResource(Resource): api.add_resource(InstalledAppsListApi, '/installed-apps')
@login_required api.add_resource(InstalledAppApi, '/installed-apps/<uuid:installed_app_id>')
@marshal_with(recommended_app_list_fields)
def get(self):
recommended_apps = db.session.query(RecommendedApp).filter(
RecommendedApp.is_listed == True
).all()
categories = set()
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
recommended_apps_result = []
for recommended_app in recommended_apps:
installed = db.session.query(InstalledApp).filter(
and_(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id == current_user.current_tenant_id
)
).first() is not None
language_prefix = current_user.interface_language.split('-')[0]
desc = None
if recommended_app.description:
if language_prefix in recommended_app.description:
desc = recommended_app.description[language_prefix]
elif 'en' in recommended_app.description:
desc = recommended_app.description['en']
recommended_app_result = {
'id': recommended_app.id,
'app': recommended_app.app,
'app_id': recommended_app.app_id,
'description': desc,
'copyright': recommended_app.copyright,
'privacy_policy': recommended_app.privacy_policy,
'category': recommended_app.category,
'position': recommended_app.position,
'is_listed': recommended_app.is_listed,
'install_count': recommended_app.install_count,
'installed': installed,
'editable': current_user.role in ['owner', 'admin'],
}
recommended_apps_result.append(recommended_app_result)
categories.add(recommended_app.category) # add category to categories
return {'recommended_apps': recommended_apps_result, 'categories': list(categories)}
api.add_resource(InstalledAppsListResource, '/installed-apps')
api.add_resource(InstalledAppResource, '/installed-apps/<uuid:installed_app_id>')
api.add_resource(RecommendedAppsResource, '/explore/apps')
# -*- coding:utf-8 -*-
import json
import logging
from typing import Generator, Union
from flask import stream_with_context, Response
from flask_login import current_user
from flask_restful import reqparse, fields, marshal_with
from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound, InternalServerError
import services
from controllers.console import api
from controllers.console.app.error import AppMoreLikeThisDisabledError, ProviderNotInitializeError, \
ProviderQuotaExceededError, ProviderModelCurrentlyNotSupportError, CompletionRequestError
from controllers.console.explore.error import NotCompletionAppError, AppSuggestedQuestionsAfterAnswerDisabledError
from controllers.console.explore.wraps import InstalledAppResource
from core.llm.error import LLMRateLimitError, LLMBadRequestError, LLMAuthorizationError, LLMAPIConnectionError, \
ProviderTokenNotInitError, LLMAPIUnavailableError, QuotaExceededError, ModelCurrentlyNotSupportError
from libs.helper import uuid_value, TimestampField
from services.completion_service import CompletionService
from services.errors.app import MoreLikeThisDisabledError
from services.errors.conversation import ConversationNotExistsError
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
from services.message_service import MessageService
class MessageListApi(InstalledAppResource):
feedback_fields = {
'rating': fields.String
}
message_fields = {
'id': fields.String,
'conversation_id': fields.String,
'inputs': fields.Raw,
'query': fields.String,
'answer': fields.String,
'feedback': fields.Nested(feedback_fields, attribute='user_feedback', allow_null=True),
'created_at': TimestampField
}
message_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
'data': fields.List(fields.Nested(message_fields))
}
@marshal_with(message_infinite_scroll_pagination_fields)
def get(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotChatAppError()
parser = reqparse.RequestParser()
parser.add_argument('conversation_id', required=True, type=uuid_value, location='args')
parser.add_argument('first_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()
try:
return MessageService.pagination_by_first_id(app_model, current_user,
args['conversation_id'], args['first_id'], args['limit'])
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.message.FirstMessageNotExistsError:
raise NotFound("First Message Not Exists.")
class MessageFeedbackApi(InstalledAppResource):
def post(self, installed_app, message_id):
app_model = installed_app.app
message_id = str(message_id)
parser = reqparse.RequestParser()
parser.add_argument('rating', type=str, choices=['like', 'dislike', None], location='json')
args = parser.parse_args()
try:
MessageService.create_feedback(app_model, message_id, current_user, args['rating'])
except services.errors.message.MessageNotExistsError:
raise NotFound("Message Not Exists.")
return {'result': 'success'}
class MessageMoreLikeThisApi(InstalledAppResource):
def get(self, installed_app, message_id):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
message_id = str(message_id)
parser = reqparse.RequestParser()
parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'], location='args')
args = parser.parse_args()
streaming = args['response_mode'] == 'streaming'
try:
response = CompletionService.generate_more_like_this(app_model, current_user, message_id, streaming)
return compact_response(response)
except MessageNotExistsError:
raise NotFound("Message Not Exists.")
except MoreLikeThisDisabledError:
raise AppMoreLikeThisDisabledError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except ValueError as e:
raise e
except Exception:
logging.exception("internal server error.")
raise InternalServerError()
def compact_response(response: Union[dict | Generator]) -> Response:
if isinstance(response, dict):
return Response(response=json.dumps(response), status=200, mimetype='application/json')
else:
def generate() -> Generator:
try:
for chunk in response:
yield chunk
except MessageNotExistsError:
yield "data: " + json.dumps(api.handle_error(NotFound("Message Not Exists.")).get_json()) + "\n\n"
except MoreLikeThisDisabledError:
yield "data: " + json.dumps(api.handle_error(AppMoreLikeThisDisabledError()).get_json()) + "\n\n"
except ProviderTokenNotInitError:
yield "data: " + json.dumps(api.handle_error(ProviderNotInitializeError()).get_json()) + "\n\n"
except QuotaExceededError:
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
except ModelCurrentlyNotSupportError:
yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
except ValueError as e:
yield "data: " + json.dumps(api.handle_error(e).get_json()) + "\n\n"
except Exception:
logging.exception("internal server error.")
yield "data: " + json.dumps(api.handle_error(InternalServerError()).get_json()) + "\n\n"
return Response(stream_with_context(generate()), status=200,
mimetype='text/event-stream')
class MessageSuggestedQuestionApi(InstalledAppResource):
def get(self, installed_app, message_id):
app_model = installed_app.app
if app_model.mode != 'chat':
raise NotCompletionAppError()
message_id = str(message_id)
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model,
user=current_user,
message_id=message_id
)
except MessageNotExistsError:
raise NotFound("Message not found")
except ConversationNotExistsError:
raise NotFound("Conversation not found")
except SuggestedQuestionsAfterAnswerDisabledError:
raise AppSuggestedQuestionsAfterAnswerDisabledError()
except ProviderTokenNotInitError:
raise ProviderNotInitializeError()
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
LLMRateLimitError, LLMAuthorizationError) as e:
raise CompletionRequestError(str(e))
except Exception:
logging.exception("internal server error.")
raise InternalServerError()
return {'data': questions}
api.add_resource(MessageListApi, '/installed-apps/<uuid:installed_app_id>/messages', endpoint='installed_app_messages')
api.add_resource(MessageFeedbackApi, '/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/feedbacks', endpoint='installed_app_message_feedback')
api.add_resource(MessageMoreLikeThisApi, '/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/more-like-this', endpoint='installed_app_more_like_this')
api.add_resource(MessageSuggestedQuestionApi, '/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/suggested-questions', endpoint='installed_app_suggested_question')
# -*- coding:utf-8 -*-
from flask_restful import marshal_with, fields
from controllers.console import api
from controllers.console.explore.wraps import InstalledAppResource
class AppParameterApi(InstalledAppResource):
"""Resource for app variables."""
variable_fields = {
'key': fields.String,
'name': fields.String,
'description': fields.String,
'type': fields.String,
'default': fields.String,
'max_length': fields.Integer,
'options': fields.List(fields.String)
}
parameters_fields = {
'opening_statement': fields.String,
'suggested_questions': fields.Raw,
'suggested_questions_after_answer': fields.Raw,
'more_like_this': fields.Raw,
'user_input_form': fields.Raw,
}
@marshal_with(parameters_fields)
def get(self, installed_app):
"""Retrieve app parameters."""
app_model = installed_app.app
app_model_config = app_model.app_model_config
return {
'opening_statement': app_model_config.opening_statement,
'suggested_questions': app_model_config.suggested_questions_list,
'suggested_questions_after_answer': app_model_config.suggested_questions_after_answer_dict,
'more_like_this': app_model_config.more_like_this_dict,
'user_input_form': app_model_config.user_input_form_list
}
api.add_resource(AppParameterApi, '/installed-apps/<uuid:installed_app_id>/parameters', endpoint='installed_app_parameters')
# -*- coding:utf-8 -*-
from flask_login import login_required, current_user
from flask_restful import Resource, fields, marshal_with
from sqlalchemy import and_
from controllers.console import api
from controllers.console.app.error import AppNotFoundError
from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db
from models.model import App, InstalledApp, RecommendedApp
from services.account_service import TenantService
app_fields = {
'id': fields.String,
'name': fields.String,
'mode': fields.String,
'icon': fields.String,
'icon_background': fields.String
}
recommended_app_fields = {
'app': fields.Nested(app_fields, attribute='app'),
'app_id': fields.String,
'description': fields.String(attribute='description'),
'copyright': fields.String,
'privacy_policy': fields.String,
'category': fields.String,
'position': fields.Integer,
'is_listed': fields.Boolean,
'install_count': fields.Integer,
'installed': fields.Boolean,
'editable': fields.Boolean
}
recommended_app_list_fields = {
'recommended_apps': fields.List(fields.Nested(recommended_app_fields)),
'categories': fields.List(fields.String)
}
class RecommendedAppListApi(Resource):
@login_required
@account_initialization_required
@marshal_with(recommended_app_list_fields)
def get(self):
language_prefix = current_user.interface_language if current_user.interface_language else 'en-US'
recommended_apps = db.session.query(RecommendedApp).filter(
RecommendedApp.is_listed == True,
RecommendedApp.language == language_prefix
).all()
categories = set()
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
recommended_apps_result = []
for recommended_app in recommended_apps:
installed = db.session.query(InstalledApp).filter(
and_(
InstalledApp.app_id == recommended_app.app_id,
InstalledApp.tenant_id == current_user.current_tenant_id
)
).first() is not None
app = recommended_app.app
if not app or not app.is_public:
continue
site = app.site
if not site:
continue
recommended_app_result = {
'id': recommended_app.id,
'app': app,
'app_id': recommended_app.app_id,
'description': site.description,
'copyright': site.copyright,
'privacy_policy': site.privacy_policy,
'category': recommended_app.category,
'position': recommended_app.position,
'is_listed': recommended_app.is_listed,
'install_count': recommended_app.install_count,
'installed': installed,
'editable': current_user.role in ['owner', 'admin'],
}
recommended_apps_result.append(recommended_app_result)
categories.add(recommended_app.category) # add category to categories
return {'recommended_apps': recommended_apps_result, 'categories': list(categories)}
class RecommendedAppApi(Resource):
model_config_fields = {
'opening_statement': fields.String,
'suggested_questions': fields.Raw(attribute='suggested_questions_list'),
'suggested_questions_after_answer': fields.Raw(attribute='suggested_questions_after_answer_dict'),
'more_like_this': fields.Raw(attribute='more_like_this_dict'),
'model': fields.Raw(attribute='model_dict'),
'user_input_form': fields.Raw(attribute='user_input_form_list'),
'pre_prompt': fields.String,
'agent_mode': fields.Raw(attribute='agent_mode_dict'),
}
app_simple_detail_fields = {
'id': fields.String,
'name': fields.String,
'icon': fields.String,
'icon_background': fields.String,
'mode': fields.String,
'app_model_config': fields.Nested(model_config_fields),
}
@login_required
@account_initialization_required
@marshal_with(app_simple_detail_fields)
def get(self, app_id):
app_id = str(app_id)
# is in public recommended list
recommended_app = db.session.query(RecommendedApp).filter(
RecommendedApp.is_listed == True,
RecommendedApp.app_id == app_id
).first()
if not recommended_app:
raise AppNotFoundError
# get app detail
app = db.session.query(App).filter(App.id == app_id).first()
if not app or not app.is_public:
raise AppNotFoundError
return app
api.add_resource(RecommendedAppListApi, '/explore/apps')
api.add_resource(RecommendedAppApi, '/explore/apps/<uuid:app_id>')
from flask_login import current_user
from flask_restful import reqparse, marshal_with, fields
from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.explore.error import NotCompletionAppError
from controllers.console.explore.wraps import InstalledAppResource
from libs.helper import uuid_value, TimestampField
from services.errors.message import MessageNotExistsError
from services.saved_message_service import SavedMessageService
feedback_fields = {
'rating': fields.String
}
message_fields = {
'id': fields.String,
'inputs': fields.Raw,
'query': fields.String,
'answer': fields.String,
'feedback': fields.Nested(feedback_fields, attribute='user_feedback', allow_null=True),
'created_at': TimestampField
}
class SavedMessageListApi(InstalledAppResource):
saved_message_infinite_scroll_pagination_fields = {
'limit': fields.Integer,
'has_more': fields.Boolean,
'data': fields.List(fields.Nested(message_fields))
}
@marshal_with(saved_message_infinite_scroll_pagination_fields)
def get(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
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()
return SavedMessageService.pagination_by_last_id(app_model, current_user, args['last_id'], args['limit'])
def post(self, installed_app):
app_model = installed_app.app
if app_model.mode != 'completion':
raise NotCompletionAppError()
parser = reqparse.RequestParser()
parser.add_argument('message_id', type=uuid_value, required=True, location='json')
args = parser.parse_args()
try:
SavedMessageService.save(app_model, current_user, args['message_id'])
except MessageNotExistsError:
raise NotFound("Message Not Exists.")
return {'result': 'success'}
class SavedMessageApi(InstalledAppResource):
def delete(self, installed_app, message_id):
app_model = installed_app.app
message_id = str(message_id)
if app_model.mode != 'completion':
raise NotCompletionAppError()
SavedMessageService.delete(app_model, current_user, message_id)
return {'result': 'success'}
api.add_resource(SavedMessageListApi, '/installed-apps/<uuid:installed_app_id>/saved-messages', endpoint='installed_app_saved_messages')
api.add_resource(SavedMessageApi, '/installed-apps/<uuid:installed_app_id>/saved-messages/<uuid:message_id>', endpoint='installed_app_saved_message')
from flask_login import login_required, current_user
from flask_restful import Resource
from functools import wraps
from werkzeug.exceptions import NotFound
from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db
from models.model import InstalledApp
def installed_app_required(view=None):
def decorator(view):
@wraps(view)
def decorated(*args, **kwargs):
if not kwargs.get('installed_app_id'):
raise ValueError('missing installed_app_id in path parameters')
installed_app_id = kwargs.get('installed_app_id')
installed_app_id = str(installed_app_id)
del kwargs['installed_app_id']
installed_app = db.session.query(InstalledApp).filter(
InstalledApp.id == str(installed_app_id),
InstalledApp.tenant_id == current_user.current_tenant_id
).first()
if installed_app is None:
raise NotFound('Installed app not found')
if not installed_app.app:
db.session.delete(installed_app)
db.session.commit()
raise NotFound('Installed app not found')
return view(installed_app, *args, **kwargs)
return decorated
if view:
return decorator(view)
return decorator
class InstalledAppResource(Resource):
# must be reversed if there are multiple decorators
method_decorators = [installed_app_required, account_initialization_required, login_required]
...@@ -47,7 +47,7 @@ class ConversationListApi(WebApiResource): ...@@ -47,7 +47,7 @@ class ConversationListApi(WebApiResource):
try: try:
return WebConversationService.pagination_by_last_id( return WebConversationService.pagination_by_last_id(
app_model=app_model, app_model=app_model,
end_user=end_user, user=end_user,
last_id=args['last_id'], last_id=args['last_id'],
limit=args['limit'], limit=args['limit'],
pinned=pinned pinned=pinned
......
...@@ -16,7 +16,7 @@ def validate_token(view=None): ...@@ -16,7 +16,7 @@ def validate_token(view=None):
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
site = validate_and_get_site() site = validate_and_get_site()
app_model = db.session.query(App).get(site.app_id) app_model = db.session.query(App).filter(App.id == site.app_id).first()
if not app_model: if not app_model:
raise NotFound() raise NotFound()
...@@ -42,13 +42,16 @@ def validate_and_get_site(): ...@@ -42,13 +42,16 @@ def validate_and_get_site():
""" """
auth_header = request.headers.get('Authorization') auth_header = request.headers.get('Authorization')
if auth_header is None: if auth_header is None:
raise Unauthorized() raise Unauthorized('Authorization header is missing.')
if ' ' not in auth_header:
raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
auth_scheme, auth_token = auth_header.split(None, 1) auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower() auth_scheme = auth_scheme.lower()
if auth_scheme != 'bearer': if auth_scheme != 'bearer':
raise Unauthorized() raise Unauthorized('Invalid Authorization header format. Expected \'Bearer <api-key>\' format.')
site = db.session.query(Site).filter( site = db.session.query(Site).filter(
Site.code == auth_token, Site.code == auth_token,
......
...@@ -34,5 +34,9 @@ class DatasetIndexToolCallbackHandler(IndexToolCallbackHandler): ...@@ -34,5 +34,9 @@ class DatasetIndexToolCallbackHandler(IndexToolCallbackHandler):
db.session.query(DocumentSegment).filter( db.session.query(DocumentSegment).filter(
DocumentSegment.dataset_id == self.dataset_id, DocumentSegment.dataset_id == self.dataset_id,
DocumentSegment.index_node_id == index_node_id DocumentSegment.index_node_id == index_node_id
).update({DocumentSegment.hit_count: DocumentSegment.hit_count + 1}, synchronize_session=False) ).update(
{DocumentSegment.hit_count: DocumentSegment.hit_count + 1},
synchronize_session=False
)
db.session.commit()
"""Base classes for LLM-powered router chains."""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Type, cast, NamedTuple
from langchain.chains.base import Chain
from pydantic import root_validator
from langchain.chains import LLMChain
from langchain.prompts import BasePromptTemplate
from langchain.schema import BaseOutputParser, OutputParserException, BaseLanguageModel
from libs.json_in_md_parser import parse_and_check_json_markdown
class Route(NamedTuple):
destination: Optional[str]
next_inputs: Dict[str, Any]
class LLMRouterChain(Chain):
"""A router chain that uses an LLM chain to perform routing."""
llm_chain: LLMChain
"""LLM chain used to perform routing"""
@root_validator()
def validate_prompt(cls, values: dict) -> dict:
prompt = values["llm_chain"].prompt
if prompt.output_parser is None:
raise ValueError(
"LLMRouterChain requires base llm_chain prompt to have an output"
" parser that converts LLM text output to a dictionary with keys"
" 'destination' and 'next_inputs'. Received a prompt with no output"
" parser."
)
return values
@property
def input_keys(self) -> List[str]:
"""Will be whatever keys the LLM chain prompt expects.
:meta private:
"""
return self.llm_chain.input_keys
def _validate_outputs(self, outputs: Dict[str, Any]) -> None:
super()._validate_outputs(outputs)
if not isinstance(outputs["next_inputs"], dict):
raise ValueError
def _call(
self,
inputs: Dict[str, Any]
) -> Dict[str, Any]:
output = cast(
Dict[str, Any],
self.llm_chain.predict_and_parse(**inputs),
)
return output
@classmethod
def from_llm(
cls, llm: BaseLanguageModel, prompt: BasePromptTemplate, **kwargs: Any
) -> LLMRouterChain:
"""Convenience constructor."""
llm_chain = LLMChain(llm=llm, prompt=prompt)
return cls(llm_chain=llm_chain, **kwargs)
@property
def output_keys(self) -> List[str]:
return ["destination", "next_inputs"]
def route(self, inputs: Dict[str, Any]) -> Route:
result = self(inputs)
return Route(result["destination"], result["next_inputs"])
class RouterOutputParser(BaseOutputParser[Dict[str, str]]):
"""Parser for output of router chain int he multi-prompt chain."""
default_destination: str = "DEFAULT"
next_inputs_type: Type = str
next_inputs_inner_key: str = "input"
def parse(self, text: str) -> Dict[str, Any]:
try:
expected_keys = ["destination", "next_inputs"]
parsed = parse_and_check_json_markdown(text, expected_keys)
if not isinstance(parsed["destination"], str):
raise ValueError("Expected 'destination' to be a string.")
if not isinstance(parsed["next_inputs"], self.next_inputs_type):
raise ValueError(
f"Expected 'next_inputs' to be {self.next_inputs_type}."
)
parsed["next_inputs"] = {self.next_inputs_inner_key: parsed["next_inputs"]}
if (
parsed["destination"].strip().lower()
== self.default_destination.lower()
):
parsed["destination"] = None
else:
parsed["destination"] = parsed["destination"].strip()
return parsed
except Exception as e:
raise OutputParserException(
f"Parsing text\n{text}\n of llm router raised following error:\n{e}"
)
from typing import Optional, List from typing import Optional, List
from langchain.callbacks import SharedCallbackManager from langchain.callbacks import SharedCallbackManager, CallbackManager
from langchain.chains import SequentialChain from langchain.chains import SequentialChain
from langchain.chains.base import Chain from langchain.chains.base import Chain
from langchain.memory.chat_memory import BaseChatMemory from langchain.memory.chat_memory import BaseChatMemory
from core.agent.agent_builder import AgentBuilder
from core.callback_handler.agent_loop_gather_callback_handler import AgentLoopGatherCallbackHandler from core.callback_handler.agent_loop_gather_callback_handler import AgentLoopGatherCallbackHandler
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
from core.callback_handler.main_chain_gather_callback_handler import MainChainGatherCallbackHandler from core.callback_handler.main_chain_gather_callback_handler import MainChainGatherCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
from core.chain.chain_builder import ChainBuilder from core.chain.chain_builder import ChainBuilder
from core.constant import llm_constant from core.chain.multi_dataset_router_chain import MultiDatasetRouterChain
from core.conversation_message_task import ConversationMessageTask from core.conversation_message_task import ConversationMessageTask
from core.tool.dataset_tool_builder import DatasetToolBuilder from extensions.ext_database import db
from models.dataset import Dataset
class MainChainBuilder: class MainChainBuilder:
...@@ -31,8 +31,7 @@ class MainChainBuilder: ...@@ -31,8 +31,7 @@ class MainChainBuilder:
tenant_id=tenant_id, tenant_id=tenant_id,
agent_mode=agent_mode, agent_mode=agent_mode,
memory=memory, memory=memory,
dataset_tool_callback_handler=DatasetToolCallbackHandler(conversation_message_task), conversation_message_task=conversation_message_task
agent_loop_gather_callback_handler=chain_callback_handler.agent_loop_gather_callback_handler
) )
chains += tool_chains chains += tool_chains
...@@ -59,15 +58,15 @@ class MainChainBuilder: ...@@ -59,15 +58,15 @@ class MainChainBuilder:
@classmethod @classmethod
def get_agent_chains(cls, tenant_id: str, agent_mode: dict, memory: Optional[BaseChatMemory], def get_agent_chains(cls, tenant_id: str, agent_mode: dict, memory: Optional[BaseChatMemory],
dataset_tool_callback_handler: DatasetToolCallbackHandler, conversation_message_task: ConversationMessageTask):
agent_loop_gather_callback_handler: AgentLoopGatherCallbackHandler):
# agent mode # agent mode
chains = [] chains = []
if agent_mode and agent_mode.get('enabled'): if agent_mode and agent_mode.get('enabled'):
tools = agent_mode.get('tools', []) tools = agent_mode.get('tools', [])
pre_fixed_chains = [] pre_fixed_chains = []
agent_tools = [] # agent_tools = []
datasets = []
for tool in tools: for tool in tools:
tool_type = list(tool.keys())[0] tool_type = list(tool.keys())[0]
tool_config = list(tool.values())[0] tool_config = list(tool.values())[0]
...@@ -76,34 +75,27 @@ class MainChainBuilder: ...@@ -76,34 +75,27 @@ class MainChainBuilder:
if chain: if chain:
pre_fixed_chains.append(chain) pre_fixed_chains.append(chain)
elif tool_type == "dataset": elif tool_type == "dataset":
dataset_tool = DatasetToolBuilder.build_dataset_tool( # get dataset from dataset id
tenant_id=tenant_id, dataset = db.session.query(Dataset).filter(
dataset_id=tool_config.get("id"), Dataset.tenant_id == tenant_id,
response_mode='no_synthesizer', # "compact" Dataset.id == tool_config.get("id")
callback_handler=dataset_tool_callback_handler ).first()
)
if dataset_tool: if dataset:
agent_tools.append(dataset_tool) datasets.append(dataset)
# add pre-fixed chains # add pre-fixed chains
chains += pre_fixed_chains chains += pre_fixed_chains
if len(agent_tools) == 1: if len(datasets) > 0:
# tool to chain # tool to chain
tool_chain = ChainBuilder.to_tool_chain(tool=agent_tools[0], output_key='tool_output') multi_dataset_router_chain = MultiDatasetRouterChain.from_datasets(
chains.append(tool_chain)
elif len(agent_tools) > 1:
# build agent config
agent_chain = AgentBuilder.to_agent_chain(
tenant_id=tenant_id, tenant_id=tenant_id,
tools=agent_tools, datasets=datasets,
memory=memory, conversation_message_task=conversation_message_task,
dataset_tool_callback_handler=dataset_tool_callback_handler, callback_manager=CallbackManager([DifyStdOutCallbackHandler()])
agent_loop_gather_callback_handler=agent_loop_gather_callback_handler
) )
chains.append(multi_dataset_router_chain)
chains.append(agent_chain)
final_output_key = cls.get_chains_output_key(chains) final_output_key = cls.get_chains_output_key(chains)
......
from typing import Mapping, List, Dict, Any, Optional
from langchain import LLMChain, PromptTemplate, ConversationChain
from langchain.callbacks import CallbackManager
from langchain.chains.base import Chain
from langchain.schema import BaseLanguageModel
from pydantic import Extra
from core.callback_handler.dataset_tool_callback_handler import DatasetToolCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStdOutCallbackHandler
from core.chain.llm_router_chain import LLMRouterChain, RouterOutputParser
from core.conversation_message_task import ConversationMessageTask
from core.llm.llm_builder import LLMBuilder
from core.tool.dataset_tool_builder import DatasetToolBuilder
from core.tool.llama_index_tool import EnhanceLlamaIndexTool
from models.dataset import Dataset
MULTI_PROMPT_ROUTER_TEMPLATE = """
Given a raw text input to a language model select the model prompt best suited for \
the input. You will be given the names of the available prompts and a description of \
what the prompt is best suited for. You may also revise the original input if you \
think that revising it will ultimately lead to a better response from the language \
model.
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like, \
no any other string out of markdown code snippet:
```json
{{{{
"destination": string \\ name of the prompt to use or "DEFAULT"
"next_inputs": string \\ a potentially modified version of the original input
}}}}
```
REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR \
it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input if you don't think any \
modifications are needed.
<< CANDIDATE PROMPTS >>
{destinations}
<< INPUT >>
{{input}}
<< OUTPUT >>
"""
class MultiDatasetRouterChain(Chain):
"""Use a single chain to route an input to one of multiple candidate chains."""
router_chain: LLMRouterChain
"""Chain for deciding a destination chain and the input to it."""
dataset_tools: Mapping[str, EnhanceLlamaIndexTool]
"""Map of name to candidate chains that inputs can be routed to."""
class Config:
"""Configuration for this pydantic object."""
extra = Extra.forbid
arbitrary_types_allowed = True
@property
def input_keys(self) -> List[str]:
"""Will be whatever keys the router chain prompt expects.
:meta private:
"""
return self.router_chain.input_keys
@property
def output_keys(self) -> List[str]:
return ["text"]
@classmethod
def from_datasets(
cls,
tenant_id: str,
datasets: List[Dataset],
conversation_message_task: ConversationMessageTask,
**kwargs: Any,
):
"""Convenience constructor for instantiating from destination prompts."""
llm_callback_manager = CallbackManager([DifyStdOutCallbackHandler()])
llm = LLMBuilder.to_llm(
tenant_id=tenant_id,
model_name='gpt-3.5-turbo',
temperature=0,
max_tokens=1024,
callback_manager=llm_callback_manager
)
destinations = ["{}: {}".format(d.id, d.description.replace('\n', ' ') if d.description
else ('useful for when you want to answer queries about the ' + d.name))
for d in datasets]
destinations_str = "\n".join(destinations)
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(
destinations=destinations_str
)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),
)
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
dataset_tools = {}
for dataset in datasets:
dataset_tool = DatasetToolBuilder.build_dataset_tool(
dataset=dataset,
response_mode='no_synthesizer', # "compact"
callback_handler=DatasetToolCallbackHandler(conversation_message_task)
)
if dataset_tool:
dataset_tools[dataset.id] = dataset_tool
return cls(
router_chain=router_chain,
dataset_tools=dataset_tools,
**kwargs,
)
def _call(
self,
inputs: Dict[str, Any]
) -> Dict[str, Any]:
if len(self.dataset_tools) == 0:
return {"text": ''}
elif len(self.dataset_tools) == 1:
return {"text": next(iter(self.dataset_tools.values())).run(inputs['input'])}
route = self.router_chain.route(inputs)
if not route.destination:
return {"text": ''}
elif route.destination in self.dataset_tools:
return {"text": self.dataset_tools[route.destination].run(
route.next_inputs['input']
)}
else:
raise ValueError(
f"Received invalid destination chain name '{route.destination}'"
)
import logging
from typing import Optional, List, Union, Tuple from typing import Optional, List, Union, Tuple
from langchain.callbacks import CallbackManager from langchain.callbacks import CallbackManager
from langchain.chat_models.base import BaseChatModel from langchain.chat_models.base import BaseChatModel
from langchain.llms import BaseLLM from langchain.llms import BaseLLM
from langchain.schema import BaseMessage, BaseLanguageModel, HumanMessage from langchain.schema import BaseMessage, BaseLanguageModel, HumanMessage
from requests.exceptions import ChunkedEncodingError
from core.constant import llm_constant from core.constant import llm_constant
from core.callback_handler.llm_callback_handler import LLMCallbackHandler from core.callback_handler.llm_callback_handler import LLMCallbackHandler
from core.callback_handler.std_out_callback_handler import DifyStreamingStdOutCallbackHandler, \ from core.callback_handler.std_out_callback_handler import DifyStreamingStdOutCallbackHandler, \
DifyStdOutCallbackHandler DifyStdOutCallbackHandler
from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException from core.conversation_message_task import ConversationMessageTask, ConversationTaskStoppedException, PubHandler
from core.llm.error import LLMBadRequestError from core.llm.error import LLMBadRequestError
from core.llm.llm_builder import LLMBuilder from core.llm.llm_builder import LLMBuilder
from core.chain.main_chain_builder import MainChainBuilder from core.chain.main_chain_builder import MainChainBuilder
...@@ -84,6 +87,11 @@ class Completion: ...@@ -84,6 +87,11 @@ class Completion:
) )
except ConversationTaskStoppedException: except ConversationTaskStoppedException:
return return
except ChunkedEncodingError as e:
# Interrupt by LLM (like OpenAI), handle it.
logging.warning(f'ChunkedEncodingError: {e}')
conversation_message_task.end()
return
@classmethod @classmethod
def run_final_llm(cls, tenant_id: str, mode: str, app_model_config: AppModelConfig, query: str, inputs: dict, def run_final_llm(cls, tenant_id: str, mode: str, app_model_config: AppModelConfig, query: str, inputs: dict,
......
...@@ -80,7 +80,10 @@ class ConversationMessageTask: ...@@ -80,7 +80,10 @@ class ConversationMessageTask:
if introduction: if introduction:
prompt_template = OutLinePromptTemplate.from_template(template=PromptBuilder.process_template(introduction)) prompt_template = OutLinePromptTemplate.from_template(template=PromptBuilder.process_template(introduction))
prompt_inputs = {k: self.inputs[k] for k in prompt_template.input_variables if k in self.inputs} prompt_inputs = {k: self.inputs[k] for k in prompt_template.input_variables if k in self.inputs}
introduction = prompt_template.format(**prompt_inputs) try:
introduction = prompt_template.format(**prompt_inputs)
except KeyError:
pass
if self.app_model_config.pre_prompt: if self.app_model_config.pre_prompt:
pre_prompt = PromptBuilder.process_template(self.app_model_config.pre_prompt) pre_prompt = PromptBuilder.process_template(self.app_model_config.pre_prompt)
...@@ -171,7 +174,7 @@ class ConversationMessageTask: ...@@ -171,7 +174,7 @@ class ConversationMessageTask:
) )
if not by_stopped: if not by_stopped:
self._pub_handler.pub_end() self.end()
def update_provider_quota(self): def update_provider_quota(self):
llm_provider_service = LLMProviderService( llm_provider_service = LLMProviderService(
...@@ -268,6 +271,9 @@ class ConversationMessageTask: ...@@ -268,6 +271,9 @@ class ConversationMessageTask:
total_price = message_tokens_per_1k * message_unit_price + answer_tokens_per_1k * answer_unit_price total_price = message_tokens_per_1k * message_unit_price + answer_tokens_per_1k * answer_unit_price
return total_price.quantize(decimal.Decimal('0.0000001'), rounding=decimal.ROUND_HALF_UP) return total_price.quantize(decimal.Decimal('0.0000001'), rounding=decimal.ROUND_HALF_UP)
def end(self):
self._pub_handler.pub_end()
class PubHandler: class PubHandler:
def __init__(self, user: Union[Account | EndUser], task_id: str, def __init__(self, user: Union[Account | EndUser], task_id: str,
......
...@@ -173,6 +173,13 @@ class OpenAIEmbedding(BaseEmbedding): ...@@ -173,6 +173,13 @@ class OpenAIEmbedding(BaseEmbedding):
Can be overriden for batch queries. Can be overriden for batch queries.
""" """
if self.openai_api_type and self.openai_api_type == 'azure':
embeddings = []
for text in texts:
embeddings.append(self._get_text_embedding(text))
return embeddings
if self.deployment_name is not None: if self.deployment_name is not None:
engine = self.deployment_name engine = self.deployment_name
else: else:
...@@ -187,6 +194,13 @@ class OpenAIEmbedding(BaseEmbedding): ...@@ -187,6 +194,13 @@ class OpenAIEmbedding(BaseEmbedding):
async def _aget_text_embeddings(self, texts: List[str]) -> List[List[float]]: async def _aget_text_embeddings(self, texts: List[str]) -> List[List[float]]:
"""Asynchronously get text embeddings.""" """Asynchronously get text embeddings."""
if self.openai_api_type and self.openai_api_type == 'azure':
embeddings = []
for text in texts:
embeddings.append(await self._aget_text_embedding(text))
return embeddings
if self.deployment_name is not None: if self.deployment_name is not None:
engine = self.deployment_name engine = self.deployment_name
else: else:
......
...@@ -7,6 +7,7 @@ from core.constant import llm_constant ...@@ -7,6 +7,7 @@ from core.constant import llm_constant
from core.llm.llm_builder import LLMBuilder from core.llm.llm_builder import LLMBuilder
from core.llm.streamable_open_ai import StreamableOpenAI from core.llm.streamable_open_ai import StreamableOpenAI
from core.llm.token_calculator import TokenCalculator from core.llm.token_calculator import TokenCalculator
from core.prompt.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser
from core.prompt.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser from core.prompt.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser
from core.prompt.prompt_template import OutLinePromptTemplate from core.prompt.prompt_template import OutLinePromptTemplate
...@@ -118,3 +119,46 @@ class LLMGenerator: ...@@ -118,3 +119,46 @@ class LLMGenerator:
questions = [] questions = []
return questions return questions
@classmethod
def generate_rule_config(cls, tenant_id: str, audiences: str, hoping_to_solve: str) -> dict:
output_parser = RuleConfigGeneratorOutputParser()
prompt = OutLinePromptTemplate(
template=output_parser.get_format_instructions(),
input_variables=["audiences", "hoping_to_solve"],
partial_variables={
"variable": '{variable}',
"lanA": '{lanA}',
"lanB": '{lanB}',
"topic": '{topic}'
},
validate_template=False
)
_input = prompt.format_prompt(audiences=audiences, hoping_to_solve=hoping_to_solve)
llm: StreamableOpenAI = LLMBuilder.to_llm(
tenant_id=tenant_id,
model_name=generate_base_model,
temperature=0,
max_tokens=512
)
if isinstance(llm, BaseChatModel):
query = [HumanMessage(content=_input.to_string())]
else:
query = _input.to_string()
try:
output = llm(query)
rule_config = output_parser.parse(output)
except Exception:
logging.exception("Error generating prompt")
rule_config = {
"prompt": "",
"variables": [],
"opening_statement": ""
}
return rule_config
...@@ -110,6 +110,8 @@ class AzureProvider(BaseProvider): ...@@ -110,6 +110,8 @@ class AzureProvider(BaseProvider):
if missing_model_ids: if missing_model_ids:
raise ValidateFailedError("Please add deployments for '{}'.".format(", ".join(missing_model_ids))) raise ValidateFailedError("Please add deployments for '{}'.".format(", ".join(missing_model_ids)))
except ValidateFailedError as e:
raise e
except AzureAuthenticationError: except AzureAuthenticationError:
raise ValidateFailedError('Validation failed, please check your API Key.') raise ValidateFailedError('Validation failed, please check your API Key.')
except (requests.ConnectionError, requests.RequestException): except (requests.ConnectionError, requests.RequestException):
......
from typing import Any
from langchain.schema import BaseOutputParser, OutputParserException
from core.prompt.prompts import RULE_CONFIG_GENERATE_TEMPLATE
from libs.json_in_md_parser import parse_and_check_json_markdown
class RuleConfigGeneratorOutputParser(BaseOutputParser):
def get_format_instructions(self) -> str:
return RULE_CONFIG_GENERATE_TEMPLATE
def parse(self, text: str) -> Any:
try:
expected_keys = ["prompt", "variables", "opening_statement"]
parsed = parse_and_check_json_markdown(text, expected_keys)
if not isinstance(parsed["prompt"], str):
raise ValueError("Expected 'prompt' to be a string.")
if not isinstance(parsed["variables"], list):
raise ValueError(
f"Expected 'variables' to be a list."
)
if not isinstance(parsed["opening_statement"], str):
raise ValueError(
f"Expected 'opening_statement' to be a str."
)
return parsed
except Exception as e:
raise OutputParserException(
f"Parsing text\n{text}\n of rule config generator raised following error:\n{e}"
)
...@@ -32,6 +32,6 @@ class PromptBuilder: ...@@ -32,6 +32,6 @@ class PromptBuilder:
@classmethod @classmethod
def process_template(cls, template: str): def process_template(cls, template: str):
processed_template = re.sub(r'\{(.+?)\}', r'\1', template) processed_template = re.sub(r'\{([a-zA-Z_]\w+?)\}', r'\1', template)
processed_template = re.sub(r'\{\{(.+?)\}\}', r'{\1}', processed_template) processed_template = re.sub(r'\{\{([a-zA-Z_]\w+?)\}\}', r'{\1}', processed_template)
return processed_template return processed_template
...@@ -61,3 +61,60 @@ QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL = ( ...@@ -61,3 +61,60 @@ QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL = (
QUERY_KEYWORD_EXTRACT_TEMPLATE = QueryKeywordExtractPrompt( QUERY_KEYWORD_EXTRACT_TEMPLATE = QueryKeywordExtractPrompt(
QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL QUERY_KEYWORD_EXTRACT_TEMPLATE_TMPL
) )
RULE_CONFIG_GENERATE_TEMPLATE = """Given MY INTENDED AUDIENCES and HOPING TO SOLVE using a language model, please select \
the model prompt that best suits the input.
You will be provided with the prompt, variables, and an opening statement.
Only the content enclosed in double curly braces, such as {{variable}}, in the prompt can be considered as a variable; \
otherwise, it cannot exist as a variable in the variables.
If you believe revising the original input will result in a better response from the language model, you may \
suggest revisions.
<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like, \
no any other string out of markdown code snippet:
```json
{{{{
"prompt": string \\ generated prompt
"variables": list of string \\ variables
"opening_statement": string \\ an opening statement to guide users on how to ask questions with generated prompt \
and fill in variables, with a welcome sentence, and keep TLDR.
}}}}
```
<< EXAMPLES >>
[EXAMPLE A]
```json
{
"prompt": "Write a letter about love",
"variables": [],
"opening_statement": "Hi! I'm your love letter writer AI."
}
```
[EXAMPLE B]
```json
{
"prompt": "Translate from {{lanA}} to {{lanB}}",
"variables": ["lanA", "lanB"],
"opening_statement": "Welcome to use translate app"
}
```
[EXAMPLE C]
```json
{
"prompt": "Write a story about {{topic}}",
"variables": ["topic"],
"opening_statement": "I'm your story writer"
}
```
<< MY INTENDED AUDIENCES >>
{audiences}
<< HOPING TO SOLVE >>
{hoping_to_solve}
<< OUTPUT >>
"""
\ No newline at end of file
...@@ -10,24 +10,14 @@ from core.index.keyword_table_index import KeywordTableIndex ...@@ -10,24 +10,14 @@ from core.index.keyword_table_index import KeywordTableIndex
from core.index.vector_index import VectorIndex from core.index.vector_index import VectorIndex
from core.prompt.prompts import QUERY_KEYWORD_EXTRACT_TEMPLATE from core.prompt.prompts import QUERY_KEYWORD_EXTRACT_TEMPLATE
from core.tool.llama_index_tool import EnhanceLlamaIndexTool from core.tool.llama_index_tool import EnhanceLlamaIndexTool
from extensions.ext_database import db
from models.dataset import Dataset from models.dataset import Dataset
class DatasetToolBuilder: class DatasetToolBuilder:
@classmethod @classmethod
def build_dataset_tool(cls, tenant_id: str, dataset_id: str, def build_dataset_tool(cls, dataset: Dataset,
response_mode: str = "no_synthesizer", response_mode: str = "no_synthesizer",
callback_handler: Optional[DatasetToolCallbackHandler] = None): callback_handler: Optional[DatasetToolCallbackHandler] = None):
# get dataset from dataset id
dataset = db.session.query(Dataset).filter(
Dataset.tenant_id == tenant_id,
Dataset.id == dataset_id
).first()
if not dataset:
return None
if dataset.indexing_technique == "economy": if dataset.indexing_technique == "economy":
# use keyword table query # use keyword table query
index = KeywordTableIndex(dataset=dataset).query_index index = KeywordTableIndex(dataset=dataset).query_index
...@@ -65,7 +55,7 @@ class DatasetToolBuilder: ...@@ -65,7 +55,7 @@ class DatasetToolBuilder:
index_tool_config = IndexToolConfig( index_tool_config = IndexToolConfig(
index=index, index=index,
name=f"dataset-{dataset_id}", name=f"dataset-{dataset.id}",
description=description, description=description,
index_query_kwargs=query_kwargs, index_query_kwargs=query_kwargs,
tool_kwargs={ tool_kwargs={
...@@ -75,7 +65,7 @@ class DatasetToolBuilder: ...@@ -75,7 +65,7 @@ class DatasetToolBuilder:
# return_direct: Whether to return LLM results directly or process the output data with an Output Parser # return_direct: Whether to return LLM results directly or process the output data with an Output Parser
) )
index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset_id) index_callback_handler = DatasetIndexToolCallbackHandler(dataset_id=dataset.id)
return EnhanceLlamaIndexTool.from_tool_config( return EnhanceLlamaIndexTool.from_tool_config(
tool_config=index_tool_config, tool_config=index_tool_config,
......
import json
from typing import List
from langchain.schema import OutputParserException
def parse_json_markdown(json_string: str) -> dict:
# Remove the triple backticks if present
json_string = json_string.strip()
start_index = json_string.find("```json")
end_index = json_string.find("```", start_index + len("```json"))
if start_index != -1 and end_index != -1:
extracted_content = json_string[start_index + len("```json"):end_index].strip()
# Parse the JSON string into a Python dictionary
parsed = json.loads(extracted_content)
elif json_string.startswith("{"):
# Parse the JSON string into a Python dictionary
parsed = json.loads(json_string)
else:
raise Exception("Could not find JSON block in the output.")
return parsed
def parse_and_check_json_markdown(text: str, expected_keys: List[str]) -> dict:
try:
json_obj = parse_json_markdown(text)
except json.JSONDecodeError as e:
raise OutputParserException(f"Got invalid JSON object. Error: {e}")
for key in expected_keys:
if key not in json_obj:
raise OutputParserException(
f"Got invalid return object. Expected key `{key}` "
f"to be present, but got {json_obj}"
)
return json_obj
"""add created by role
Revision ID: 9f4e3427ea84
Revises: 64b051264f32
Create Date: 2023-05-17 17:29:01.060435
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9f4e3427ea84'
down_revision = '64b051264f32'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('pinned_conversations', schema=None) as batch_op:
batch_op.add_column(sa.Column('created_by_role', sa.String(length=255), server_default=sa.text("'end_user'::character varying"), nullable=False))
batch_op.drop_index('pinned_conversation_conversation_idx')
batch_op.create_index('pinned_conversation_conversation_idx', ['app_id', 'conversation_id', 'created_by_role', 'created_by'], unique=False)
with op.batch_alter_table('saved_messages', schema=None) as batch_op:
batch_op.add_column(sa.Column('created_by_role', sa.String(length=255), server_default=sa.text("'end_user'::character varying"), nullable=False))
batch_op.drop_index('saved_message_message_idx')
batch_op.create_index('saved_message_message_idx', ['app_id', 'message_id', 'created_by_role', 'created_by'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('saved_messages', schema=None) as batch_op:
batch_op.drop_index('saved_message_message_idx')
batch_op.create_index('saved_message_message_idx', ['app_id', 'message_id', 'created_by'], unique=False)
batch_op.drop_column('created_by_role')
with op.batch_alter_table('pinned_conversations', schema=None) as batch_op:
batch_op.drop_index('pinned_conversation_conversation_idx')
batch_op.create_index('pinned_conversation_conversation_idx', ['app_id', 'conversation_id', 'created_by'], unique=False)
batch_op.drop_column('created_by_role')
# ### end Alembic commands ###
"""add language to recommend apps
Revision ID: a45f4dfde53b
Revises: 9f4e3427ea84
Create Date: 2023-05-25 17:50:32.052335
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a45f4dfde53b'
down_revision = '9f4e3427ea84'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('recommended_apps', schema=None) as batch_op:
batch_op.add_column(sa.Column('language', sa.String(length=255), server_default=sa.text("'en-US'::character varying"), nullable=False))
batch_op.drop_index('recommended_app_is_listed_idx')
batch_op.create_index('recommended_app_is_listed_idx', ['is_listed', 'language'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('recommended_apps', schema=None) as batch_op:
batch_op.drop_index('recommended_app_is_listed_idx')
batch_op.create_index('recommended_app_is_listed_idx', ['is_listed'], unique=False)
batch_op.drop_column('language')
# ### end Alembic commands ###
...@@ -123,7 +123,7 @@ class RecommendedApp(db.Model): ...@@ -123,7 +123,7 @@ class RecommendedApp(db.Model):
__table_args__ = ( __table_args__ = (
db.PrimaryKeyConstraint('id', name='recommended_app_pkey'), db.PrimaryKeyConstraint('id', name='recommended_app_pkey'),
db.Index('recommended_app_app_id_idx', 'app_id'), db.Index('recommended_app_app_id_idx', 'app_id'),
db.Index('recommended_app_is_listed_idx', 'is_listed') db.Index('recommended_app_is_listed_idx', 'is_listed', 'language')
) )
id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()')) id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
...@@ -135,6 +135,7 @@ class RecommendedApp(db.Model): ...@@ -135,6 +135,7 @@ class RecommendedApp(db.Model):
position = db.Column(db.Integer, nullable=False, default=0) position = db.Column(db.Integer, nullable=False, default=0)
is_listed = db.Column(db.Boolean, nullable=False, default=True) is_listed = db.Column(db.Boolean, nullable=False, default=True)
install_count = db.Column(db.Integer, nullable=False, default=0) install_count = db.Column(db.Integer, nullable=False, default=0)
language = db.Column(db.String(255), nullable=False, server_default=db.text("'en-US'::character varying"))
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
...@@ -143,17 +144,6 @@ class RecommendedApp(db.Model): ...@@ -143,17 +144,6 @@ class RecommendedApp(db.Model):
app = db.session.query(App).filter(App.id == self.app_id).first() app = db.session.query(App).filter(App.id == self.app_id).first()
return app return app
# def set_description(self, lang, desc):
# if self.description is None:
# self.description = {}
# self.description[lang] = desc
def get_description(self, lang):
if self.description and lang in self.description:
return self.description[lang]
else:
return self.description.get('en')
class InstalledApp(db.Model): class InstalledApp(db.Model):
__tablename__ = 'installed_apps' __tablename__ = 'installed_apps'
...@@ -314,6 +304,10 @@ class Conversation(db.Model): ...@@ -314,6 +304,10 @@ class Conversation(db.Model):
def app(self): def app(self):
return db.session.query(App).filter(App.id == self.app_id).first() return db.session.query(App).filter(App.id == self.app_id).first()
@property
def in_debug_mode(self):
return self.override_model_configs is not None
class Message(db.Model): class Message(db.Model):
__tablename__ = 'messages' __tablename__ = 'messages'
...@@ -380,6 +374,10 @@ class Message(db.Model): ...@@ -380,6 +374,10 @@ class Message(db.Model):
return None return None
@property
def in_debug_mode(self):
return self.override_model_configs is not None
class MessageFeedback(db.Model): class MessageFeedback(db.Model):
__tablename__ = 'message_feedbacks' __tablename__ = 'message_feedbacks'
......
...@@ -8,12 +8,13 @@ class SavedMessage(db.Model): ...@@ -8,12 +8,13 @@ class SavedMessage(db.Model):
__tablename__ = 'saved_messages' __tablename__ = 'saved_messages'
__table_args__ = ( __table_args__ = (
db.PrimaryKeyConstraint('id', name='saved_message_pkey'), db.PrimaryKeyConstraint('id', name='saved_message_pkey'),
db.Index('saved_message_message_idx', 'app_id', 'message_id', 'created_by'), db.Index('saved_message_message_idx', 'app_id', 'message_id', 'created_by_role', 'created_by'),
) )
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()')) id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
app_id = db.Column(UUID, nullable=False) app_id = db.Column(UUID, nullable=False)
message_id = db.Column(UUID, nullable=False) message_id = db.Column(UUID, nullable=False)
created_by_role = db.Column(db.String(255), nullable=False, server_default=db.text("'end_user'::character varying"))
created_by = db.Column(UUID, nullable=False) created_by = db.Column(UUID, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
...@@ -26,11 +27,12 @@ class PinnedConversation(db.Model): ...@@ -26,11 +27,12 @@ class PinnedConversation(db.Model):
__tablename__ = 'pinned_conversations' __tablename__ = 'pinned_conversations'
__table_args__ = ( __table_args__ = (
db.PrimaryKeyConstraint('id', name='pinned_conversation_pkey'), db.PrimaryKeyConstraint('id', name='pinned_conversation_pkey'),
db.Index('pinned_conversation_conversation_idx', 'app_id', 'conversation_id', 'created_by'), db.Index('pinned_conversation_conversation_idx', 'app_id', 'conversation_id', 'created_by_role', 'created_by'),
) )
id = db.Column(UUID, server_default=db.text('uuid_generate_v4()')) id = db.Column(UUID, server_default=db.text('uuid_generate_v4()'))
app_id = db.Column(UUID, nullable=False) app_id = db.Column(UUID, nullable=False)
conversation_id = db.Column(UUID, nullable=False) conversation_id = db.Column(UUID, nullable=False)
created_by_role = db.Column(db.String(255), nullable=False, server_default=db.text("'end_user'::character varying"))
created_by = db.Column(UUID, nullable=False) created_by = db.Column(UUID, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
...@@ -33,6 +33,10 @@ class CompletionService: ...@@ -33,6 +33,10 @@ class CompletionService:
# is streaming mode # is streaming mode
inputs = args['inputs'] inputs = args['inputs']
query = args['query'] query = args['query']
if not query:
raise ValueError('query is required')
conversation_id = args['conversation_id'] if 'conversation_id' in args else None conversation_id = args['conversation_id'] if 'conversation_id' in args else None
conversation = None conversation = None
......
...@@ -127,7 +127,7 @@ class MessageService: ...@@ -127,7 +127,7 @@ class MessageService:
message_id=message_id message_id=message_id
) )
feedback = message.user_feedback feedback = message.user_feedback if isinstance(user, EndUser) else message.admin_feedback
if not rating and feedback: if not rating and feedback:
db.session.delete(feedback) db.session.delete(feedback)
......
from typing import Optional from typing import Optional, Union
from libs.infinite_scroll_pagination import InfiniteScrollPagination from libs.infinite_scroll_pagination import InfiniteScrollPagination
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Account
from models.model import App, EndUser from models.model import App, EndUser
from models.web import SavedMessage from models.web import SavedMessage
from services.message_service import MessageService from services.message_service import MessageService
...@@ -9,27 +10,29 @@ from services.message_service import MessageService ...@@ -9,27 +10,29 @@ from services.message_service import MessageService
class SavedMessageService: class SavedMessageService:
@classmethod @classmethod
def pagination_by_last_id(cls, app_model: App, end_user: Optional[EndUser], def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
last_id: Optional[str], limit: int) -> InfiniteScrollPagination: last_id: Optional[str], limit: int) -> InfiniteScrollPagination:
saved_messages = db.session.query(SavedMessage).filter( saved_messages = db.session.query(SavedMessage).filter(
SavedMessage.app_id == app_model.id, SavedMessage.app_id == app_model.id,
SavedMessage.created_by == end_user.id SavedMessage.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
SavedMessage.created_by == user.id
).order_by(SavedMessage.created_at.desc()).all() ).order_by(SavedMessage.created_at.desc()).all()
message_ids = [sm.message_id for sm in saved_messages] message_ids = [sm.message_id for sm in saved_messages]
return MessageService.pagination_by_last_id( return MessageService.pagination_by_last_id(
app_model=app_model, app_model=app_model,
user=end_user, user=user,
last_id=last_id, last_id=last_id,
limit=limit, limit=limit,
include_ids=message_ids include_ids=message_ids
) )
@classmethod @classmethod
def save(cls, app_model: App, user: Optional[EndUser], message_id: str): def save(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
saved_message = db.session.query(SavedMessage).filter( saved_message = db.session.query(SavedMessage).filter(
SavedMessage.app_id == app_model.id, SavedMessage.app_id == app_model.id,
SavedMessage.message_id == message_id, SavedMessage.message_id == message_id,
SavedMessage.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
SavedMessage.created_by == user.id SavedMessage.created_by == user.id
).first() ).first()
...@@ -45,6 +48,7 @@ class SavedMessageService: ...@@ -45,6 +48,7 @@ class SavedMessageService:
saved_message = SavedMessage( saved_message = SavedMessage(
app_id=app_model.id, app_id=app_model.id,
message_id=message.id, message_id=message.id,
created_by_role='account' if isinstance(user, Account) else 'end_user',
created_by=user.id created_by=user.id
) )
...@@ -52,10 +56,11 @@ class SavedMessageService: ...@@ -52,10 +56,11 @@ class SavedMessageService:
db.session.commit() db.session.commit()
@classmethod @classmethod
def delete(cls, app_model: App, user: Optional[EndUser], message_id: str): def delete(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
saved_message = db.session.query(SavedMessage).filter( saved_message = db.session.query(SavedMessage).filter(
SavedMessage.app_id == app_model.id, SavedMessage.app_id == app_model.id,
SavedMessage.message_id == message_id, SavedMessage.message_id == message_id,
SavedMessage.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
SavedMessage.created_by == user.id SavedMessage.created_by == user.id
).first() ).first()
......
...@@ -2,6 +2,7 @@ from typing import Optional, Union ...@@ -2,6 +2,7 @@ from typing import Optional, Union
from libs.infinite_scroll_pagination import InfiniteScrollPagination from libs.infinite_scroll_pagination import InfiniteScrollPagination
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Account
from models.model import App, EndUser from models.model import App, EndUser
from models.web import PinnedConversation from models.web import PinnedConversation
from services.conversation_service import ConversationService from services.conversation_service import ConversationService
...@@ -9,14 +10,15 @@ from services.conversation_service import ConversationService ...@@ -9,14 +10,15 @@ from services.conversation_service import ConversationService
class WebConversationService: class WebConversationService:
@classmethod @classmethod
def pagination_by_last_id(cls, app_model: App, end_user: Optional[EndUser], def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
last_id: Optional[str], limit: int, pinned: Optional[bool] = None) -> InfiniteScrollPagination: last_id: Optional[str], limit: int, pinned: Optional[bool] = None) -> InfiniteScrollPagination:
include_ids = None include_ids = None
exclude_ids = None exclude_ids = None
if pinned is not None: if pinned is not None:
pinned_conversations = db.session.query(PinnedConversation).filter( pinned_conversations = db.session.query(PinnedConversation).filter(
PinnedConversation.app_id == app_model.id, PinnedConversation.app_id == app_model.id,
PinnedConversation.created_by == end_user.id PinnedConversation.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
PinnedConversation.created_by == user.id
).order_by(PinnedConversation.created_at.desc()).all() ).order_by(PinnedConversation.created_at.desc()).all()
pinned_conversation_ids = [pc.conversation_id for pc in pinned_conversations] pinned_conversation_ids = [pc.conversation_id for pc in pinned_conversations]
if pinned: if pinned:
...@@ -26,7 +28,7 @@ class WebConversationService: ...@@ -26,7 +28,7 @@ class WebConversationService:
return ConversationService.pagination_by_last_id( return ConversationService.pagination_by_last_id(
app_model=app_model, app_model=app_model,
user=end_user, user=user,
last_id=last_id, last_id=last_id,
limit=limit, limit=limit,
include_ids=include_ids, include_ids=include_ids,
...@@ -34,10 +36,11 @@ class WebConversationService: ...@@ -34,10 +36,11 @@ class WebConversationService:
) )
@classmethod @classmethod
def pin(cls, app_model: App, conversation_id: str, user: Optional[EndUser]): def pin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
pinned_conversation = db.session.query(PinnedConversation).filter( pinned_conversation = db.session.query(PinnedConversation).filter(
PinnedConversation.app_id == app_model.id, PinnedConversation.app_id == app_model.id,
PinnedConversation.conversation_id == conversation_id, PinnedConversation.conversation_id == conversation_id,
PinnedConversation.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
PinnedConversation.created_by == user.id PinnedConversation.created_by == user.id
).first() ).first()
...@@ -53,6 +56,7 @@ class WebConversationService: ...@@ -53,6 +56,7 @@ class WebConversationService:
pinned_conversation = PinnedConversation( pinned_conversation = PinnedConversation(
app_id=app_model.id, app_id=app_model.id,
conversation_id=conversation.id, conversation_id=conversation.id,
created_by_role='account' if isinstance(user, Account) else 'end_user',
created_by=user.id created_by=user.id
) )
...@@ -60,10 +64,11 @@ class WebConversationService: ...@@ -60,10 +64,11 @@ class WebConversationService:
db.session.commit() db.session.commit()
@classmethod @classmethod
def unpin(cls, app_model: App, conversation_id: str, user: Optional[EndUser]): def unpin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
pinned_conversation = db.session.query(PinnedConversation).filter( pinned_conversation = db.session.query(PinnedConversation).filter(
PinnedConversation.app_id == app_model.id, PinnedConversation.app_id == app_model.id,
PinnedConversation.conversation_id == conversation_id, PinnedConversation.conversation_id == conversation_id,
PinnedConversation.created_by_role == ('account' if isinstance(user, Account) else 'end_user'),
PinnedConversation.created_by == user.id PinnedConversation.created_by == user.id
).first() ).first()
......
...@@ -2,7 +2,7 @@ version: '3.1' ...@@ -2,7 +2,7 @@ version: '3.1'
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:latest image: langgenius/dify-api:0.3.1
restart: always restart: always
environment: environment:
# Startup mode, 'api' starts the API server. # Startup mode, 'api' starts the API server.
...@@ -110,7 +110,7 @@ services: ...@@ -110,7 +110,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:latest image: langgenius/dify-api:0.3.1
restart: always restart: always
environment: environment:
# Startup mode, 'worker' starts the Celery worker for processing the queue. # Startup mode, 'worker' starts the Celery worker for processing the queue.
...@@ -156,7 +156,7 @@ services: ...@@ -156,7 +156,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:latest image: langgenius/dify-web:0.3.1
restart: always restart: always
environment: environment:
EDITION: SELF_HOSTED EDITION: SELF_HOSTED
......
...@@ -11,7 +11,7 @@ class DifyClient { ...@@ -11,7 +11,7 @@ class DifyClient {
public function __construct($api_key) { public function __construct($api_key) {
$this->api_key = $api_key; $this->api_key = $api_key;
$this->base_url = "https://api.dify.ai/v1"; $this->base_url = "https://api.dify.ai/v1/";
$this->client = new Client([ $this->client = new Client([
'base_uri' => $this->base_url, 'base_uri' => $this->base_url,
'headers' => [ 'headers' => [
...@@ -37,12 +37,12 @@ class DifyClient { ...@@ -37,12 +37,12 @@ class DifyClient {
'rating' => $rating, 'rating' => $rating,
'user' => $user, 'user' => $user,
]; ];
return $this->send_request('POST', "/messages/{$message_id}/feedbacks", $data); return $this->send_request('POST', "messages/{$message_id}/feedbacks", $data);
} }
public function get_application_parameters($user) { public function get_application_parameters($user) {
$params = ['user' => $user]; $params = ['user' => $user];
return $this->send_request('GET', '/parameters', null, $params); return $this->send_request('GET', 'parameters', null, $params);
} }
} }
...@@ -54,7 +54,7 @@ class CompletionClient extends DifyClient { ...@@ -54,7 +54,7 @@ class CompletionClient extends DifyClient {
'response_mode' => $response_mode, 'response_mode' => $response_mode,
'user' => $user, 'user' => $user,
]; ];
return $this->send_request('POST', '/completion-messages', $data, null, $response_mode === 'streaming'); return $this->send_request('POST', 'completion-messages', $data, null, $response_mode === 'streaming');
} }
} }
...@@ -70,7 +70,7 @@ class ChatClient extends DifyClient { ...@@ -70,7 +70,7 @@ class ChatClient extends DifyClient {
$data['conversation_id'] = $conversation_id; $data['conversation_id'] = $conversation_id;
} }
return $this->send_request('POST', '/chat-messages', $data, null, $response_mode === 'streaming'); return $this->send_request('POST', 'chat-messages', $data, null, $response_mode === 'streaming');
} }
public function get_conversation_messages($user, $conversation_id = null, $first_id = null, $limit = null) { public function get_conversation_messages($user, $conversation_id = null, $first_id = null, $limit = null) {
...@@ -86,7 +86,7 @@ class ChatClient extends DifyClient { ...@@ -86,7 +86,7 @@ class ChatClient extends DifyClient {
$params['limit'] = $limit; $params['limit'] = $limit;
} }
return $this->send_request('GET', '/messages', null, $params); return $this->send_request('GET', 'messages', null, $params);
} }
public function get_conversations($user, $first_id = null, $limit = null, $pinned = null) { public function get_conversations($user, $first_id = null, $limit = null, $pinned = null) {
...@@ -96,7 +96,7 @@ class ChatClient extends DifyClient { ...@@ -96,7 +96,7 @@ class ChatClient extends DifyClient {
'limit' => $limit, 'limit' => $limit,
'pinned'=> $pinned, 'pinned'=> $pinned,
]; ];
return $this->send_request('GET', '/conversations', null, $params); return $this->send_request('GET', 'conversations', null, $params);
} }
public function rename_conversation($conversation_id, $name, $user) { public function rename_conversation($conversation_id, $name, $user) {
...@@ -104,6 +104,6 @@ class ChatClient extends DifyClient { ...@@ -104,6 +104,6 @@ class ChatClient extends DifyClient {
'name' => $name, 'name' => $name,
'user' => $user, 'user' => $user,
]; ];
return $this->send_request('PATCH', "/conversations/{$conversation_id}", $data); return $this->send_request('PATCH', "conversations/{$conversation_id}", $data);
} }
} }
/**/node_modules/*
node_modules/
dist/
build/
out/
.next/
\ No newline at end of file
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
] ]
} }
], ],
"react-hooks/exhaustive-deps": "warning" "react-hooks/exhaustive-deps": "warn"
} }
} }
\ No newline at end of file
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd ./web && npx lint-staged
{
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.format.enable": true,
"[python]": {
"editor.formatOnType": true
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
\ No newline at end of file
...@@ -71,6 +71,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => { ...@@ -71,6 +71,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
<AppCard <AppCard
className='mr-3 flex-1' className='mr-3 flex-1'
appInfo={response} appInfo={response}
cardType='webapp'
onChangeStatus={onChangeSiteStatus} onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode} onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig} /> onSaveSiteConfig={onSaveSiteConfig} />
......
...@@ -3,8 +3,10 @@ import React, { useState } from 'react' ...@@ -3,8 +3,10 @@ import React, { useState } from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { fetchAppDetail } from '@/service/apps'
import type { PeriodParams } from '@/app/components/app/overview/appChart' import type { PeriodParams } from '@/app/components/app/overview/appChart'
import { ConversationsChart, CostChart, EndUsersChart } from '@/app/components/app/overview/appChart' import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, UserSatisfactionRate } from '@/app/components/app/overview/appChart'
import type { Item } from '@/app/components/base/select' import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter' import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
...@@ -20,6 +22,9 @@ export type IChartViewProps = { ...@@ -20,6 +22,9 @@ export type IChartViewProps = {
} }
export default function ChartView({ appId }: IChartViewProps) { export default function ChartView({ appId }: IChartViewProps) {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const isChatApp = response?.mode === 'chat'
const { t } = useTranslation() const { t } = useTranslation()
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
...@@ -27,6 +32,9 @@ export default function ChartView({ appId }: IChartViewProps) { ...@@ -27,6 +32,9 @@ export default function ChartView({ appId }: IChartViewProps) {
setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
} }
if (!response)
return null
return ( return (
<div> <div>
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'> <div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
...@@ -46,6 +54,20 @@ export default function ChartView({ appId }: IChartViewProps) { ...@@ -46,6 +54,20 @@ export default function ChartView({ appId }: IChartViewProps) {
<EndUsersChart period={period} id={appId} /> <EndUsersChart period={period} id={appId} />
</div> </div>
</div> </div>
<div className='flex flex-row w-full mb-6'>
<div className='flex-1 mr-3'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
</div>
<div className='flex-1 ml-3'>
<UserSatisfactionRate period={period} id={appId} />
</div>
</div>
<CostChart period={period} id={appId} /> <CostChart period={period} id={appId} />
</div> </div>
) )
......
...@@ -19,16 +19,16 @@ import I18n from '@/context/i18n' ...@@ -19,16 +19,16 @@ import I18n from '@/context/i18n'
type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill' type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
const STATUS_COLOR_MAP = { const STATUS_COLOR_MAP = {
normal: { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' }, 'normal': { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' },
error: { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' }, 'error': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
verified: { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' }, 'verified': { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' },
'error-api-key-exceed-bill': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' }, 'error-api-key-exceed-bill': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
} }
const CheckCircleIcon: FC<{ className?: string }> = ({ className }) => { const CheckCircleIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<rect width="20" height="20" rx="10" fill="#DEF7EC" /> <rect width="20" height="20" rx="10" fill="#DEF7EC" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" /> <path fillRule="evenodd" clipRule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" />
</svg> </svg>
} }
...@@ -81,11 +81,11 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on ...@@ -81,11 +81,11 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on
catch (err: any) { catch (err: any) {
if (err.status === 400) { if (err.status === 400) {
err.json().then(({ code }: any) => { err.json().then(({ code }: any) => {
if (code === 'provider_request_failed') { if (code === 'provider_request_failed')
setEditStatus('error-api-key-exceed-bill') setEditStatus('error-api-key-exceed-bill')
}
}) })
} else { }
else {
setEditStatus('error') setEditStatus('error')
} }
} }
...@@ -96,19 +96,19 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on ...@@ -96,19 +96,19 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on
const renderErrorMessage = () => { const renderErrorMessage = () => {
if (validating) { if (validating) {
return ( return (
<div className={`text-primary-600 mt-2 text-xs`}> <div className={'text-primary-600 mt-2 text-xs'}>
{t('common.provider.validating')} {t('common.provider.validating')}
</div> </div>
) )
} }
if (editStatus === 'error-api-key-exceed-bill') { if (editStatus === 'error-api-key-exceed-bill') {
return ( return (
<div className={`text-[#D92D20] mt-2 text-xs`}> <div className={'text-[#D92D20] mt-2 text-xs'}>
{t('common.provider.apiKeyExceedBill')} {t('common.provider.apiKeyExceedBill')}
{locale === 'en' ? ' ' : ''} {locale === 'en' ? ' ' : ''}
<Link <Link
className='underline' className='underline'
href="https://platform.openai.com/account/api-keys" href="https://platform.openai.com/account/api-keys"
target={'_blank'}> target={'_blank'}>
{locale === 'en' ? 'this link' : '这篇文档'} {locale === 'en' ? 'this link' : '这篇文档'}
</Link> </Link>
...@@ -117,7 +117,7 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on ...@@ -117,7 +117,7 @@ const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, on
} }
if (editStatus === 'error') { if (editStatus === 'error') {
return ( return (
<div className={`text-[#D92D20] mt-2 text-xs`}> <div className={'text-[#D92D20] mt-2 text-xs'}>
{t('common.provider.invalidKey')} {t('common.provider.invalidKey')}
</div> </div>
) )
......
...@@ -7,9 +7,9 @@ export type IAppDetail = { ...@@ -7,9 +7,9 @@ export type IAppDetail = {
const AppDetail: FC<IAppDetail> = ({ children }) => { const AppDetail: FC<IAppDetail> = ({ children }) => {
return ( return (
<> <>
{children} {children}
</> </>
) )
} }
......
...@@ -8,6 +8,8 @@ import NewAppCard from './NewAppCard' ...@@ -8,6 +8,8 @@ import NewAppCard from './NewAppCard'
import { AppListResponse } from '@/models/app' import { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps' import { fetchAppList } from '@/service/apps'
import { useSelector } from '@/context/app-context' import { useSelector } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useTranslation } from 'react-i18next'
const getKey = (pageIndex: number, previousPageData: AppListResponse) => { const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
if (!pageIndex || previousPageData.has_more) if (!pageIndex || previousPageData.has_more)
...@@ -16,11 +18,20 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => { ...@@ -16,11 +18,20 @@ const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
} }
const Apps = () => { const Apps = () => {
const { t } = useTranslation()
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false })
const loadingStateRef = useRef(false) const loadingStateRef = useRef(false)
const pageContainerRef = useSelector(state => state.pageContainerRef) const pageContainerRef = useSelector(state => state.pageContainerRef)
const anchorRef = useRef<HTMLAnchorElement>(null) const anchorRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
document.title = `${t('app.title')} - Dify`;
if(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()
}
}, [])
useEffect(() => { useEffect(() => {
loadingStateRef.current = isLoading loadingStateRef.current = isLoading
}, [isLoading]) }, [isLoading])
......
...@@ -37,7 +37,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => { ...@@ -37,7 +37,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
// Emoji Picker // Emoji Picker
const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' }) const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps) const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
...@@ -102,7 +102,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => { ...@@ -102,7 +102,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
setShowEmojiPicker(false) setShowEmojiPicker(false)
}} }}
onClose={() => { onClose={() => {
setEmoji({ icon: '🍌', icon_background: '#FFEAD5' }) setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false) setShowEmojiPicker(false)
}} }}
/>} />}
......
...@@ -14,23 +14,19 @@ const AppList = async () => { ...@@ -14,23 +14,19 @@ const AppList = async () => {
<footer className='px-12 py-6 grow-0 shrink-0'> <footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3> <h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p> <p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
{/*<p className='mt-3 text-sm'>*/} {/* <p className='mt-3 text-sm'> */}
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}>*/} {/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}> */}
{/* {t('roadmap')}*/} {/* {t('roadmap')} */}
{/* <span className={style.linkIcon} />*/} {/* <span className={style.linkIcon} /> */}
{/* </a>*/} {/* </a> */}
{/*</p>*/} {/* </p> */}
<div className='flex items-center gap-2 mt-3'> <div className='flex items-center gap-2 mt-3'>
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a> <a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius/dify'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/AhzKf7dNgk'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a> <a className={style.socialMediaLink} target='_blank' href='https://discord.gg/FngNHpbcY7'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
</div> </div>
</footer> </footer>
</div > </div >
) )
} }
export const metadata = {
title: 'Apps - Dify',
}
export default AppList export default AppList
...@@ -3,7 +3,13 @@ import { getLocaleOnServer } from '@/i18n/server' ...@@ -3,7 +3,13 @@ import { getLocaleOnServer } from '@/i18n/server'
import { useTranslation } from '@/i18n/i18next-serverside-config' import { useTranslation } from '@/i18n/i18next-serverside-config'
import Form from '@/app/components/datasets/settings/form' import Form from '@/app/components/datasets/settings/form'
const Settings = async () => { type Props = {
params: { datasetId: string }
}
const Settings = async ({
params: { datasetId },
}: Props) => {
const locale = getLocaleOnServer() const locale = getLocaleOnServer()
const { t } = await useTranslation(locale, 'dataset-settings') const { t } = await useTranslation(locale, 'dataset-settings')
...@@ -14,7 +20,7 @@ const Settings = async () => { ...@@ -14,7 +20,7 @@ const Settings = async () => {
<div className='text-sm text-gray-500'>{t('desc')}</div> <div className='text-sm text-gray-500'>{t('desc')}</div>
</div> </div>
<div> <div>
<Form /> <Form datasetId={datasetId} />
</div> </div>
</div> </div>
) )
......
import AppList from "@/app/components/explore/app-list"
import React from 'react'
const Apps = ({ }) => {
return <AppList />
}
export default React.memo(Apps)
import React, { FC } from 'react'
import Main from '@/app/components/explore/installed-app'
export interface IInstalledAppProps {
params: {
appId: string
}
}
const InstalledApp: FC<IInstalledAppProps> = ({ params: {appId} }) => {
return (
<Main id={appId} />
)
}
export default React.memo(InstalledApp)
import type { FC } from 'react'
import React from 'react'
import ExploreClient from '@/app/components/explore'
export type IAppDetail = {
children: React.ReactNode
}
const AppDetail: FC<IAppDetail> = ({ children }) => {
return (
<ExploreClient>
{children}
</ExploreClient>
)
}
export default React.memo(AppDetail)
...@@ -4,12 +4,10 @@ import React from 'react' ...@@ -4,12 +4,10 @@ import React from 'react'
import type { IMainProps } from '@/app/components/share/chat' import type { IMainProps } from '@/app/components/share/chat'
import Main from '@/app/components/share/chat' import Main from '@/app/components/share/chat'
const Chat: FC<IMainProps> = ({ const Chat: FC<IMainProps> = () => {
params,
}: any) => {
return ( return (
<Main params={params} /> <Main />
) )
} }
......
...@@ -14,32 +14,37 @@ export function randomString(length: number) { ...@@ -14,32 +14,37 @@ export function randomString(length: number) {
} }
export type IAppBasicProps = { export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' iconType?: 'app' | 'api' | 'dataset' | 'webapp'
icon?: string, icon?: string
icon_background?: string, icon_background?: string
name: string name: string
type: string | React.ReactNode type: string | React.ReactNode
hoverTip?: string hoverTip?: string
textStyle?: { main?: string; extra?: string } textStyle?: { main?: string; extra?: string }
} }
const AlgorithmSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9H1.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" /> <path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
const ICON_MAP = { const ICON_MAP = {
'app': <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />, app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
'api': <AppIcon innerIcon={AlgorithmSvg} className='border !bg-purple-50 !border-purple-200' />, api: <AppIcon innerIcon={ApiSvg} className='border !bg-purple-50 !border-purple-200' />,
'dataset': <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' /> dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !bg-primary-100 !border-primary-200' />,
} }
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) { export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) {
...@@ -50,8 +55,8 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip, ...@@ -50,8 +55,8 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip,
<AppIcon icon={icon} background={icon_background} /> <AppIcon icon={icon} background={icon_background} />
</div> </div>
)} )}
{iconType !== 'app' && {iconType !== 'app'
<div className='flex-shrink-0 mr-3'> && <div className='flex-shrink-0 mr-3'>
{ICON_MAP[iconType]} {ICON_MAP[iconType]}
</div> </div>
......
...@@ -18,7 +18,6 @@ export default function NavLink({ ...@@ -18,7 +18,6 @@ export default function NavLink({
return ( return (
<Link <Link
prefetch
key={name} key={name}
href={href} href={href}
className={classNames( className={classNames(
......
This diff is collapsed.
...@@ -102,6 +102,10 @@ ...@@ -102,6 +102,10 @@
background: url(./icons/send.svg) center center no-repeat; background: url(./icons/send.svg) center center no-repeat;
} }
.sendBtnActive {
background-image: url(./icons/send-active.svg);
}
.sendBtn:hover { .sendBtn:hover {
background-image: url(./icons/send-active.svg); background-image: url(./icons/send-active.svg);
background-color: #EBF5FF; background-color: #EBF5FF;
......
'use client' 'use client'
import React, { FC } from 'react' import type { FC } from 'react'
import React from 'react'
const SuggestedQuestionsAfterAnswerIcon: FC = () => { const SuggestedQuestionsAfterAnswerIcon: FC = () => {
return ( return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" /> <path fillRule="evenodd" clipRule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" />
</svg> </svg>
) )
} }
......
...@@ -29,6 +29,7 @@ const options = [ ...@@ -29,6 +29,7 @@ const options = [
{ id: 'gpt-4', name: 'gpt-4', type: AppType.chat }, // 8k version { id: 'gpt-4', name: 'gpt-4', type: AppType.chat }, // 8k version
{ id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo', type: AppType.completion }, { id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo', type: AppType.completion },
{ id: 'text-davinci-003', name: 'text-davinci-003', type: AppType.completion }, { id: 'text-davinci-003', name: 'text-davinci-003', type: AppType.completion },
{ id: 'gpt-4', name: 'gpt-4', type: AppType.completion }, // 8k version
] ]
const ModelIcon = ({ className }: { className?: string }) => ( const ModelIcon = ({ className }: { className?: string }) => (
...@@ -205,14 +206,14 @@ const ConifgModel: FC<IConifgModelProps> = ({ ...@@ -205,14 +206,14 @@ const ConifgModel: FC<IConifgModelProps> = ({
<div className="flex items-center justify-between my-5 h-9"> <div className="flex items-center justify-between my-5 h-9">
<div>{t('appDebug.modelConfig.model')}</div> <div>{t('appDebug.modelConfig.model')}</div>
{/* model selector */} {/* model selector */}
<div className="relative"> <div className="relative" style={{zIndex: 30}}>
<div ref={triggerRef} onClick={() => !selectModelDisabled && toogleOption()} className={cn(selectModelDisabled ? 'cursor-not-allowed' : 'cursor-pointer', "flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ")}> <div ref={triggerRef} onClick={() => !selectModelDisabled && toogleOption()} className={cn(selectModelDisabled ? 'cursor-not-allowed' : 'cursor-pointer', "flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ")}>
<ModelIcon /> <ModelIcon />
<div className="text-sm gray-900">{selectedModel?.name}</div> <div className="text-sm gray-900">{selectedModel?.name}</div>
{!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />} {!selectModelDisabled && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
</div> </div>
{isShowOption && ( {isShowOption && (
<div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', "absolute right-0 bg-gray-50 rounded-lg")}> <div className={cn(isChatApp ? 'w-[159px]' : 'w-[179px]', "absolute right-0 bg-gray-50 rounded-lg shadow")}>
{availableModels.map(item => ( {availableModels.map(item => (
<div key={item.id} onClick={handleSelectModel(item.id)} className="flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100"> <div key={item.id} onClick={handleSelectModel(item.id)} className="flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100">
<ModelIcon className='mr-2' /> <ModelIcon className='mr-2' />
......
'use client' 'use client'
import React, { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
import s from './style.module.css'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
export interface IFeatureItemProps { export type IFeatureItemProps = {
icon: React.ReactNode icon: React.ReactNode
previewImgClassName?: string
title: string title: string
description: string description: string
value: boolean value: boolean
...@@ -12,13 +16,14 @@ export interface IFeatureItemProps { ...@@ -12,13 +16,14 @@ export interface IFeatureItemProps {
const FeatureItem: FC<IFeatureItemProps> = ({ const FeatureItem: FC<IFeatureItemProps> = ({
icon, icon,
previewImgClassName,
title, title,
description, description,
value, value,
onChange onChange,
}) => { }) => {
return ( return (
<div className='flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer'> <div className={cn(s.wrap, 'relative flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer')}>
<div className='flex space-x-3 mr-2'> <div className='flex space-x-3 mr-2'>
{/* icon */} {/* icon */}
<div <div
...@@ -36,6 +41,11 @@ const FeatureItem: FC<IFeatureItemProps> = ({ ...@@ -36,6 +41,11 @@ const FeatureItem: FC<IFeatureItemProps> = ({
</div> </div>
<Switch onChange={onChange} defaultValue={value} /> <Switch onChange={onChange} defaultValue={value} />
{
previewImgClassName && (
<div className={cn(s.preview, s[previewImgClassName])}>
</div>)
}
</div> </div>
) )
} }
......
.preview {
display: none;
position: fixed;
transform: translate(410px, -54px);
width: 280px;
height: 360px;
background: center center no-repeat;
background-size: contain;
}
.wrap:hover .preview {
display: block;
}
.openingStatementPreview {
background-image: url(./preview-imgs/opening-statement.svg);
}
.suggestedQuestionsAfterAnswerPreview {
background-image: url(./preview-imgs/suggested-questions-after-answer.svg);
}
.moreLikeThisPreview {
background-image: url(./preview-imgs/more-like-this.svg);
}
\ No newline at end of file
'use client' 'use client'
import React, { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import FeatureItem from './feature-item'
import FeatureGroup from '../feature-group' import FeatureGroup from '../feature-group'
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon' import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
import FeatureItem from './feature-item'
import Modal from '@/app/components/base/modal'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon' import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
type IConfig = {
interface IConfig {
openingStatement: boolean openingStatement: boolean
moreLikeThis: boolean moreLikeThis: boolean
suggestedQuestionsAfterAnswer: boolean suggestedQuestionsAfterAnswer: boolean
} }
export interface IChooseFeatureProps { export type IChooseFeatureProps = {
isShow: boolean isShow: boolean
onClose: () => void onClose: () => void
config: IConfig config: IConfig
...@@ -32,7 +32,7 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({ ...@@ -32,7 +32,7 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
onClose, onClose,
isChatApp, isChatApp,
config, config,
onChange onChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -43,6 +43,7 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({ ...@@ -43,6 +43,7 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
className='w-[400px]' className='w-[400px]'
title={t('appDebug.operation.addFeature')} title={t('appDebug.operation.addFeature')}
closable closable
overflowVisible
> >
<div className='pt-5 pb-10'> <div className='pt-5 pb-10'>
{/* Chat Feature */} {/* Chat Feature */}
...@@ -54,17 +55,19 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({ ...@@ -54,17 +55,19 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
<> <>
<FeatureItem <FeatureItem
icon={OpeningStatementIcon} icon={OpeningStatementIcon}
previewImgClassName='openingStatementPreview'
title={t('appDebug.feature.conversationOpener.title')} title={t('appDebug.feature.conversationOpener.title')}
description={t('appDebug.feature.conversationOpener.description')} description={t('appDebug.feature.conversationOpener.description')}
value={config.openingStatement} value={config.openingStatement}
onChange={(value) => onChange('openingStatement', value)} onChange={value => onChange('openingStatement', value)}
/> />
<FeatureItem <FeatureItem
icon={<SuggestedQuestionsAfterAnswerIcon />} icon={<SuggestedQuestionsAfterAnswerIcon />}
previewImgClassName='suggestedQuestionsAfterAnswerPreview'
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')} title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')} description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
value={config.suggestedQuestionsAfterAnswer} value={config.suggestedQuestionsAfterAnswer}
onChange={(value) => onChange('suggestedQuestionsAfterAnswer', value)} onChange={value => onChange('suggestedQuestionsAfterAnswer', value)}
/> />
</> </>
</FeatureGroup> </FeatureGroup>
...@@ -76,10 +79,11 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({ ...@@ -76,10 +79,11 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
<> <>
<FeatureItem <FeatureItem
icon={<MoreLikeThisIcon />} icon={<MoreLikeThisIcon />}
previewImgClassName='moreLikeThisPreview'
title={t('appDebug.feature.moreLikeThis.title')} title={t('appDebug.feature.moreLikeThis.title')}
description={t('appDebug.feature.moreLikeThis.description')} description={t('appDebug.feature.moreLikeThis.description')}
value={config.moreLikeThis} value={config.moreLikeThis}
onChange={(value) => onChange('moreLikeThis', value)} onChange={value => onChange('moreLikeThis', value)}
/> />
</> </>
</FeatureGroup> </FeatureGroup>
......
...@@ -12,7 +12,6 @@ import { formatNumber } from '@/utils/format' ...@@ -12,7 +12,6 @@ import { formatNumber } from '@/utils/format'
import Link from 'next/link' import Link from 'next/link'
import s from './style.module.css' import s from './style.module.css'
import Toast from '@/app/components/base/toast'
export interface ISelectDataSetProps { export interface ISelectDataSetProps {
isShow: boolean isShow: boolean
...@@ -32,8 +31,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({ ...@@ -32,8 +31,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
const [loaded, setLoaded] = React.useState(false) const [loaded, setLoaded] = React.useState(false)
const [datasets, setDataSets] = React.useState<DataSet[] | null>(null) const [datasets, setDataSets] = React.useState<DataSet[] | null>(null)
const hasNoData = !datasets || datasets?.length === 0 const hasNoData = !datasets || datasets?.length === 0
// Only one dataset can be selected. Historical data retains data and supports multiple selections, but when saving, only one can be selected. This is based on considerations of performance and accuracy. const canSelectMulti = true
const canSelectMulti = selectedIds.length > 1
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } }) const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
...@@ -57,13 +55,6 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({ ...@@ -57,13 +55,6 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
} }
const handleSelect = () => { const handleSelect = () => {
if (selected.length > 1) {
Toast.notify({
type: 'error',
message: t('appDebug.feature.dataSet.notSupportSelectMulti')
})
return
}
onSelect(selected) onSelect(selected)
} }
return ( return (
......
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import React, { useEffect, useState, useRef } from 'react' import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import produce from 'immer' import produce from 'immer'
import { useBoolean, useGetState } from 'ahooks' import { useBoolean, useGetState } from 'ahooks'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import dayjs from 'dayjs'
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed'
import GroupName from '../base/group-name'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { IChatItem } from '@/app/components/app/chat' import type { IChatItem } from '@/app/components/app/chat'
import Chat from '@/app/components/app/chat' import Chat from '@/app/components/app/chat'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import { sendChatMessage, sendCompletionMessage, fetchSuggestedQuestions, fetchConvesationMessages } from '@/service/debug' import { fetchConvesationMessages, fetchSuggestedQuestions, sendChatMessage, sendCompletionMessage } from '@/service/debug'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import type { ModelConfig as BackendModelConfig } from '@/types/app' import type { ModelConfig as BackendModelConfig } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config' import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed'
import TextGeneration from '@/app/components/app/text-generate/item' import TextGeneration from '@/app/components/app/text-generate/item'
import GroupName from '../base/group-name'
import dayjs from 'dayjs'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
interface IDebug { type IDebug = {
hasSetAPIKEY: boolean hasSetAPIKEY: boolean
onSetting: () => void onSetting: () => void
} }
const Debug: FC<IDebug> = ({ const Debug: FC<IDebug> = ({
hasSetAPIKEY = true, hasSetAPIKEY = true,
onSetting onSetting,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
...@@ -51,14 +51,12 @@ const Debug: FC<IDebug> = ({ ...@@ -51,14 +51,12 @@ const Debug: FC<IDebug> = ({
completionParams, completionParams,
} = useContext(ConfigContext) } = useContext(ConfigContext)
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
const chatListDomRef = useRef<HTMLDivElement>(null) const chatListDomRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
// scroll to bottom // scroll to bottom
if (chatListDomRef.current) { if (chatListDomRef.current)
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
}
}, [chatList]) }, [chatList])
const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs) const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs)
...@@ -68,7 +66,7 @@ const Debug: FC<IDebug> = ({ ...@@ -68,7 +66,7 @@ const Debug: FC<IDebug> = ({
id: `${Date.now()}`, id: `${Date.now()}`,
content: getIntroduction(), content: getIntroduction(),
isAnswer: true, isAnswer: true,
isOpeningStatement: true isOpeningStatement: true,
}]) }])
} }
}, [introduction, modelConfig.configs.prompt_variables, inputs]) }, [introduction, modelConfig.configs.prompt_variables, inputs])
...@@ -76,11 +74,12 @@ const Debug: FC<IDebug> = ({ ...@@ -76,11 +74,12 @@ const Debug: FC<IDebug> = ({
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
const [abortController, setAbortController] = useState<AbortController | null>(null) const [abortController, setAbortController] = useState<AbortController | null>(null)
const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false) const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
useEffect(() => { useEffect(() => {
if (formattingChanged && chatList.some(item => !item.isAnswer)) { if (formattingChanged && chatList.some(item => !item.isAnswer))
setIsShowFormattingChangeConfirm(true) setIsShowFormattingChangeConfirm(true)
}
setFormattingChanged(false) setFormattingChanged(false)
}, [formattingChanged]) }, [formattingChanged])
...@@ -88,12 +87,14 @@ const Debug: FC<IDebug> = ({ ...@@ -88,12 +87,14 @@ const Debug: FC<IDebug> = ({
setConversationId(null) setConversationId(null)
abortController?.abort() abortController?.abort()
setResponsingFalse() setResponsingFalse()
setChatList(introduction ? [{ setChatList(introduction
id: `${Date.now()}`, ? [{
content: getIntroduction(), id: `${Date.now()}`,
isAnswer: true, content: getIntroduction(),
isOpeningStatement: true isAnswer: true,
}] : []) isOpeningStatement: true,
}]
: [])
setIsShowSuggestion(false) setIsShowSuggestion(false)
} }
...@@ -119,12 +120,11 @@ const Debug: FC<IDebug> = ({ ...@@ -119,12 +120,11 @@ const Debug: FC<IDebug> = ({
}) // compatible with old version }) // compatible with old version
// debugger // debugger
requiredVars.forEach(({ key }) => { requiredVars.forEach(({ key }) => {
if (hasEmptyInput) { if (hasEmptyInput)
return return
}
if (!inputs[key]) { if (!inputs[key])
hasEmptyInput = true hasEmptyInput = true
}
}) })
if (hasEmptyInput) { if (hasEmptyInput) {
...@@ -134,7 +134,6 @@ const Debug: FC<IDebug> = ({ ...@@ -134,7 +134,6 @@ const Debug: FC<IDebug> = ({
return !hasEmptyInput return !hasEmptyInput
} }
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
const doShowSuggestion = isShowSuggestion && !isResponsing const doShowSuggestion = isShowSuggestion && !isResponsing
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([]) const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
const onSend = async (message: string) => { const onSend = async (message: string) => {
...@@ -147,7 +146,7 @@ const Debug: FC<IDebug> = ({ ...@@ -147,7 +146,7 @@ const Debug: FC<IDebug> = ({
dataset: { dataset: {
enabled: true, enabled: true,
id, id,
} },
})) }))
const postModelConfig: BackendModelConfig = { const postModelConfig: BackendModelConfig = {
...@@ -155,17 +154,17 @@ const Debug: FC<IDebug> = ({ ...@@ -155,17 +154,17 @@ const Debug: FC<IDebug> = ({
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables), user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
opening_statement: introduction, opening_statement: introduction,
more_like_this: { more_like_this: {
enabled: false enabled: false,
}, },
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig, suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
agent_mode: { agent_mode: {
enabled: true, enabled: true,
tools: [...postDatasets] tools: [...postDatasets],
}, },
model: { model: {
provider: modelConfig.provider, provider: modelConfig.provider,
name: modelConfig.model_id, name: modelConfig.model_id,
completion_params: completionParams as any completion_params: completionParams as any,
}, },
} }
...@@ -215,32 +214,32 @@ const Debug: FC<IDebug> = ({ ...@@ -215,32 +214,32 @@ const Debug: FC<IDebug> = ({
setConversationId(newConversationId) setConversationId(newConversationId)
_newConversationId = newConversationId _newConversationId = newConversationId
} }
if (messageId) { if (messageId)
responseItem.id = messageId responseItem.id = messageId
}
// closesure new list is outdated. // closesure new list is outdated.
const newListWithAnswer = produce( const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => { (draft) => {
if (!draft.find(item => item.id === questionId)) { if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem }) draft.push({ ...questionItem })
}
draft.push({ ...responseItem }) draft.push({ ...responseItem })
}) })
setChatList(newListWithAnswer) setChatList(newListWithAnswer)
}, },
async onCompleted(hasError?: boolean) { async onCompleted(hasError?: boolean) {
setResponsingFalse() setResponsingFalse()
if (hasError) { if (hasError)
return return
}
if (_newConversationId) { if (_newConversationId) {
const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string) const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string)
const newResponseItem = data.find((item: any) => item.id === responseItem.id) const newResponseItem = data.find((item: any) => item.id === responseItem.id)
if (!newResponseItem) { if (!newResponseItem)
return return
}
setChatList(produce(getChatList(), draft => { setChatList(produce(getChatList(), (draft) => {
const index = draft.findIndex(item => item.id === responseItem.id) const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) { if (index !== -1) {
draft[index] = { draft[index] = {
...@@ -249,7 +248,7 @@ const Debug: FC<IDebug> = ({ ...@@ -249,7 +248,7 @@ const Debug: FC<IDebug> = ({
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'), time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens, tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2), latency: newResponseItem.provider_response_latency.toFixed(2),
} },
} }
} }
})) }))
...@@ -263,10 +262,10 @@ const Debug: FC<IDebug> = ({ ...@@ -263,10 +262,10 @@ const Debug: FC<IDebug> = ({
onError() { onError() {
setResponsingFalse() setResponsingFalse()
// role back placeholder answer // role back placeholder answer
setChatList(produce(getChatList(), draft => { setChatList(produce(getChatList(), (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
})) }))
} },
}) })
return true return true
} }
...@@ -277,7 +276,7 @@ const Debug: FC<IDebug> = ({ ...@@ -277,7 +276,7 @@ const Debug: FC<IDebug> = ({
}, [controlClearChatMessage]) }, [controlClearChatMessage])
const [completionQuery, setCompletionQuery] = useState('') const [completionQuery, setCompletionQuery] = useState('')
const [completionRes, setCompletionRes] = useState(``) const [completionRes, setCompletionRes] = useState('')
const sendTextCompletion = async () => { const sendTextCompletion = async () => {
if (isResponsing) { if (isResponsing) {
...@@ -297,7 +296,7 @@ const Debug: FC<IDebug> = ({ ...@@ -297,7 +296,7 @@ const Debug: FC<IDebug> = ({
dataset: { dataset: {
enabled: true, enabled: true,
id, id,
} },
})) }))
const postModelConfig: BackendModelConfig = { const postModelConfig: BackendModelConfig = {
...@@ -308,16 +307,15 @@ const Debug: FC<IDebug> = ({ ...@@ -308,16 +307,15 @@ const Debug: FC<IDebug> = ({
more_like_this: moreLikeThisConifg, more_like_this: moreLikeThisConifg,
agent_mode: { agent_mode: {
enabled: true, enabled: true,
tools: [...postDatasets] tools: [...postDatasets],
}, },
model: { model: {
provider: modelConfig.provider, provider: modelConfig.provider,
name: modelConfig.model_id, name: modelConfig.model_id,
completion_params: completionParams as any completion_params: completionParams as any,
}, },
} }
const data = { const data = {
inputs, inputs,
query: completionQuery, query: completionQuery,
...@@ -338,11 +336,10 @@ const Debug: FC<IDebug> = ({ ...@@ -338,11 +336,10 @@ const Debug: FC<IDebug> = ({
}, },
onError() { onError() {
setResponsingFalse() setResponsingFalse()
} },
}) })
} }
return ( return (
<> <>
<div className="shrink-0"> <div className="shrink-0">
...@@ -368,7 +365,7 @@ const Debug: FC<IDebug> = ({ ...@@ -368,7 +365,7 @@ const Debug: FC<IDebug> = ({
{/* Chat */} {/* Chat */}
{mode === AppType.chat && ( {mode === AppType.chat && (
<div className="mt-[34px] h-full flex flex-col"> <div className="mt-[34px] h-full flex flex-col">
<div className={cn(doShowSuggestion ? 'pb-[140px]' : 'pb-[66px]', "relative mt-1.5 grow h-[200px] overflow-hidden")}> <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
<div className="h-full overflow-y-auto" ref={chatListDomRef}> <div className="h-full overflow-y-auto" ref={chatListDomRef}>
{/* {JSON.stringify(chatList)} */} {/* {JSON.stringify(chatList)} */}
<Chat <Chat
......
...@@ -56,8 +56,11 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({ ...@@ -56,8 +56,11 @@ const OpeningStatement: FC<IOpeningStatementProps> = ({
}, [value]) }, [value])
const coloredContent = (tempValue || '') const coloredContent = (tempValue || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>` .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />') .replace(/\n/g, '<br />')
const handleEdit = () => { const handleEdit = () => {
......
...@@ -75,7 +75,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({ ...@@ -75,7 +75,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<div <div
className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all" className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: format(replaceStringWithValuesWithFormat(promptTemplate, promptVariables, inputs)), __html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
}} }}
> >
</div> </div>
......
...@@ -31,7 +31,7 @@ const limit = 10 ...@@ -31,7 +31,7 @@ const limit = 10
const ThreeDotsIcon: FC<{ className?: string }> = ({ className }) => { const ThreeDotsIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
} }
...@@ -63,9 +63,9 @@ const Logs: FC<ILogsProps> = ({ appId }) => { ...@@ -63,9 +63,9 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
limit, limit,
...(queryParams.period !== 'all' ...(queryParams.period !== 'all'
? { ? {
start: dayjs().subtract(queryParams.period as number, 'day').format('YYYY-MM-DD HH:mm'), start: dayjs().subtract(queryParams.period as number, 'day').format('YYYY-MM-DD HH:mm'),
end: dayjs().format('YYYY-MM-DD HH:mm'), end: dayjs().format('YYYY-MM-DD HH:mm'),
} }
: {}), : {}),
...omit(queryParams, ['period']), ...omit(queryParams, ['period']),
} }
...@@ -77,16 +77,16 @@ const Logs: FC<ILogsProps> = ({ appId }) => { ...@@ -77,16 +77,16 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
// When the details are obtained, proceed to the next request // When the details are obtained, proceed to the next request
const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode
? { ? {
url: `/apps/${appId}/chat-conversations`, url: `/apps/${appId}/chat-conversations`,
params: query, params: query,
} }
: null, fetchChatConversations) : null, fetchChatConversations)
const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode
? { ? {
url: `/apps/${appId}/completion-conversations`, url: `/apps/${appId}/completion-conversations`,
params: query, params: query,
} }
: null, fetchCompletionConversations) : null, fetchCompletionConversations)
const total = isChatMode ? chatConversations?.total : completionConversations?.total const total = isChatMode ? chatConversations?.total : completionConversations?.total
......
...@@ -22,7 +22,7 @@ import type { AppDetailResponse } from '@/models/app' ...@@ -22,7 +22,7 @@ import type { AppDetailResponse } from '@/models/app'
export type IAppCardProps = { export type IAppCardProps = {
className?: string className?: string
appInfo: AppDetailResponse appInfo: AppDetailResponse
cardType?: 'app' | 'api' cardType?: 'app' | 'api' | 'webapp'
customBgColor?: string customBgColor?: string
onChangeStatus: (val: boolean) => Promise<any> onChangeStatus: (val: boolean) => Promise<any>
onSaveSiteConfig?: (params: any) => Promise<any> onSaveSiteConfig?: (params: any) => Promise<any>
...@@ -46,15 +46,16 @@ function AppCard({ ...@@ -46,15 +46,16 @@ function AppCard({
const { t } = useTranslation() const { t } = useTranslation()
const OPERATIONS_MAP = { const OPERATIONS_MAP = {
app: [ webapp: [
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon }, { opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon }, { opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
{ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon }, { opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon },
], ],
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }], api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
app: [],
} }
const isApp = cardType === 'app' const isApp = cardType === 'app' || cardType === 'webapp'
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title') const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title')
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
const { app_base_url, access_token } = appInfo.site ?? {} const { app_base_url, access_token } = appInfo.site ?? {}
...@@ -100,7 +101,7 @@ function AppCard({ ...@@ -100,7 +101,7 @@ function AppCard({
<div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}> <div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between"> <div className="mb-2.5 flex flex-row items-start justify-between">
<AppBasic <AppBasic
iconType={isApp ? 'app' : 'api'} iconType={cardType}
icon={appInfo.icon} icon={appInfo.icon}
icon_background={appInfo.icon_background} icon_background={appInfo.icon_background}
name={basicName} name={basicName}
...@@ -129,7 +130,7 @@ function AppCard({ ...@@ -129,7 +130,7 @@ function AppCard({
</div> </div>
<div <div
className={`pt-2 flex flex-row items-center ${!isApp ? 'mb-[calc(2rem_+_1px)]' : '' className={`pt-2 flex flex-row items-center ${!isApp ? 'mb-[calc(2rem_+_1px)]' : ''
}`} }`}
> >
{OPERATIONS_MAP[cardType].map((op) => { {OPERATIONS_MAP[cardType].map((op) => {
return ( return (
......
...@@ -6,12 +6,12 @@ import type { EChartsOption } from 'echarts' ...@@ -6,12 +6,12 @@ import type { EChartsOption } from 'echarts'
import useSWR from 'swr' import useSWR from 'swr'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import { formatNumber } from '@/utils/format'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic' import Basic from '@/app/components/app-sidebar/basic'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app' import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app'
import { getAppDailyConversations, getAppDailyEndUsers, getAppTokenCosts } from '@/service/apps' import { getAppDailyConversations, getAppDailyEndUsers, getAppStatistics, getAppTokenCosts } from '@/service/apps'
const valueFormatter = (v: string | number) => v const valueFormatter = (v: string | number) => v
const COLOR_TYPE_MAP = { const COLOR_TYPE_MAP = {
...@@ -76,6 +76,9 @@ export type IBizChartProps = { ...@@ -76,6 +76,9 @@ export type IBizChartProps = {
export type IChartProps = { export type IChartProps = {
className?: string className?: string
basicInfo: { title: string; explanation: string; timePeriod: string } basicInfo: { title: string; explanation: string; timePeriod: string }
valueKey?: string
isAvg?: boolean
unit?: string
yMax?: number yMax?: number
chartType: IChartType chartType: IChartType
chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> } chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> }
...@@ -85,6 +88,9 @@ const Chart: React.FC<IChartProps> = ({ ...@@ -85,6 +88,9 @@ const Chart: React.FC<IChartProps> = ({
basicInfo: { title, explanation, timePeriod }, basicInfo: { title, explanation, timePeriod },
chartType = 'conversations', chartType = 'conversations',
chartData, chartData,
valueKey,
isAvg,
unit = '',
yMax, yMax,
className, className,
}) => { }) => {
...@@ -96,7 +102,7 @@ const Chart: React.FC<IChartProps> = ({ ...@@ -96,7 +102,7 @@ const Chart: React.FC<IChartProps> = ({
extraDataForMarkLine.unshift('') extraDataForMarkLine.unshift('')
const xData = statistics.map(({ date }) => date) const xData = statistics.map(({ date }) => date)
const yField = Object.keys(statistics[0]).find(name => name.includes('count')) || '' const yField = valueKey || Object.keys(statistics[0]).find(name => name.includes('count')) || ''
const yData = statistics.map((item) => { const yData = statistics.map((item) => {
// @ts-expect-error field is valid // @ts-expect-error field is valid
return item[yField] || 0 return item[yField] || 0
...@@ -199,8 +205,8 @@ const Chart: React.FC<IChartProps> = ({ ...@@ -199,8 +205,8 @@ const Chart: React.FC<IChartProps> = ({
return `<div style='color:#6B7280;font-size:12px'>${params.name}</div> return `<div style='color:#6B7280;font-size:12px'>${params.name}</div>
<div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])} <div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])}
${!CHART_TYPE_CONFIG[chartType].showTokens ${!CHART_TYPE_CONFIG[chartType].showTokens
? '' ? ''
: `<span style='font-size:12px'> : `<span style='font-size:12px'>
<span style='margin-left:4px;color:#6B7280'>(</span> <span style='margin-left:4px;color:#6B7280'>(</span>
<span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span> <span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span>
<span style='color:#6B7280'>)</span> <span style='color:#6B7280'>)</span>
...@@ -211,8 +217,7 @@ const Chart: React.FC<IChartProps> = ({ ...@@ -211,8 +217,7 @@ const Chart: React.FC<IChartProps> = ({
}, },
], ],
} }
const sumData = isAvg ? (sum(yData) / yData.length) : sum(yData)
const sumData = sum(yData)
return ( return (
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}> <div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}>
...@@ -221,7 +226,7 @@ const Chart: React.FC<IChartProps> = ({ ...@@ -221,7 +226,7 @@ const Chart: React.FC<IChartProps> = ({
</div> </div>
<div className='mb-4'> <div className='mb-4'>
<Basic <Basic
name={chartType !== 'costs' ? sumData.toLocaleString() : `${sumData < 1000 ? sumData : (formatNumber(Math.round(sumData / 1000)) + 'k')}`} name={chartType !== 'costs' ? (sumData.toLocaleString() + unit) : `${sumData < 1000 ? sumData : (`${formatNumber(Math.round(sumData / 1000))}k`)}`}
type={!CHART_TYPE_CONFIG[chartType].showTokens type={!CHART_TYPE_CONFIG[chartType].showTokens
? '' ? ''
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'> : <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
...@@ -236,9 +241,9 @@ const Chart: React.FC<IChartProps> = ({ ...@@ -236,9 +241,9 @@ const Chart: React.FC<IChartProps> = ({
) )
} }
const getDefaultChartData = ({ start, end }: { start: string; end: string }) => { const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end: string; key?: string }) => {
const diffDays = dayjs(end).diff(dayjs(start), 'day') const diffDays = dayjs(end).diff(dayjs(start), 'day')
return Array.from({ length: diffDays || 1 }, () => ({ date: '', count: 0 })).map((item, index) => { return Array.from({ length: diffDays || 1 }, () => ({ date: '', [key]: 0 })).map((item, index) => {
item.date = dayjs(start).add(index, 'day').format(commonDateFormat) item.date = dayjs(start).add(index, 'day').format(commonDateFormat)
return item return item
}) })
...@@ -273,6 +278,55 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => { ...@@ -273,6 +278,55 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
/> />
} }
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.avgSessionInteractions.title'), explanation: t('appOverview.analysis.avgSessionInteractions.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'interactions' }) } as any}
chartType='conversations'
valueKey='interactions'
isAvg
{...(noDataFlag && { yMax: 500 })}
/>
}
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.avgResponseTime.title'), explanation: t('appOverview.analysis.avgResponseTime.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'latency' }) } as any}
valueKey='latency'
chartType='conversations'
isAvg
unit={t('appOverview.analysis.ms') as string}
{...(noDataFlag && { yMax: 500 })}
/>
}
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
basicInfo={{ title: t('appOverview.analysis.userSatisfactionRate.title'), explanation: t('appOverview.analysis.userSatisfactionRate.explanation'), timePeriod: period.name }}
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...period.query, key: 'rate' }) } as any}
valueKey='rate'
chartType='endUsers'
isAvg
{...(noDataFlag && { yMax: 1000 })}
/>
}
export const CostChart: FC<IBizChartProps> = ({ id, period }) => { export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation() const { t } = useTranslation()
......
...@@ -9,7 +9,7 @@ import Toast from '@/app/components/base/toast' ...@@ -9,7 +9,7 @@ import Toast from '@/app/components/base/toast'
import { Feedbacktype } from '@/app/components/app/chat' import { Feedbacktype } from '@/app/components/app/chat'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { fetcMoreLikeThis, updateFeedback } from '@/service/share' import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
const MAX_DEPTH = 3 const MAX_DEPTH = 3
export interface IGenerationItemProps { export interface IGenerationItemProps {
...@@ -24,6 +24,8 @@ export interface IGenerationItemProps { ...@@ -24,6 +24,8 @@ export interface IGenerationItemProps {
onFeedback?: (feedback: Feedbacktype) => void onFeedback?: (feedback: Feedbacktype) => void
onSave?: (messageId: string) => void onSave?: (messageId: string) => void
isMobile?: boolean isMobile?: boolean
isInstalledApp: boolean,
installedAppId?: string,
} }
export const SimpleBtn = ({ className, onClick, children }: { export const SimpleBtn = ({ className, onClick, children }: {
...@@ -75,7 +77,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -75,7 +77,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({
onFeedback, onFeedback,
onSave, onSave,
depth = 1, depth = 1,
isMobile isMobile,
isInstalledApp,
installedAppId,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const isTop = depth === 1 const isTop = depth === 1
...@@ -88,7 +92,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -88,7 +92,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}) })
const handleFeedback = async (childFeedback: Feedbacktype) => { const handleFeedback = async (childFeedback: Feedbacktype) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }) await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
setChildFeedback(childFeedback) setChildFeedback(childFeedback)
} }
...@@ -104,7 +108,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -104,7 +108,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({
isLoading: isQuerying, isLoading: isQuerying,
feedback: childFeedback, feedback: childFeedback,
onSave, onSave,
isMobile isMobile,
isInstalledApp,
installedAppId,
} }
const handleMoreLikeThis = async () => { const handleMoreLikeThis = async () => {
...@@ -113,7 +119,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ...@@ -113,7 +119,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
return return
} }
startQuerying() startQuerying()
const res: any = await fetcMoreLikeThis(messageId as string) const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
setCompletionRes(res.answer) setCompletionRes(res.answer)
setChildMessageId(res.id) setChildMessageId(res.id)
stopQuerying() stopQuerying()
......
'use client' 'use client'
import React, { FC } from 'react' import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { PlusIcon } from '@heroicons/react/24/outline' import { PlusIcon } from '@heroicons/react/24/outline'
export interface INoDataProps { import Button from '@/app/components/base/button'
export type INoDataProps = {
onStartCreateContent: () => void onStartCreateContent: () => void
} }
const markIcon = ( const markIcon = (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
) )
const lightIcon = ( const lightIcon = (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path></svg>
) )
const NoData: FC<INoDataProps> = ({ const NoData: FC<INoDataProps> = ({
onStartCreateContent onStartCreateContent,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
......
...@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({ ...@@ -39,7 +39,7 @@ const AppIcon: FC<AppIconProps> = ({
}} }}
onClick={onClick} onClick={onClick}
> >
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />} {innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id='🤖' />}
</span> </span>
) )
} }
......
...@@ -68,8 +68,11 @@ const BlockInput: FC<IBlockInputProps> = ({ ...@@ -68,8 +68,11 @@ const BlockInput: FC<IBlockInputProps> = ({
}) })
const coloredContent = (currentValue || '') const coloredContent = (currentValue || '')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>` .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
.replace(/\n/g, '<br />') .replace(/\n/g, '<br />')
// Not use useCallback. That will cause out callback get old data. // Not use useCallback. That will cause out callback get old data.
const handleSubmit = () => { const handleSubmit = () => {
......
...@@ -156,7 +156,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({ ...@@ -156,7 +156,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
</div> </div>
{/* Color Select */} {/* Color Select */}
<div className={cn('flex flex-col p-3 ', selectedEmoji == '' ? 'opacity-25' : '')}> <div className={cn('p-3 ', selectedEmoji == '' ? 'opacity-25' : '')}>
<p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p> <p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p>
<div className='w-full h-full grid grid-cols-8 gap-1'> <div className='w-full h-full grid grid-cols-8 gap-1'>
{backgroundColors.map((color) => { {backgroundColors.map((color) => {
......
...@@ -18,7 +18,7 @@ type InputProps = { ...@@ -18,7 +18,7 @@ type InputProps = {
const GlassIcon: FC<{ className?: string }> = ({ className }) => ( const GlassIcon: FC<{ className?: string }> = ({ className }) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M12.25 12.25L10.2084 10.2083M11.6667 6.70833C11.6667 9.44675 9.44675 11.6667 6.70833 11.6667C3.96992 11.6667 1.75 9.44675 1.75 6.70833C1.75 3.96992 3.96992 1.75 6.70833 1.75C9.44675 1.75 11.6667 3.96992 11.6667 6.70833Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" /> <path d="M12.25 12.25L10.2084 10.2083M11.6667 6.70833C11.6667 9.44675 9.44675 11.6667 6.70833 11.6667C3.96992 11.6667 1.75 9.44675 1.75 6.70833C1.75 3.96992 3.96992 1.75 6.70833 1.75C9.44675 1.75 11.6667 3.96992 11.6667 6.70833Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
) )
......
...@@ -12,6 +12,7 @@ type IModal = { ...@@ -12,6 +12,7 @@ type IModal = {
description?: React.ReactNode description?: React.ReactNode
children: React.ReactNode children: React.ReactNode
closable?: boolean closable?: boolean
overflowVisible?: boolean
} }
export default function Modal({ export default function Modal({
...@@ -23,6 +24,7 @@ export default function Modal({ ...@@ -23,6 +24,7 @@ export default function Modal({
description, description,
children, children,
closable = false, closable = false,
overflowVisible = false,
}: IModal) { }: IModal) {
return ( return (
<Transition appear show={isShow} as={Fragment}> <Transition appear show={isShow} as={Fragment}>
...@@ -50,7 +52,7 @@ export default function Modal({ ...@@ -50,7 +52,7 @@ export default function Modal({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className={`w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}> <Dialog.Panel className={`w-full max-w-md transform ${overflowVisible ? 'overflow-visible' : 'overflow-hidden'} rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}>
{title && <Dialog.Title {title && <Dialog.Title
as="h3" as="h3"
className="text-lg font-medium leading-6 text-gray-900" className="text-lg font-medium leading-6 text-gray-900"
......
import { FC, useCallback, useMemo, useState } from 'react' import type { FC } from 'react'
import React from 'react' import React, { useCallback, useMemo, useState } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
import cn from 'classnames' import cn from 'classnames'
import { ArrowRightIcon } from '@heroicons/react/24/solid'
import SegmentCard from '../completed/SegmentCard'
import { FieldInfo } from '../metadata'
import style from '../completed/style.module.css'
import { DocumentContext } from '../index'
import s from './style.module.css'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets' import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
import type { CommonResponse } from '@/models/common' import type { CommonResponse } from '@/models/common'
import { asyncRunSafe } from '@/utils' import { asyncRunSafe } from '@/utils'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
import { fetchProcessRule, fetchIndexingEstimate, fetchIndexingStatus, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets' import { fetchIndexingEstimate, fetchIndexingStatus, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
import SegmentCard from '../completed/SegmentCard'
import { FieldInfo } from '../metadata'
import s from './style.module.css'
import style from '../completed/style.module.css'
import { DocumentContext } from '../index'
import DatasetDetailContext from '@/context/dataset-detail' import DatasetDetailContext from '@/context/dataset-detail'
import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal' import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'
import { ArrowRightIcon } from '@heroicons/react/24/solid'
type Props = { type Props = {
detail?: FullDocumentDetail detail?: FullDocumentDetail
...@@ -35,7 +34,7 @@ type Props = { ...@@ -35,7 +34,7 @@ type Props = {
const StopIcon: FC<{ className?: string }> = ({ className }) => { const StopIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<g clip-path="url(#clip0_2328_2798)"> <g clip-path="url(#clip0_2328_2798)">
<path d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V8.1C10.5 8.94008 10.5 9.36012 10.3365 9.68099C10.1927 9.96323 9.96323 10.1927 9.68099 10.3365C9.36012 10.5 8.94008 10.5 8.1 10.5H3.9C3.05992 10.5 2.63988 10.5 2.31901 10.3365C2.03677 10.1927 1.8073 9.96323 1.66349 9.68099C1.5 9.36012 1.5 8.94008 1.5 8.1V3.9Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V8.1C10.5 8.94008 10.5 9.36012 10.3365 9.68099C10.1927 9.96323 9.96323 10.1927 9.68099 10.3365C9.36012 10.5 8.94008 10.5 8.1 10.5H3.9C3.05992 10.5 2.63988 10.5 2.31901 10.3365C2.03677 10.1927 1.8073 9.96323 1.66349 9.68099C1.5 9.36012 1.5 8.94008 1.5 8.1V3.9Z" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</g> </g>
<defs> <defs>
<clipPath id="clip0_2328_2798"> <clipPath id="clip0_2328_2798">
...@@ -47,9 +46,8 @@ const StopIcon: FC<{ className?: string }> = ({ className }) => { ...@@ -47,9 +46,8 @@ const StopIcon: FC<{ className?: string }> = ({ className }) => {
const ResumeIcon: FC<{ className?: string }> = ({ className }) => { const ResumeIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M10 3.5H5C3.34315 3.5 2 4.84315 2 6.5C2 8.15685 3.34315 9.5 5 9.5H10M10 3.5L8 1.5M10 3.5L8 5.5" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M10 3.5H5C3.34315 3.5 2 4.84315 2 6.5C2 8.15685 3.34315 9.5 5 9.5H10M10 3.5L8 1.5M10 3.5L8 5.5" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
} }
const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ({ sourceData, docName }) => { const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ({ sourceData, docName }) => {
...@@ -61,43 +59,43 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ( ...@@ -61,43 +59,43 @@ const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = (
segmentLength: t('datasetDocuments.embedding.segmentLength'), segmentLength: t('datasetDocuments.embedding.segmentLength'),
textCleaning: t('datasetDocuments.embedding.textCleaning'), textCleaning: t('datasetDocuments.embedding.textCleaning'),
} }
const getRuleName = (key: string) => {
if (key === 'remove_extra_spaces')
return t('datasetCreation.stepTwo.removeExtraSpaces')
if (key === 'remove_urls_emails')
return t('datasetCreation.stepTwo.removeUrlEmails')
if (key === 'remove_stopwords')
return t('datasetCreation.stepTwo.removeStopwords')
}
const getValue = useCallback((field: string) => { const getValue = useCallback((field: string) => {
let value: string | number | undefined = '-'; let value: string | number | undefined = '-'
switch (field) { switch (field) {
case 'docName': case 'docName':
value = docName value = docName
break; break
case 'mode': case 'mode':
value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string); value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)
break; break
case 'segmentLength': case 'segmentLength':
value = sourceData?.rules?.segmentation?.max_tokens value = sourceData?.rules?.segmentation?.max_tokens
break; break
default: default:
value = sourceData?.mode === 'automatic' ? value = sourceData?.mode === 'automatic'
(t('datasetDocuments.embedding.automatic') as string) : ? (t('datasetDocuments.embedding.automatic') as string)
sourceData?.rules?.pre_processing_rules?.map(rule => { // eslint-disable-next-line array-callback-return
if (rule.enabled) { : sourceData?.rules?.pre_processing_rules?.map((rule) => {
if (rule.enabled)
return getRuleName(rule.id) return getRuleName(rule.id)
}
}).filter(Boolean).join(';') }).filter(Boolean).join(';')
break; break
} }
return value return value
}, [sourceData, docName]) }, [sourceData, docName])
const getRuleName = (key: string) => {
if (key === 'remove_extra_spaces') {
return t('datasetCreation.stepTwo.removeExtraSpaces')
}
if (key === 'remove_urls_emails') {
return t('datasetCreation.stepTwo.removeUrlEmails')
}
if (key === 'remove_stopwords') {
return t('datasetCreation.stepTwo.removeStopwords')
}
}
return <div className='flex flex-col pt-8 pb-10 first:mt-0'> return <div className='flex flex-col pt-8 pb-10 first:mt-0'>
{Object.keys(segmentationRuleMap).map((field) => { {Object.keys(segmentationRuleMap).map((field) => {
return <FieldInfo return <FieldInfo
...@@ -134,12 +132,12 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d ...@@ -134,12 +132,12 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
datasetId: localDatasetId, datasetId: localDatasetId,
documentId: localDocumentId, documentId: localDocumentId,
}, apiParams => fetchIndexingEstimate(omit(apiParams, 'action')), { }, apiParams => fetchIndexingEstimate(omit(apiParams, 'action')), {
revalidateOnFocus: false revalidateOnFocus: false,
}) })
const { data: ruleDetail, error: ruleError } = useSWR({ const { data: ruleDetail, error: ruleError } = useSWR({
action: 'fetchProcessRule', action: 'fetchProcessRule',
params: { documentId: localDocumentId } params: { documentId: localDocumentId },
}, apiParams => fetchProcessRule(omit(apiParams, 'action')), { }, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
revalidateOnFocus: false, revalidateOnFocus: false,
}) })
...@@ -159,7 +157,8 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d ...@@ -159,7 +157,8 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
const percent = useMemo(() => { const percent = useMemo(() => {
const completedCount = indexingStatusDetail?.completed_segments || 0 const completedCount = indexingStatusDetail?.completed_segments || 0
const totalCount = indexingStatusDetail?.total_segments || 0 const totalCount = indexingStatusDetail?.total_segments || 0
if (totalCount === 0) return 0 if (totalCount === 0)
return 0
const percent = Math.round(completedCount * 100 / totalCount) const percent = Math.round(completedCount * 100 / totalCount)
return percent > 100 ? 100 : percent return percent > 100 ? 100 : percent
}, [indexingStatusDetail]) }, [indexingStatusDetail])
...@@ -170,7 +169,8 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d ...@@ -170,7 +169,8 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
if (!e) { if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
statusMutate() statusMutate()
} else { }
else {
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') }) notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
} }
} }
...@@ -211,7 +211,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d ...@@ -211,7 +211,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
(isEmbeddingPaused || isEmbeddingError) && s.barPaused, (isEmbeddingPaused || isEmbeddingError) && s.barPaused,
indexingStatusDetail?.indexing_status === 'completed' && 'rounded-r-md') indexingStatusDetail?.indexing_status === 'completed' && 'rounded-r-md')
} }
style={{ width: `${percent}%` }} style={{ width: `${percent}%` }}
/> />
</div> </div>
<div className={s.progressData}> <div className={s.progressData}>
...@@ -255,7 +255,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d ...@@ -255,7 +255,7 @@ const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: d
<Divider /> <Divider />
<div className={s.previewTip}>{t('datasetDocuments.embedding.previewTip')}</div> <div className={s.previewTip}>{t('datasetDocuments.embedding.previewTip')}</div>
<div className={style.cardWrapper}> <div className={style.cardWrapper}>
{[1, 2, 3].map((v) => ( {[1, 2, 3].map(v => (
<SegmentCard loading={true} detail={{ position: v } as any} /> <SegmentCard loading={true} detail={{ position: v } as any} />
))} ))}
</div> </div>
......
'use client' 'use client'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard' import copy from 'copy-to-clipboard'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { t } from 'i18next' import { t } from 'i18next'
import s from './style.module.css' import s from './style.module.css'
...@@ -18,7 +18,6 @@ const InputCopy = ({ ...@@ -18,7 +18,6 @@ const InputCopy = ({
readOnly = true, readOnly = true,
children, children,
}: IInputCopyProps) => { }: IInputCopyProps) => {
const [_, copy] = useCopyToClipboard()
const [isCopied, setIsCopied] = useState(false) const [isCopied, setIsCopied] = useState(false)
useEffect(() => { useEffect(() => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment