Merge branch 'main' into feat/workflow
|
|
@ -27,7 +27,7 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
**Dify** is an LLM application development platform that has helped built over **100,000** applications. It integrates BaaS and LLMOps, covering the essential tech stack for building generative AI-native applications, including a built-in RAG engine. Dify allows you to **deploy your own version of Assistants API and GPTs, based on any LLMs.**
|
||||
**Dify** is an open-source LLM app development platform. Dify's intuitive interface combines a RAG pipeline, AI workflow orchestration, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production.
|
||||
|
||||

|
||||
|
||||
|
|
@ -122,6 +122,9 @@ For those who'd like to contribute code, see our [Contribution Guide](https://gi
|
|||
|
||||
At the same time, please consider supporting Dify by sharing it on social media and at events and conferences.
|
||||
|
||||
### Projects made by community
|
||||
|
||||
- [Chatbot Chrome Extension by @charli117](https://github.com/langgenius/chatbot-chrome-extension)
|
||||
|
||||
### Contributors
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ from controllers.console import api
|
|||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import InvalidActionError, NoFileUploadedError, TooManyFilesError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
)
|
||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.model_manager import ModelManager
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
|
|
@ -207,6 +211,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
|||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
@cloud_edition_billing_knowledge_limit_check('add_segment')
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
|
|
@ -357,6 +362,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
|||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
@cloud_edition_billing_knowledge_limit_check('add_segment')
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
|
|
|
|||
|
|
@ -51,14 +51,12 @@ def cloud_edition_billing_resource_check(resource: str,
|
|||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
features = FeatureService.get_features(current_user.current_tenant_id)
|
||||
|
||||
if features.billing.enabled:
|
||||
members = features.members
|
||||
apps = features.apps
|
||||
vector_space = features.vector_space
|
||||
documents_upload_quota = features.documents_upload_quota
|
||||
annotation_quota_limit = features.annotation_quota_limit
|
||||
|
||||
if resource == 'members' and 0 < members.limit <= members.size:
|
||||
abort(403, error_msg)
|
||||
elif resource == 'apps' and 0 < apps.limit <= apps.size:
|
||||
|
|
@ -80,7 +78,29 @@ def cloud_edition_billing_resource_check(resource: str,
|
|||
return view(*args, **kwargs)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_edition_billing_knowledge_limit_check(resource: str,
|
||||
error_msg: str = "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan."):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
features = FeatureService.get_features(current_user.current_tenant_id)
|
||||
if features.billing.enabled:
|
||||
if resource == 'add_segment':
|
||||
if features.billing.subscription.plan == 'sandbox':
|
||||
abort(403, error_msg)
|
||||
else:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
|
||||
|
|
@ -99,4 +119,5 @@ def cloud_utm_record(view):
|
|||
except Exception as e:
|
||||
pass
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ from werkzeug.exceptions import NotFound
|
|||
|
||||
from controllers.service_api import api
|
||||
from controllers.service_api.app.error import ProviderNotInitializeError
|
||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
||||
from controllers.service_api.wraps import (
|
||||
DatasetApiResource,
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
)
|
||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.model_manager import ModelManager
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
|
|
@ -18,6 +22,7 @@ class SegmentApi(DatasetApiResource):
|
|||
"""Resource for segments."""
|
||||
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
@cloud_edition_billing_knowledge_limit_check('add_segment', 'dataset')
|
||||
def post(self, tenant_id, dataset_id, document_id):
|
||||
"""Create single segment."""
|
||||
# check dataset
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from flask import current_app, request
|
|||
from flask_login import user_logged_in
|
||||
from flask_restful import Resource
|
||||
from pydantic import BaseModel
|
||||
from werkzeug.exceptions import NotFound, Unauthorized
|
||||
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
|
||||
|
||||
from extensions.ext_database import db
|
||||
from libs.login import _get_user
|
||||
|
|
@ -92,13 +92,13 @@ def cloud_edition_billing_resource_check(resource: str,
|
|||
documents_upload_quota = features.documents_upload_quota
|
||||
|
||||
if resource == 'members' and 0 < members.limit <= members.size:
|
||||
raise Unauthorized(error_msg)
|
||||
raise Forbidden(error_msg)
|
||||
elif resource == 'apps' and 0 < apps.limit <= apps.size:
|
||||
raise Unauthorized(error_msg)
|
||||
raise Forbidden(error_msg)
|
||||
elif resource == 'vector_space' and 0 < vector_space.limit <= vector_space.size:
|
||||
raise Unauthorized(error_msg)
|
||||
raise Forbidden(error_msg)
|
||||
elif resource == 'documents' and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
||||
raise Unauthorized(error_msg)
|
||||
raise Forbidden(error_msg)
|
||||
else:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
|
|
@ -107,6 +107,27 @@ def cloud_edition_billing_resource_check(resource: str,
|
|||
return interceptor
|
||||
|
||||
|
||||
def cloud_edition_billing_knowledge_limit_check(resource: str,
|
||||
api_token_type: str,
|
||||
error_msg: str = "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan."):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
api_token = validate_and_get_api_token(api_token_type)
|
||||
features = FeatureService.get_features(api_token.tenant_id)
|
||||
if features.billing.enabled:
|
||||
if resource == 'add_segment':
|
||||
if features.billing.subscription.plan == 'sandbox':
|
||||
raise Forbidden(error_msg)
|
||||
else:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
def validate_dataset_token(view=None):
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from core.rag.datasource.entity.embedding import Embeddings
|
|||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs import helper
|
||||
from models.dataset import Embedding
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -23,32 +24,55 @@ class CacheEmbedding(Embeddings):
|
|||
|
||||
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
||||
"""Embed search docs in batches of 10."""
|
||||
text_embeddings = []
|
||||
try:
|
||||
model_type_instance = cast(TextEmbeddingModel, self._model_instance.model_type_instance)
|
||||
model_schema = model_type_instance.get_model_schema(self._model_instance.model, self._model_instance.credentials)
|
||||
max_chunks = model_schema.model_properties[ModelPropertyKey.MAX_CHUNKS] \
|
||||
if model_schema and ModelPropertyKey.MAX_CHUNKS in model_schema.model_properties else 1
|
||||
for i in range(0, len(texts), max_chunks):
|
||||
batch_texts = texts[i:i + max_chunks]
|
||||
# use doc embedding cache or store if not exists
|
||||
text_embeddings = [None for _ in range(len(texts))]
|
||||
embedding_queue_indices = []
|
||||
for i, text in enumerate(texts):
|
||||
hash = helper.generate_text_hash(text)
|
||||
embedding = db.session.query(Embedding).filter_by(model_name=self._model_instance.model,
|
||||
hash=hash,
|
||||
provider_name=self._model_instance.provider).first()
|
||||
if embedding:
|
||||
text_embeddings[i] = embedding.get_embedding()
|
||||
else:
|
||||
embedding_queue_indices.append(i)
|
||||
if embedding_queue_indices:
|
||||
embedding_queue_texts = [texts[i] for i in embedding_queue_indices]
|
||||
embedding_queue_embeddings = []
|
||||
try:
|
||||
model_type_instance = cast(TextEmbeddingModel, self._model_instance.model_type_instance)
|
||||
model_schema = model_type_instance.get_model_schema(self._model_instance.model, self._model_instance.credentials)
|
||||
max_chunks = model_schema.model_properties[ModelPropertyKey.MAX_CHUNKS] \
|
||||
if model_schema and ModelPropertyKey.MAX_CHUNKS in model_schema.model_properties else 1
|
||||
for i in range(0, len(embedding_queue_texts), max_chunks):
|
||||
batch_texts = embedding_queue_texts[i:i + max_chunks]
|
||||
|
||||
embedding_result = self._model_instance.invoke_text_embedding(
|
||||
texts=batch_texts,
|
||||
user=self._user
|
||||
)
|
||||
embedding_result = self._model_instance.invoke_text_embedding(
|
||||
texts=batch_texts,
|
||||
user=self._user
|
||||
)
|
||||
|
||||
for vector in embedding_result.embeddings:
|
||||
try:
|
||||
normalized_embedding = (vector / np.linalg.norm(vector)).tolist()
|
||||
text_embeddings.append(normalized_embedding)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
except Exception as e:
|
||||
logging.exception('Failed to add embedding to redis')
|
||||
|
||||
except Exception as ex:
|
||||
logger.error('Failed to embed documents: ', ex)
|
||||
raise ex
|
||||
for vector in embedding_result.embeddings:
|
||||
try:
|
||||
normalized_embedding = (vector / np.linalg.norm(vector)).tolist()
|
||||
embedding_queue_embeddings.append(normalized_embedding)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
except Exception as e:
|
||||
logging.exception('Failed transform embedding: ', e)
|
||||
for i, embedding in zip(embedding_queue_indices, embedding_queue_embeddings):
|
||||
text_embeddings[i] = embedding
|
||||
hash = helper.generate_text_hash(texts[i])
|
||||
embedding_cache = Embedding(model_name=self._model_instance.model,
|
||||
hash=hash,
|
||||
provider_name=self._model_instance.provider)
|
||||
embedding_cache.set_embedding(embedding)
|
||||
db.session.add(embedding_cache)
|
||||
db.session.commit()
|
||||
except Exception as ex:
|
||||
db.session.rollback()
|
||||
logger.error('Failed to embed documents: ', ex)
|
||||
raise ex
|
||||
|
||||
return text_embeddings
|
||||
|
||||
|
|
@ -61,8 +85,6 @@ class CacheEmbedding(Embeddings):
|
|||
if embedding:
|
||||
redis_client.expire(embedding_cache_key, 600)
|
||||
return list(np.frombuffer(base64.b64decode(embedding), dtype="float"))
|
||||
|
||||
|
||||
try:
|
||||
embedding_result = self._model_instance.invoke_text_embedding(
|
||||
texts=[text],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
- cohere
|
||||
- bedrock
|
||||
- togetherai
|
||||
- openrouter
|
||||
- ollama
|
||||
- mistralai
|
||||
- groq
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="25" height="21" viewBox="0 0 25 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.05858 10.1738C1.76158 10.1738 4.47988 9.56715 5.88589 8.77041C7.2919 7.97367 7.2919 7.97367 10.1977 5.91152C13.8766 3.30069 16.4779 4.17486 20.7428 4.17486" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4182 7.63145L11.3787 7.65951C8.50565 9.69845 8.42504 9.75566 6.92566 10.6053C5.98567 11.138 4.74704 11.5436 3.75151 11.8089C2.80313 12.0615 1.71203 12.2829 1.05858 12.2829V8.06483C1.05075 8.06483 1.05422 8.06445 1.06984 8.06276C1.11491 8.05788 1.26116 8.04203 1.52896 7.9926C1.84599 7.9341 2.24205 7.84582 2.6657 7.73296C3.55657 7.49564 4.3801 7.1996 4.84612 6.93552C4.88175 6.91533 4.91635 6.89573 4.95001 6.87666C6.15007 6.19693 6.15657 6.19325 8.97708 4.1916C12.5199 1.67735 15.5815 1.83587 18.5849 1.99138C19.3056 2.0287 20.0229 2.06584 20.7428 2.06584V6.28388C19.6102 6.28388 18.6583 6.24193 17.8263 6.20527C15.1245 6.08621 13.685 6.02278 11.4182 7.63145Z" fill="black"/>
|
||||
<path d="M24.8671 4.20087L17.6613 8.36117V0.0405881L24.8671 4.20087Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6378 0L24.9139 4.20087L17.6378 8.40176V0ZM17.6847 0.0811762V8.32058L24.8202 4.20087L17.6847 0.0811762Z" fill="black"/>
|
||||
<path d="M0.917975 10.1764C1.62098 10.1764 4.33927 10.7831 5.74529 11.5799C7.1513 12.3766 7.1513 12.3766 10.0571 14.4388C13.736 17.0496 16.3373 16.1754 20.6022 16.1754" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.929234 12.2875C0.913615 12.2858 0.910145 12.2854 0.917975 12.2854V8.06741C1.57142 8.06741 2.66253 8.28878 3.61091 8.54142C4.60644 8.80663 5.84507 9.21231 6.78506 9.74497C8.28444 10.5946 8.36505 10.6518 11.2381 12.6908L11.2776 12.7188C13.5444 14.3275 14.9839 14.2641 17.6857 14.145C18.5177 14.1083 19.4696 14.0664 20.6022 14.0664V18.2844C19.8823 18.2844 19.165 18.3216 18.4443 18.3589C15.4409 18.5144 12.3793 18.6729 8.83648 16.1587C6.01597 14.157 6.00947 14.1533 4.80941 13.4736C4.77575 13.4545 4.74115 13.4349 4.70551 13.4148C4.2395 13.1507 3.41597 12.8546 2.5251 12.6173C2.10145 12.5045 1.70538 12.4162 1.38836 12.3577C1.12056 12.3083 0.974309 12.2924 0.929234 12.2875Z" fill="black"/>
|
||||
<path d="M24.7265 16.1494L17.5207 11.9892V20.3097L24.7265 16.1494Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4972 11.9486L24.7733 16.1494L17.4972 20.3503V11.9486ZM17.5441 12.0297V20.2691L24.6796 16.1494L17.5441 12.0297Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,46 @@
|
|||
from collections.abc import Generator
|
||||
from typing import Optional, Union
|
||||
|
||||
from core.model_runtime.entities.llm_entities import LLMResult
|
||||
from core.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool
|
||||
from core.model_runtime.entities.model_entities import AIModelEntity
|
||||
from core.model_runtime.model_providers.openai_api_compatible.llm.llm import OAIAPICompatLargeLanguageModel
|
||||
|
||||
|
||||
class OpenRouterLargeLanguageModel(OAIAPICompatLargeLanguageModel):
|
||||
|
||||
def _update_endpoint_url(self, credentials: dict):
|
||||
credentials['endpoint_url'] = "https://openrouter.ai/api/v1"
|
||||
return credentials
|
||||
|
||||
def _invoke(self, model: str, credentials: dict,
|
||||
prompt_messages: list[PromptMessage], model_parameters: dict,
|
||||
tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None,
|
||||
stream: bool = True, user: Optional[str] = None) \
|
||||
-> Union[LLMResult, Generator]:
|
||||
cred_with_endpoint = self._update_endpoint_url(credentials=credentials)
|
||||
|
||||
return super()._invoke(model, cred_with_endpoint, prompt_messages, model_parameters, tools, stop, stream, user)
|
||||
|
||||
def validate_credentials(self, model: str, credentials: dict) -> None:
|
||||
cred_with_endpoint = self._update_endpoint_url(credentials=credentials)
|
||||
|
||||
return super().validate_credentials(model, cred_with_endpoint)
|
||||
|
||||
def _generate(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict,
|
||||
tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None,
|
||||
stream: bool = True, user: Optional[str] = None) -> Union[LLMResult, Generator]:
|
||||
cred_with_endpoint = self._update_endpoint_url(credentials=credentials)
|
||||
|
||||
return super()._generate(model, cred_with_endpoint, prompt_messages, model_parameters, tools, stop, stream, user)
|
||||
|
||||
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity:
|
||||
cred_with_endpoint = self._update_endpoint_url(credentials=credentials)
|
||||
|
||||
return super().get_customizable_model_schema(model, cred_with_endpoint)
|
||||
|
||||
def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage],
|
||||
tools: Optional[list[PromptMessageTool]] = None) -> int:
|
||||
cred_with_endpoint = self._update_endpoint_url(credentials=credentials)
|
||||
|
||||
return super().get_num_tokens(model, cred_with_endpoint, prompt_messages, tools)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import logging
|
||||
|
||||
from core.model_runtime.model_providers.__base.model_provider import ModelProvider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenRouterProvider(ModelProvider):
|
||||
|
||||
def validate_provider_credentials(self, credentials: dict) -> None:
|
||||
pass
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
provider: openrouter
|
||||
label:
|
||||
en_US: openrouter.ai
|
||||
icon_small:
|
||||
en_US: openrouter_square.svg
|
||||
icon_large:
|
||||
en_US: openrouter.svg
|
||||
background: "#F1EFED"
|
||||
help:
|
||||
title:
|
||||
en_US: Get your API key from openrouter.ai
|
||||
zh_Hans: 从 openrouter.ai 获取 API Key
|
||||
url:
|
||||
en_US: https://openrouter.ai/keys
|
||||
supported_model_types:
|
||||
- llm
|
||||
configurate_methods:
|
||||
- customizable-model
|
||||
model_credential_schema:
|
||||
model:
|
||||
label:
|
||||
en_US: Model Name
|
||||
zh_Hans: 模型名称
|
||||
placeholder:
|
||||
en_US: Enter full model name
|
||||
zh_Hans: 输入模型全称
|
||||
credential_form_schemas:
|
||||
- variable: api_key
|
||||
required: true
|
||||
label:
|
||||
en_US: API Key
|
||||
type: secret-input
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的 API Key
|
||||
en_US: Enter your API Key
|
||||
- variable: mode
|
||||
show_on:
|
||||
- variable: __model_type
|
||||
value: llm
|
||||
label:
|
||||
en_US: Completion mode
|
||||
type: select
|
||||
required: false
|
||||
default: chat
|
||||
placeholder:
|
||||
zh_Hans: 选择对话类型
|
||||
en_US: Select completion mode
|
||||
options:
|
||||
- value: completion
|
||||
label:
|
||||
en_US: Completion
|
||||
zh_Hans: 补全
|
||||
- value: chat
|
||||
label:
|
||||
en_US: Chat
|
||||
zh_Hans: 对话
|
||||
- variable: context_size
|
||||
label:
|
||||
zh_Hans: 模型上下文长度
|
||||
en_US: Model context size
|
||||
required: true
|
||||
type: text-input
|
||||
default: "4096"
|
||||
placeholder:
|
||||
zh_Hans: 在此输入您的模型上下文长度
|
||||
en_US: Enter your Model context size
|
||||
- variable: max_tokens_to_sample
|
||||
label:
|
||||
zh_Hans: 最大 token 上限
|
||||
en_US: Upper bound for max tokens
|
||||
show_on:
|
||||
- variable: __model_type
|
||||
value: llm
|
||||
default: "4096"
|
||||
type: text-input
|
||||
|
|
@ -46,11 +46,11 @@ def init_app(app: Flask) -> Celery:
|
|||
beat_schedule = {
|
||||
'clean_embedding_cache_task': {
|
||||
'task': 'schedule.clean_embedding_cache_task.clean_embedding_cache_task',
|
||||
'schedule': timedelta(days=7),
|
||||
'schedule': timedelta(days=1),
|
||||
},
|
||||
'clean_unused_datasets_task': {
|
||||
'task': 'schedule.clean_unused_datasets_task.clean_unused_datasets_task',
|
||||
'schedule': timedelta(minutes=3),
|
||||
'schedule': timedelta(days=1),
|
||||
}
|
||||
}
|
||||
celery_app.conf.update(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
"""add-embeddings-provider-name
|
||||
|
||||
Revision ID: a8d7385a7b66
|
||||
Revises: 17b5ab037c40
|
||||
Create Date: 2024-04-02 12:17:22.641525
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a8d7385a7b66'
|
||||
down_revision = '17b5ab037c40'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('embeddings', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('provider_name', sa.String(length=40), server_default=sa.text("''::character varying"), nullable=False))
|
||||
batch_op.drop_constraint('embedding_hash_idx', type_='unique')
|
||||
batch_op.create_unique_constraint('embedding_hash_idx', ['model_name', 'hash', 'provider_name'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('embeddings', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('embedding_hash_idx', type_='unique')
|
||||
batch_op.create_unique_constraint('embedding_hash_idx', ['model_name', 'hash'])
|
||||
batch_op.drop_column('provider_name')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -123,6 +123,7 @@ class Dataset(db.Model):
|
|||
normalized_dataset_id = dataset_id.replace("-", "_")
|
||||
return f'Vector_index_{normalized_dataset_id}_Node'
|
||||
|
||||
|
||||
class DatasetProcessRule(db.Model):
|
||||
__tablename__ = 'dataset_process_rules'
|
||||
__table_args__ = (
|
||||
|
|
@ -443,7 +444,8 @@ class DatasetKeywordTable(db.Model):
|
|||
id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
|
||||
dataset_id = db.Column(UUID, nullable=False, unique=True)
|
||||
keyword_table = db.Column(db.Text, nullable=False)
|
||||
data_source_type = db.Column(db.String(255), nullable=False, server_default=db.text("'database'::character varying"))
|
||||
data_source_type = db.Column(db.String(255), nullable=False,
|
||||
server_default=db.text("'database'::character varying"))
|
||||
|
||||
@property
|
||||
def keyword_table_dict(self):
|
||||
|
|
@ -457,6 +459,7 @@ class DatasetKeywordTable(db.Model):
|
|||
if isinstance(node_idxs, list):
|
||||
dct[keyword] = set(node_idxs)
|
||||
return dct
|
||||
|
||||
# get dataset
|
||||
dataset = Dataset.query.filter_by(
|
||||
id=self.dataset_id
|
||||
|
|
@ -481,7 +484,7 @@ class Embedding(db.Model):
|
|||
__tablename__ = 'embeddings'
|
||||
__table_args__ = (
|
||||
db.PrimaryKeyConstraint('id', name='embedding_pkey'),
|
||||
db.UniqueConstraint('model_name', 'hash', name='embedding_hash_idx')
|
||||
db.UniqueConstraint('model_name', 'hash', 'provider_name', name='embedding_hash_idx')
|
||||
)
|
||||
|
||||
id = db.Column(UUID, primary_key=True, server_default=db.text('uuid_generate_v4()'))
|
||||
|
|
@ -490,6 +493,8 @@ class Embedding(db.Model):
|
|||
hash = db.Column(db.String(64), nullable=False)
|
||||
embedding = db.Column(db.LargeBinary, nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||
provider_name = db.Column(db.String(40), nullable=False,
|
||||
server_default=db.text("''::character varying"))
|
||||
|
||||
def set_embedding(self, embedding_data: list[float]):
|
||||
self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ yfinance~=0.2.35
|
|||
pydub~=0.25.1
|
||||
gmpy2~=2.1.5
|
||||
numexpr~=2.9.0
|
||||
duckduckgo-search==5.1.0
|
||||
duckduckgo-search==5.2.2
|
||||
arxiv==2.1.0
|
||||
yarl~=1.9.4
|
||||
twilio==9.0.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import os
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta
|
||||
from core.model_runtime.entities.message_entities import (AssistantPromptMessage, PromptMessageTool,
|
||||
SystemPromptMessage, UserPromptMessage)
|
||||
from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
||||
from core.model_runtime.model_providers.openrouter.llm.llm import OpenRouterLargeLanguageModel
|
||||
|
||||
|
||||
def test_validate_credentials():
|
||||
model = OpenRouterLargeLanguageModel()
|
||||
|
||||
with pytest.raises(CredentialsValidateFailedError):
|
||||
model.validate_credentials(
|
||||
model='mistralai/mixtral-8x7b-instruct',
|
||||
credentials={
|
||||
'api_key': 'invalid_key',
|
||||
'mode': 'chat'
|
||||
}
|
||||
)
|
||||
|
||||
model.validate_credentials(
|
||||
model='mistralai/mixtral-8x7b-instruct',
|
||||
credentials={
|
||||
'api_key': os.environ.get('TOGETHER_API_KEY'),
|
||||
'mode': 'chat'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_invoke_model():
|
||||
model = OpenRouterLargeLanguageModel()
|
||||
|
||||
response = model.invoke(
|
||||
model='mistralai/mixtral-8x7b-instruct',
|
||||
credentials={
|
||||
'api_key': os.environ.get('TOGETHER_API_KEY'),
|
||||
'mode': 'completion'
|
||||
},
|
||||
prompt_messages=[
|
||||
SystemPromptMessage(
|
||||
content='You are a helpful AI assistant.',
|
||||
),
|
||||
UserPromptMessage(
|
||||
content='Who are you?'
|
||||
)
|
||||
],
|
||||
model_parameters={
|
||||
'temperature': 1.0,
|
||||
'top_k': 2,
|
||||
'top_p': 0.5,
|
||||
},
|
||||
stop=['How'],
|
||||
stream=False,
|
||||
user="abc-123"
|
||||
)
|
||||
|
||||
assert isinstance(response, LLMResult)
|
||||
assert len(response.message.content) > 0
|
||||
|
||||
|
||||
def test_invoke_stream_model():
|
||||
model = OpenRouterLargeLanguageModel()
|
||||
|
||||
response = model.invoke(
|
||||
model='mistralai/mixtral-8x7b-instruct',
|
||||
credentials={
|
||||
'api_key': os.environ.get('TOGETHER_API_KEY'),
|
||||
'mode': 'chat'
|
||||
},
|
||||
prompt_messages=[
|
||||
SystemPromptMessage(
|
||||
content='You are a helpful AI assistant.',
|
||||
),
|
||||
UserPromptMessage(
|
||||
content='Who are you?'
|
||||
)
|
||||
],
|
||||
model_parameters={
|
||||
'temperature': 1.0,
|
||||
'top_k': 2,
|
||||
'top_p': 0.5,
|
||||
},
|
||||
stop=['How'],
|
||||
stream=True,
|
||||
user="abc-123"
|
||||
)
|
||||
|
||||
assert isinstance(response, Generator)
|
||||
|
||||
for chunk in response:
|
||||
assert isinstance(chunk, LLMResultChunk)
|
||||
assert isinstance(chunk.delta, LLMResultChunkDelta)
|
||||
assert isinstance(chunk.delta.message, AssistantPromptMessage)
|
||||
|
||||
|
||||
def test_get_num_tokens():
|
||||
model = OpenRouterLargeLanguageModel()
|
||||
|
||||
num_tokens = model.get_num_tokens(
|
||||
model='mistralai/mixtral-8x7b-instruct',
|
||||
credentials={
|
||||
'api_key': os.environ.get('TOGETHER_API_KEY'),
|
||||
},
|
||||
prompt_messages=[
|
||||
SystemPromptMessage(
|
||||
content='You are a helpful AI assistant.',
|
||||
),
|
||||
UserPromptMessage(
|
||||
content='Hello World!'
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert isinstance(num_tokens, int)
|
||||
assert num_tokens == 21
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
## Chrome Dify ChatBot插件
|
||||
|
||||
### 方式1:Chrome插件商店 * [点击访问](https://chrome.google.com/webstore/detail/dify-chatbot/ceehdapohffmjmkdcifjofadiaoeggaf/related?hl=zh-CN&authuser=0) *
|
||||
|
||||
### 方式2:本地开发者模式加载
|
||||
|
||||
- 进入Chrome浏览器管理扩展程序,可直接访问 [chrome://extensions/](chrome://extensions/)
|
||||
- 选择开启 “开发者模式”,并点击 “加载已解压的扩展程序”
|
||||
|
||||

|
||||
|
||||
- 然后打开插件源文件所在根目录
|
||||
- third-party
|
||||
- chrome plug-in
|
||||
- content.js 浮动按钮JS脚本
|
||||
- favicon.png 插件图标
|
||||
- manifest.json 插件描述文件
|
||||
- options.css 插件配置页面样式文件
|
||||
- options.html 插件配置静态HTML页面
|
||||
- options.js 插件配置JS脚本
|
||||
|
||||
### 插件导入完成后,后续配置无差异
|
||||
- 创建Dify应用配置,在应用概览中点击嵌入,切换到安装Chrome浏览器扩展视图,点击copy按钮获取ChatBot Url,如图:
|
||||
|
||||

|
||||
- 点击保存,确认提示配置成功即可
|
||||
|
||||

|
||||
|
||||
- 保险起见重启浏览器确保所有分页刷新成功
|
||||
- Chrome打开任意页面均可正常加载DIfy机器人浮动栏,后续如需更换机器人只需要变更ChatBot Url即可
|
||||
|
||||

|
||||
|
|
@ -1,6 +0,0 @@
|
|||
## Chrome Dify ChatBot插件
|
||||
|
||||
1、初始化设置Dify 应用配置,分别输入Dify根域名和应用Token,Token可以在Dify应用嵌入中获取;
|
||||
2、点击保存,确认提示配置成功即可;
|
||||
3、保险起见重启浏览器确保所有分页刷新成功;
|
||||
4、Chrome打开任意页面均可正常加载DIfy机器人浮动栏,后续如需更换机器人只需要变更Token即可;
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
const storage = chrome.storage.sync;
|
||||
chrome.storage.sync.get(['chatbotUrl'], function(result) {
|
||||
window.difyChatbotConfig = {
|
||||
chatbotUrl: result.chatbotUrl,
|
||||
};
|
||||
});
|
||||
|
||||
document.body.onload = embedChatbot;
|
||||
|
||||
async function embedChatbot() {
|
||||
const difyChatbotConfig = window.difyChatbotConfig;
|
||||
if (!difyChatbotConfig) {
|
||||
console.warn('Dify Chatbot Url is empty or is not provided');
|
||||
return;
|
||||
}
|
||||
const openIcon = `<svg
|
||||
id="openIcon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>`;
|
||||
const closeIcon = `<svg
|
||||
id="closeIcon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 18L6 6M6 18L18 6"
|
||||
stroke="white"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
// create iframe
|
||||
function createIframe() {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.allow = "fullscreen;microphone"
|
||||
iframe.title = "dify chatbot bubble window"
|
||||
iframe.id = 'dify-chatbot-bubble-window'
|
||||
iframe.src = difyChatbotConfig.chatbotUrl
|
||||
iframe.style.cssText = 'border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 6.7rem; right: 1rem; width: 30rem; height: 48rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;'
|
||||
document.body.appendChild(iframe);
|
||||
}
|
||||
|
||||
/**
|
||||
* rem to px
|
||||
* @param {*} rem :30rem
|
||||
*/
|
||||
function handleRemToPx(rem) {
|
||||
if (!rem) return;
|
||||
let pxValue = 0;
|
||||
try {
|
||||
const regex = /\d+/;
|
||||
// extract the numeric part and convert it to a numeric type
|
||||
const remValue = parseInt(regex.exec(rem)[0], 10);
|
||||
const rootFontSize = parseFloat(
|
||||
window.getComputedStyle(document.documentElement).fontSize
|
||||
);
|
||||
pxValue = remValue * rootFontSize;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return pxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* support element drag
|
||||
* @param {*} targetButton entry element
|
||||
*/
|
||||
function handleElementDrag(targetButton) {
|
||||
// define a variable to hold the mouse position
|
||||
let mouseX = 0,
|
||||
mouseY = 0,
|
||||
offsetX = 0,
|
||||
offsetY = 0;
|
||||
|
||||
// Listen for mouse press events, get mouse position and element position
|
||||
targetButton.addEventListener("mousedown", function (event) {
|
||||
// calculate mouse position
|
||||
mouseX = event.clientX;
|
||||
mouseY = event.clientY;
|
||||
|
||||
// calculate element position
|
||||
const rect = targetButton.getBoundingClientRect();
|
||||
offsetX = mouseX - rect.left;
|
||||
offsetY = mouseY - rect.top;
|
||||
|
||||
// listen for mouse movement events
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
});
|
||||
|
||||
// listen for mouse lift events and stop listening for mouse move events
|
||||
document.addEventListener("mouseup", function () {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
});
|
||||
|
||||
// the mouse moves the event handler to update the element position
|
||||
function onMouseMove(event) {
|
||||
// calculate element position
|
||||
let newX = event.clientX - offsetX,
|
||||
newY = event.clientY - offsetY;
|
||||
|
||||
// 计算视线边界
|
||||
const viewportWidth = window.innerWidth,
|
||||
viewportHeight = window.innerHeight;
|
||||
|
||||
const maxX = viewportWidth - targetButton.offsetWidth,
|
||||
maxY = viewportHeight - targetButton.offsetHeight;
|
||||
|
||||
// application limitation
|
||||
newX = Math.max(12, Math.min(newX, maxX));
|
||||
newY = Math.max(12, Math.min(newY, maxY));
|
||||
|
||||
// update element position
|
||||
targetButton.style.left = newX + "px";
|
||||
targetButton.style.top = newY + "px";
|
||||
}
|
||||
}
|
||||
|
||||
const targetButton = document.getElementById("dify-chatbot-bubble-button");
|
||||
|
||||
if (!targetButton) {
|
||||
// create button
|
||||
const containerDiv = document.createElement("div");
|
||||
containerDiv.id = 'dify-chatbot-bubble-button';
|
||||
containerDiv.style.cssText = `position: fixed; bottom: 3rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: move; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}`;
|
||||
const displayDiv = document.createElement('div');
|
||||
displayDiv.style.cssText = "display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;";
|
||||
displayDiv.innerHTML = openIcon;
|
||||
containerDiv.appendChild(displayDiv);
|
||||
document.body.appendChild(containerDiv);
|
||||
handleElementDrag(containerDiv);
|
||||
|
||||
// add click event to control iframe display
|
||||
containerDiv.addEventListener('click', function () {
|
||||
const targetIframe = document.getElementById('dify-chatbot-bubble-window');
|
||||
if (!targetIframe) {
|
||||
createIframe();
|
||||
displayDiv.innerHTML = closeIcon;
|
||||
return;
|
||||
}
|
||||
if (targetIframe.style.display === "none") {
|
||||
targetIframe.style.display = "block";
|
||||
displayDiv.innerHTML = closeIcon;
|
||||
} else {
|
||||
targetIframe.style.display = "none";
|
||||
displayDiv.innerHTML = openIcon;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// add any drag and drop to the floating icon
|
||||
handleElementDrag(targetButton);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Dify Chatbot",
|
||||
"version": "1.5",
|
||||
"description": "This is a chrome extension to inject a dify chatbot on any pages",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"permissions": ["webRequest", "storage"],
|
||||
"action": {
|
||||
"default_popup": "options.html",
|
||||
"default_icon": {
|
||||
"16": "images/16.png",
|
||||
"32": "images/32.png",
|
||||
"48": "images/48.png",
|
||||
"128": "images/128.png"
|
||||
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "images/16.png",
|
||||
"32": "images/32.png",
|
||||
"48": "images/48.png",
|
||||
"128": "images/128.png"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
body {
|
||||
background-color: #f2f2f2;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 280px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Dify Chatbot Extension</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="./tailwind.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 py-4 px-4 w-128">
|
||||
<div class="max-w-md mx-auto bg-white shadow-md rounded-lg p-4">
|
||||
<h2 class="text-2xl font-semibold mb-4">Dify Chatbot Extension</h2>
|
||||
<form>
|
||||
<div class="mb-4 flex items-center">
|
||||
<div class="w-1/4">
|
||||
<label for="chatbot-url" class="block font-semibold text-gray-700">ChatBot URL</label>
|
||||
</div>
|
||||
<div class="w-3/4">
|
||||
<input type="text" id="chatbot-url" name="base-url" value=""
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:border-blue-400"
|
||||
placeholder="https://udify.app/chatbot/7CQBa5yyvYLSkZtx">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center">
|
||||
<div class="w-1/4"></div>
|
||||
<div class="w-3/4">
|
||||
<span id="error-tip" class="text-red-600"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center">
|
||||
<div class="w-1/4"></div>
|
||||
<div class="w-3/4">
|
||||
<button id="save-button"
|
||||
class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:bg-blue-600">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
document.getElementById('save-button').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const chatbotUrl = document.getElementById('chatbot-url').value;
|
||||
const errorTip = document.getElementById('error-tip');
|
||||
|
||||
if (chatbotUrl.trim() === "") {
|
||||
errorTip.textContent = "Dify ChatBot URL cannot be empty.";
|
||||
} else {
|
||||
errorTip.textContent = "";
|
||||
|
||||
chrome.storage.sync.set({
|
||||
'chatbotUrl': chatbotUrl,
|
||||
}, function () {
|
||||
alert('Save Success!');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load parameters from chrome.storage when the page loads
|
||||
chrome.storage.sync.get(['chatbotUrl'], function (result) {
|
||||
const chatbotUrlInput = document.getElementById('chatbot-url');
|
||||
|
||||
if (result.chatbotUrl) {
|
||||
chatbotUrlInput.value = result.chatbotUrl;
|
||||
}
|
||||
|
||||
});
|
||||