mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Compare commits
8 Commits
4c8f9f0d77
...
f0a14f5fce
| Author | SHA1 | Date | |
|---|---|---|---|
| f0a14f5fce | |||
| 174a2578e8 | |||
| a0959b9d38 | |||
| 13299197b8 | |||
| 249296e417 | |||
| db0f6840d9 | |||
| 1033a3ae26 | |||
| 1845daf41f |
@ -86,7 +86,7 @@ Try our demo at [https://demo.ragflow.io](https://demo.ragflow.io).
|
|||||||
## 🔥 Latest Updates
|
## 🔥 Latest Updates
|
||||||
|
|
||||||
- 2025-11-19 Supports Gemini 3 Pro.
|
- 2025-11-19 Supports Gemini 3 Pro.
|
||||||
- 2025-11-12 Supports data synchronization from Confluence, AWS S3, Discord, Google Drive.
|
- 2025-11-12 Supports data synchronization from Confluence, S3, Notion, Discord, Google Drive.
|
||||||
- 2025-10-23 Supports MinerU & Docling as document parsing methods.
|
- 2025-10-23 Supports MinerU & Docling as document parsing methods.
|
||||||
- 2025-10-15 Supports orchestrable ingestion pipeline.
|
- 2025-10-15 Supports orchestrable ingestion pipeline.
|
||||||
- 2025-08-08 Supports OpenAI's latest GPT-5 series models.
|
- 2025-08-08 Supports OpenAI's latest GPT-5 series models.
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
|
|||||||
## 🔥 Pembaruan Terbaru
|
## 🔥 Pembaruan Terbaru
|
||||||
|
|
||||||
- 2025-11-19 Mendukung Gemini 3 Pro.
|
- 2025-11-19 Mendukung Gemini 3 Pro.
|
||||||
- 2025-11-12 Mendukung sinkronisasi data dari Confluence, AWS S3, Discord, Google Drive.
|
- 2025-11-12 Mendukung sinkronisasi data dari Confluence, S3, Notion, Discord, Google Drive.
|
||||||
- 2025-10-23 Mendukung MinerU & Docling sebagai metode penguraian dokumen.
|
- 2025-10-23 Mendukung MinerU & Docling sebagai metode penguraian dokumen.
|
||||||
- 2025-10-15 Dukungan untuk jalur data yang terorkestrasi.
|
- 2025-10-15 Dukungan untuk jalur data yang terorkestrasi.
|
||||||
- 2025-08-08 Mendukung model seri GPT-5 terbaru dari OpenAI.
|
- 2025-08-08 Mendukung model seri GPT-5 terbaru dari OpenAI.
|
||||||
|
|||||||
@ -67,7 +67,7 @@
|
|||||||
## 🔥 最新情報
|
## 🔥 最新情報
|
||||||
|
|
||||||
- 2025-11-19 Gemini 3 Proをサポートしています
|
- 2025-11-19 Gemini 3 Proをサポートしています
|
||||||
- 2025-11-12 Confluence、AWS S3、Discord、Google Drive からのデータ同期をサポートします。
|
- 2025-11-12 Confluence、S3、Notion、Discord、Google Drive からのデータ同期をサポートします。
|
||||||
- 2025-10-23 ドキュメント解析方法として MinerU と Docling をサポートします。
|
- 2025-10-23 ドキュメント解析方法として MinerU と Docling をサポートします。
|
||||||
- 2025-10-15 オーケストレーションされたデータパイプラインのサポート。
|
- 2025-10-15 オーケストレーションされたデータパイプラインのサポート。
|
||||||
- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。
|
- 2025-08-08 OpenAI の最新 GPT-5 シリーズモデルをサポートします。
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
## 🔥 업데이트
|
## 🔥 업데이트
|
||||||
|
|
||||||
- 2025-11-19 Gemini 3 Pro를 지원합니다.
|
- 2025-11-19 Gemini 3 Pro를 지원합니다.
|
||||||
- 2025-11-12 Confluence, AWS S3, Discord, Google Drive에서 데이터 동기화를 지원합니다.
|
- 2025-11-12 Confluence, S3, Notion, Discord, Google Drive에서 데이터 동기화를 지원합니다.
|
||||||
- 2025-10-23 문서 파싱 방법으로 MinerU 및 Docling을 지원합니다.
|
- 2025-10-23 문서 파싱 방법으로 MinerU 및 Docling을 지원합니다.
|
||||||
- 2025-10-15 조정된 데이터 파이프라인 지원.
|
- 2025-10-15 조정된 데이터 파이프라인 지원.
|
||||||
- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.
|
- 2025-08-08 OpenAI의 최신 GPT-5 시리즈 모델을 지원합니다.
|
||||||
|
|||||||
@ -87,7 +87,7 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
|
|||||||
## 🔥 Últimas Atualizações
|
## 🔥 Últimas Atualizações
|
||||||
|
|
||||||
- 19-11-2025 Suporta Gemini 3 Pro.
|
- 19-11-2025 Suporta Gemini 3 Pro.
|
||||||
- 12-11-2025 Suporta a sincronização de dados do Confluence, AWS S3, Discord e Google Drive.
|
- 12-11-2025 Suporta a sincronização de dados do Confluence, S3, Notion, Discord e Google Drive.
|
||||||
- 23-10-2025 Suporta MinerU e Docling como métodos de análise de documentos.
|
- 23-10-2025 Suporta MinerU e Docling como métodos de análise de documentos.
|
||||||
- 15-10-2025 Suporte para pipelines de dados orquestrados.
|
- 15-10-2025 Suporte para pipelines de dados orquestrados.
|
||||||
- 08-08-2025 Suporta a mais recente série GPT-5 da OpenAI.
|
- 08-08-2025 Suporta a mais recente série GPT-5 da OpenAI.
|
||||||
|
|||||||
@ -86,7 +86,7 @@
|
|||||||
## 🔥 近期更新
|
## 🔥 近期更新
|
||||||
|
|
||||||
- 2025-11-19 支援 Gemini 3 Pro.
|
- 2025-11-19 支援 Gemini 3 Pro.
|
||||||
- 2025-11-12 支援從 Confluence、AWS S3、Discord、Google Drive 進行資料同步。
|
- 2025-11-12 支援從 Confluence、S3、Notion、Discord、Google Drive 進行資料同步。
|
||||||
- 2025-10-23 支援 MinerU 和 Docling 作為文件解析方法。
|
- 2025-10-23 支援 MinerU 和 Docling 作為文件解析方法。
|
||||||
- 2025-10-15 支援可編排的資料管道。
|
- 2025-10-15 支援可編排的資料管道。
|
||||||
- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。
|
- 2025-08-08 支援 OpenAI 最新的 GPT-5 系列模型。
|
||||||
|
|||||||
@ -86,7 +86,7 @@
|
|||||||
## 🔥 近期更新
|
## 🔥 近期更新
|
||||||
|
|
||||||
- 2025-11-19 支持 Gemini 3 Pro.
|
- 2025-11-19 支持 Gemini 3 Pro.
|
||||||
- 2025-11-12 支持从 Confluence、AWS S3、Discord、Google Drive 进行数据同步。
|
- 2025-11-12 支持从 Confluence、S3、Notion、Discord、Google Drive 进行数据同步。
|
||||||
- 2025-10-23 支持 MinerU 和 Docling 作为文档解析方法。
|
- 2025-10-23 支持 MinerU 和 Docling 作为文档解析方法。
|
||||||
- 2025-10-15 支持可编排的数据管道。
|
- 2025-10-15 支持可编排的数据管道。
|
||||||
- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型。
|
- 2025-08-08 支持 OpenAI 最新的 GPT-5 系列模型。
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class IterationParam(ComponentParamBase):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.items_ref = ""
|
self.items_ref = ""
|
||||||
self.veriable={}
|
self.variable={}
|
||||||
|
|
||||||
def get_input_form(self) -> dict[str, dict]:
|
def get_input_form(self) -> dict[str, dict]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ from flasgger import Swagger
|
|||||||
from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
|
from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer
|
||||||
from quart_cors import cors
|
from quart_cors import cors
|
||||||
from common.constants import StatusEnum
|
from common.constants import StatusEnum
|
||||||
from api.db.db_models import close_connection
|
from api.db.db_models import close_connection, APIToken
|
||||||
from api.db.services import UserService
|
from api.db.services import UserService
|
||||||
from api.utils.json_encode import CustomJSONEncoder
|
from api.utils.json_encode import CustomJSONEncoder
|
||||||
from api.utils import commands
|
from api.utils import commands
|
||||||
@ -124,6 +124,10 @@ def _load_user():
|
|||||||
user = UserService.query(
|
user = UserService.query(
|
||||||
access_token=access_token, status=StatusEnum.VALID.value
|
access_token=access_token, status=StatusEnum.VALID.value
|
||||||
)
|
)
|
||||||
|
if not user and len(authorization.split()) == 2:
|
||||||
|
objs = APIToken.query(token=authorization.split()[1])
|
||||||
|
if objs:
|
||||||
|
user = UserService.query(id=objs[0].tenant_id, status=StatusEnum.VALID.value)
|
||||||
if user:
|
if user:
|
||||||
if not user[0].access_token or not user[0].access_token.strip():
|
if not user[0].access_token or not user[0].access_token.strip():
|
||||||
logging.warning(f"User {user[0].email} has empty access_token in database")
|
logging.warning(f"User {user[0].email} has empty access_token in database")
|
||||||
|
|||||||
@ -1434,6 +1434,7 @@ async def retrieval_test(tenant_id):
|
|||||||
question = req["question"]
|
question = req["question"]
|
||||||
doc_ids = req.get("document_ids", [])
|
doc_ids = req.get("document_ids", [])
|
||||||
use_kg = req.get("use_kg", False)
|
use_kg = req.get("use_kg", False)
|
||||||
|
toc_enhance = req.get("toc_enhance", False)
|
||||||
langs = req.get("cross_languages", [])
|
langs = req.get("cross_languages", [])
|
||||||
if not isinstance(doc_ids, list):
|
if not isinstance(doc_ids, list):
|
||||||
return get_error_data_result("`documents` should be a list")
|
return get_error_data_result("`documents` should be a list")
|
||||||
@ -1487,6 +1488,11 @@ async def retrieval_test(tenant_id):
|
|||||||
highlight=highlight,
|
highlight=highlight,
|
||||||
rank_feature=label_question(question, kbs),
|
rank_feature=label_question(question, kbs),
|
||||||
)
|
)
|
||||||
|
if toc_enhance:
|
||||||
|
chat_mdl = LLMBundle(kb.tenant_id, LLMType.CHAT)
|
||||||
|
cks = settings.retriever.retrieval_by_toc(question, ranks["chunks"], tenant_ids, chat_mdl, size)
|
||||||
|
if cks:
|
||||||
|
ranks["chunks"] = cks
|
||||||
if use_kg:
|
if use_kg:
|
||||||
ck = settings.kg_retriever.retrieval(question, [k.tenant_id for k in kbs], kb_ids, embd_mdl, LLMBundle(kb.tenant_id, LLMType.CHAT))
|
ck = settings.kg_retriever.retrieval(question, [k.tenant_id for k in kbs], kb_ids, embd_mdl, LLMBundle(kb.tenant_id, LLMType.CHAT))
|
||||||
if ck["content_with_weight"]:
|
if ck["content_with_weight"]:
|
||||||
|
|||||||
@ -118,6 +118,7 @@ class FileSource(StrEnum):
|
|||||||
SHAREPOINT = "sharepoint"
|
SHAREPOINT = "sharepoint"
|
||||||
SLACK = "slack"
|
SLACK = "slack"
|
||||||
TEAMS = "teams"
|
TEAMS = "teams"
|
||||||
|
MOODLE = "moodle"
|
||||||
|
|
||||||
|
|
||||||
class PipelineTaskType(StrEnum):
|
class PipelineTaskType(StrEnum):
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from .google_drive.connector import GoogleDriveConnector
|
|||||||
from .jira.connector import JiraConnector
|
from .jira.connector import JiraConnector
|
||||||
from .sharepoint_connector import SharePointConnector
|
from .sharepoint_connector import SharePointConnector
|
||||||
from .teams_connector import TeamsConnector
|
from .teams_connector import TeamsConnector
|
||||||
|
from .moodle_connector import MoodleConnector
|
||||||
from .config import BlobType, DocumentSource
|
from .config import BlobType, DocumentSource
|
||||||
from .models import Document, TextSection, ImageSection, BasicExpertInfo
|
from .models import Document, TextSection, ImageSection, BasicExpertInfo
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
@ -36,6 +37,7 @@ __all__ = [
|
|||||||
"JiraConnector",
|
"JiraConnector",
|
||||||
"SharePointConnector",
|
"SharePointConnector",
|
||||||
"TeamsConnector",
|
"TeamsConnector",
|
||||||
|
"MoodleConnector",
|
||||||
"BlobType",
|
"BlobType",
|
||||||
"DocumentSource",
|
"DocumentSource",
|
||||||
"Document",
|
"Document",
|
||||||
|
|||||||
@ -48,6 +48,7 @@ class DocumentSource(str, Enum):
|
|||||||
GOOGLE_DRIVE = "google_drive"
|
GOOGLE_DRIVE = "google_drive"
|
||||||
GMAIL = "gmail"
|
GMAIL = "gmail"
|
||||||
DISCORD = "discord"
|
DISCORD = "discord"
|
||||||
|
MOODLE = "moodle"
|
||||||
S3_COMPATIBLE = "s3_compatible"
|
S3_COMPATIBLE = "s3_compatible"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
378
common/data_source/moodle_connector.py
Normal file
378
common/data_source/moodle_connector.py
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from collections.abc import Generator
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from retry import retry
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from markdownify import markdownify as md
|
||||||
|
from moodle import Moodle as MoodleClient, MoodleException
|
||||||
|
|
||||||
|
from common.data_source.config import INDEX_BATCH_SIZE
|
||||||
|
from common.data_source.exceptions import (
|
||||||
|
ConnectorMissingCredentialError,
|
||||||
|
CredentialExpiredError,
|
||||||
|
InsufficientPermissionsError,
|
||||||
|
ConnectorValidationError,
|
||||||
|
)
|
||||||
|
from common.data_source.interfaces import LoadConnector, PollConnector, SecondsSinceUnixEpoch
|
||||||
|
from common.data_source.models import Document
|
||||||
|
from common.data_source.utils import batch_generator, rl_requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MoodleConnector(LoadConnector, PollConnector):
|
||||||
|
"""Moodle LMS connector for accessing course content"""
|
||||||
|
|
||||||
|
def __init__(self, moodle_url: str, batch_size: int = INDEX_BATCH_SIZE) -> None:
|
||||||
|
self.moodle_url = moodle_url.rstrip("/")
|
||||||
|
self.batch_size = batch_size
|
||||||
|
self.moodle_client: Optional[MoodleClient] = None
|
||||||
|
|
||||||
|
def _add_token_to_url(self, file_url: str) -> str:
|
||||||
|
"""Append Moodle token to URL if missing"""
|
||||||
|
if not self.moodle_client:
|
||||||
|
return file_url
|
||||||
|
token = getattr(self.moodle_client, "token", "")
|
||||||
|
if "token=" in file_url.lower():
|
||||||
|
return file_url
|
||||||
|
delimiter = "&" if "?" in file_url else "?"
|
||||||
|
return f"{file_url}{delimiter}token={token}"
|
||||||
|
|
||||||
|
def _log_error(self, context: str, error: Exception, level: str = "warning") -> None:
|
||||||
|
"""Simplified logging wrapper"""
|
||||||
|
msg = f"{context}: {error}"
|
||||||
|
if level == "error":
|
||||||
|
logger.error(msg)
|
||||||
|
else:
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
|
def _get_latest_timestamp(self, *timestamps: int) -> int:
|
||||||
|
"""Return latest valid timestamp"""
|
||||||
|
return max((t for t in timestamps if t and t > 0), default=0)
|
||||||
|
|
||||||
|
def _yield_in_batches(
|
||||||
|
self, generator: Generator[Document, None, None]
|
||||||
|
) -> Generator[list[Document], None, None]:
|
||||||
|
for batch in batch_generator(generator, self.batch_size):
|
||||||
|
yield batch
|
||||||
|
|
||||||
|
def load_credentials(self, credentials: dict[str, Any]) -> None:
|
||||||
|
token = credentials.get("moodle_token")
|
||||||
|
if not token:
|
||||||
|
raise ConnectorMissingCredentialError("Moodle API token is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.moodle_client = MoodleClient(
|
||||||
|
self.moodle_url + "/webservice/rest/server.php", token
|
||||||
|
)
|
||||||
|
self.moodle_client.core.webservice.get_site_info()
|
||||||
|
except MoodleException as e:
|
||||||
|
if "invalidtoken" in str(e).lower():
|
||||||
|
raise CredentialExpiredError("Moodle token is invalid or expired")
|
||||||
|
raise ConnectorMissingCredentialError(f"Failed to initialize Moodle client: {e}")
|
||||||
|
|
||||||
|
def validate_connector_settings(self) -> None:
|
||||||
|
if not self.moodle_client:
|
||||||
|
raise ConnectorMissingCredentialError("Moodle client not initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
site_info = self.moodle_client.core.webservice.get_site_info()
|
||||||
|
if not site_info.sitename:
|
||||||
|
raise InsufficientPermissionsError("Invalid Moodle API response")
|
||||||
|
except MoodleException as e:
|
||||||
|
msg = str(e).lower()
|
||||||
|
if "invalidtoken" in msg:
|
||||||
|
raise CredentialExpiredError("Moodle token is invalid or expired")
|
||||||
|
if "accessexception" in msg:
|
||||||
|
raise InsufficientPermissionsError(
|
||||||
|
"Insufficient permissions. Ensure web services are enabled and permissions are correct."
|
||||||
|
)
|
||||||
|
raise ConnectorValidationError(f"Moodle validation error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectorValidationError(f"Unexpected validation error: {e}")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Data loading & polling
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_from_state(self) -> Generator[list[Document], None, None]:
|
||||||
|
if not self.moodle_client:
|
||||||
|
raise ConnectorMissingCredentialError("Moodle client not initialized")
|
||||||
|
|
||||||
|
logger.info("Starting full load from Moodle workspace")
|
||||||
|
courses = self._get_enrolled_courses()
|
||||||
|
if not courses:
|
||||||
|
logger.warning("No courses found to process")
|
||||||
|
return
|
||||||
|
|
||||||
|
yield from self._yield_in_batches(self._process_courses(courses))
|
||||||
|
|
||||||
|
def poll_source(
|
||||||
|
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||||
|
) -> Generator[list[Document], None, None]:
|
||||||
|
if not self.moodle_client:
|
||||||
|
raise ConnectorMissingCredentialError("Moodle client not initialized")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Polling Moodle updates between {datetime.fromtimestamp(start)} and {datetime.fromtimestamp(end)}"
|
||||||
|
)
|
||||||
|
courses = self._get_enrolled_courses()
|
||||||
|
if not courses:
|
||||||
|
logger.warning("No courses found to poll")
|
||||||
|
return
|
||||||
|
|
||||||
|
yield from self._yield_in_batches(self._get_updated_content(courses, start, end))
|
||||||
|
|
||||||
|
@retry(tries=3, delay=1, backoff=2)
|
||||||
|
def _get_enrolled_courses(self) -> list:
|
||||||
|
if not self.moodle_client:
|
||||||
|
raise ConnectorMissingCredentialError("Moodle client not initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.moodle_client.core.course.get_courses()
|
||||||
|
except MoodleException as e:
|
||||||
|
self._log_error("fetching courses", e, "error")
|
||||||
|
raise ConnectorValidationError(f"Failed to fetch courses: {e}")
|
||||||
|
|
||||||
|
@retry(tries=3, delay=1, backoff=2)
|
||||||
|
def _get_course_contents(self, course_id: int):
|
||||||
|
if not self.moodle_client:
|
||||||
|
raise ConnectorMissingCredentialError("Moodle client not initialized")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.moodle_client.core.course.get_contents(courseid=course_id)
|
||||||
|
except MoodleException as e:
|
||||||
|
self._log_error(f"fetching course contents for {course_id}", e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _process_courses(self, courses) -> Generator[Document, None, None]:
|
||||||
|
for course in courses:
|
||||||
|
try:
|
||||||
|
contents = self._get_course_contents(course.id)
|
||||||
|
for section in contents:
|
||||||
|
for module in section.modules:
|
||||||
|
doc = self._process_module(course, section, module)
|
||||||
|
if doc:
|
||||||
|
yield doc
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"processing course {course.fullname}", e)
|
||||||
|
|
||||||
|
def _get_updated_content(
|
||||||
|
self, courses, start: float, end: float
|
||||||
|
) -> Generator[Document, None, None]:
|
||||||
|
for course in courses:
|
||||||
|
try:
|
||||||
|
contents = self._get_course_contents(course.id)
|
||||||
|
for section in contents:
|
||||||
|
for module in section.modules:
|
||||||
|
times = [
|
||||||
|
getattr(module, "timecreated", 0),
|
||||||
|
getattr(module, "timemodified", 0),
|
||||||
|
]
|
||||||
|
if hasattr(module, "contents"):
|
||||||
|
times.extend(
|
||||||
|
getattr(c, "timemodified", 0)
|
||||||
|
for c in module.contents
|
||||||
|
if c and getattr(c, "timemodified", 0)
|
||||||
|
)
|
||||||
|
last_mod = self._get_latest_timestamp(*times)
|
||||||
|
if start < last_mod <= end:
|
||||||
|
doc = self._process_module(course, section, module)
|
||||||
|
if doc:
|
||||||
|
yield doc
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"polling course {course.fullname}", e)
|
||||||
|
|
||||||
|
def _process_module(
|
||||||
|
self, course, section, module
|
||||||
|
) -> Optional[Document]:
|
||||||
|
try:
|
||||||
|
mtype = module.modname
|
||||||
|
if mtype in ["label", "url"]:
|
||||||
|
return None
|
||||||
|
if mtype == "resource":
|
||||||
|
return self._process_resource(course, section, module)
|
||||||
|
if mtype == "forum":
|
||||||
|
return self._process_forum(course, section, module)
|
||||||
|
if mtype == "page":
|
||||||
|
return self._process_page(course, section, module)
|
||||||
|
if mtype in ["assign", "quiz"]:
|
||||||
|
return self._process_activity(course, section, module)
|
||||||
|
if mtype == "book":
|
||||||
|
return self._process_book(course, section, module)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"processing module {getattr(module, 'name', '?')}", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_resource(self, course, section, module) -> Optional[Document]:
|
||||||
|
if not getattr(module, "contents", None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_info = module.contents[0]
|
||||||
|
if not getattr(file_info, "fileurl", None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_name = os.path.basename(file_info.filename)
|
||||||
|
ts = self._get_latest_timestamp(
|
||||||
|
getattr(module, "timecreated", 0),
|
||||||
|
getattr(module, "timemodified", 0),
|
||||||
|
getattr(file_info, "timemodified", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = rl_requests.get(self._add_token_to_url(file_info.fileurl), timeout=60)
|
||||||
|
resp.raise_for_status()
|
||||||
|
blob = resp.content
|
||||||
|
ext = os.path.splitext(file_name)[1] or ".bin"
|
||||||
|
semantic_id = f"{course.fullname} / {section.name} / {file_name}"
|
||||||
|
return Document(
|
||||||
|
id=f"moodle_resource_{module.id}",
|
||||||
|
source="moodle",
|
||||||
|
semantic_identifier=semantic_id,
|
||||||
|
extension=ext,
|
||||||
|
blob=blob,
|
||||||
|
doc_updated_at=datetime.fromtimestamp(ts or 0, tz=timezone.utc),
|
||||||
|
size_bytes=len(blob),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"downloading resource {file_name}", e, "error")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_forum(self, course, section, module) -> Optional[Document]:
|
||||||
|
if not self.moodle_client or not getattr(module, "instance", None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.moodle_client.mod.forum.get_forum_discussions(forumid=module.instance)
|
||||||
|
disc_list = getattr(result, "discussions", [])
|
||||||
|
if not disc_list:
|
||||||
|
return None
|
||||||
|
|
||||||
|
markdown = [f"# {module.name}\n"]
|
||||||
|
latest_ts = self._get_latest_timestamp(
|
||||||
|
getattr(module, "timecreated", 0),
|
||||||
|
getattr(module, "timemodified", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in disc_list:
|
||||||
|
markdown.append(f"## {d.name}\n\n{md(d.message or '')}\n\n---\n")
|
||||||
|
latest_ts = max(latest_ts, getattr(d, "timemodified", 0))
|
||||||
|
|
||||||
|
blob = "\n".join(markdown).encode("utf-8")
|
||||||
|
semantic_id = f"{course.fullname} / {section.name} / {module.name}"
|
||||||
|
return Document(
|
||||||
|
id=f"moodle_forum_{module.id}",
|
||||||
|
source="moodle",
|
||||||
|
semantic_identifier=semantic_id,
|
||||||
|
extension=".md",
|
||||||
|
blob=blob,
|
||||||
|
doc_updated_at=datetime.fromtimestamp(latest_ts or 0, tz=timezone.utc),
|
||||||
|
size_bytes=len(blob),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"processing forum {module.name}", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_page(self, course, section, module) -> Optional[Document]:
|
||||||
|
if not getattr(module, "contents", None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_info = module.contents[0]
|
||||||
|
if not getattr(file_info, "fileurl", None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_name = os.path.basename(file_info.filename)
|
||||||
|
ts = self._get_latest_timestamp(
|
||||||
|
getattr(module, "timecreated", 0),
|
||||||
|
getattr(module, "timemodified", 0),
|
||||||
|
getattr(file_info, "timemodified", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = rl_requests.get(self._add_token_to_url(file_info.fileurl), timeout=60)
|
||||||
|
resp.raise_for_status()
|
||||||
|
blob = resp.content
|
||||||
|
ext = os.path.splitext(file_name)[1] or ".html"
|
||||||
|
semantic_id = f"{course.fullname} / {section.name} / {module.name}"
|
||||||
|
return Document(
|
||||||
|
id=f"moodle_page_{module.id}",
|
||||||
|
source="moodle",
|
||||||
|
semantic_identifier=semantic_id,
|
||||||
|
extension=ext,
|
||||||
|
blob=blob,
|
||||||
|
doc_updated_at=datetime.fromtimestamp(ts or 0, tz=timezone.utc),
|
||||||
|
size_bytes=len(blob),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"processing page {file_name}", e, "error")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_activity(self, course, section, module) -> Optional[Document]:
|
||||||
|
desc = getattr(module, "description", "")
|
||||||
|
if not desc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mtype, mname = module.modname, module.name
|
||||||
|
markdown = f"# {mname}\n\n**Type:** {mtype.capitalize()}\n\n{md(desc)}"
|
||||||
|
ts = self._get_latest_timestamp(
|
||||||
|
getattr(module, "timecreated", 0),
|
||||||
|
getattr(module, "timemodified", 0),
|
||||||
|
getattr(module, "added", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
semantic_id = f"{course.fullname} / {section.name} / {mname}"
|
||||||
|
blob = markdown.encode("utf-8")
|
||||||
|
return Document(
|
||||||
|
id=f"moodle_{mtype}_{module.id}",
|
||||||
|
source="moodle",
|
||||||
|
semantic_identifier=semantic_id,
|
||||||
|
extension=".md",
|
||||||
|
blob=blob,
|
||||||
|
doc_updated_at=datetime.fromtimestamp(ts or 0, tz=timezone.utc),
|
||||||
|
size_bytes=len(blob),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_book(self, course, section, module) -> Optional[Document]:
|
||||||
|
if not getattr(module, "contents", None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
contents = module.contents
|
||||||
|
chapters = [
|
||||||
|
c for c in contents
|
||||||
|
if getattr(c, "fileurl", None) and os.path.basename(c.filename) == "index.html"
|
||||||
|
]
|
||||||
|
if not chapters:
|
||||||
|
return None
|
||||||
|
|
||||||
|
latest_ts = self._get_latest_timestamp(
|
||||||
|
getattr(module, "timecreated", 0),
|
||||||
|
getattr(module, "timemodified", 0),
|
||||||
|
*[getattr(c, "timecreated", 0) for c in contents],
|
||||||
|
*[getattr(c, "timemodified", 0) for c in contents],
|
||||||
|
)
|
||||||
|
|
||||||
|
markdown_parts = [f"# {module.name}\n"]
|
||||||
|
for ch in chapters:
|
||||||
|
try:
|
||||||
|
resp = rl_requests.get(self._add_token_to_url(ch.fileurl), timeout=60)
|
||||||
|
resp.raise_for_status()
|
||||||
|
html = resp.content.decode("utf-8", errors="ignore")
|
||||||
|
markdown_parts.append(md(html) + "\n\n---\n")
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(f"processing book chapter {ch.filename}", e)
|
||||||
|
|
||||||
|
blob = "\n".join(markdown_parts).encode("utf-8")
|
||||||
|
semantic_id = f"{course.fullname} / {section.name} / {module.name}"
|
||||||
|
return Document(
|
||||||
|
id=f"moodle_book_{module.id}",
|
||||||
|
source="moodle",
|
||||||
|
semantic_identifier=semantic_id,
|
||||||
|
extension=".md",
|
||||||
|
blob=blob,
|
||||||
|
doc_updated_at=datetime.fromtimestamp(latest_ts or 0, tz=timezone.utc),
|
||||||
|
size_bytes=len(blob),
|
||||||
|
)
|
||||||
@ -2072,6 +2072,7 @@ Retrieves chunks from specified datasets.
|
|||||||
- `"cross_languages"`: `list[string]`
|
- `"cross_languages"`: `list[string]`
|
||||||
- `"metadata_condition"`: `object`
|
- `"metadata_condition"`: `object`
|
||||||
- `"use_kg"`: `boolean`
|
- `"use_kg"`: `boolean`
|
||||||
|
- `"toc_enhance"`: `boolean`
|
||||||
##### Request example
|
##### Request example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -2122,6 +2123,8 @@ curl --request POST \
|
|||||||
The number of chunks engaged in vector cosine computation. Defaults to `1024`.
|
The number of chunks engaged in vector cosine computation. Defaults to `1024`.
|
||||||
- `"use_kg"`: (*Body parameter*), `boolean`
|
- `"use_kg"`: (*Body parameter*), `boolean`
|
||||||
The search includes text chunks related to the knowledge graph of the selected dataset to handle complex multi-hop queries. Defaults to `False`.
|
The search includes text chunks related to the knowledge graph of the selected dataset to handle complex multi-hop queries. Defaults to `False`.
|
||||||
|
- `"toc_enhance"`: (*Body parameter*), `boolean`
|
||||||
|
The search includes table of content enhancement in order to boost rank of relevant chunks. Files parsed with `TOC Enhance` enabled is prerequisite. Defaults to `False`.
|
||||||
- `"rerank_id"`: (*Body parameter*), `integer`
|
- `"rerank_id"`: (*Body parameter*), `integer`
|
||||||
The ID of the rerank model.
|
The ID of the rerank model.
|
||||||
- `"keyword"`: (*Body parameter*), `boolean`
|
- `"keyword"`: (*Body parameter*), `boolean`
|
||||||
@ -2136,6 +2139,9 @@ curl --request POST \
|
|||||||
The languages that should be translated into, in order to achieve keywords retrievals in different languages.
|
The languages that should be translated into, in order to achieve keywords retrievals in different languages.
|
||||||
- `"metadata_condition"`: (*Body parameter*), `object`
|
- `"metadata_condition"`: (*Body parameter*), `object`
|
||||||
The metadata condition used for filtering chunks:
|
The metadata condition used for filtering chunks:
|
||||||
|
- `"logic"`: (*Body parameter*), `string`
|
||||||
|
- `"and"` Intersection of the result from each condition (default).
|
||||||
|
- `"or"` union of the result from each condition.
|
||||||
- `"conditions"`: (*Body parameter*), `array`
|
- `"conditions"`: (*Body parameter*), `array`
|
||||||
A list of metadata filter conditions.
|
A list of metadata filter conditions.
|
||||||
- `"name"`: `string` - The metadata field name to filter by, e.g., `"author"`, `"company"`, `"url"`. Ensure this parameter before use. See [Set metadata](../guides/dataset/set_metadata.md) for details.
|
- `"name"`: `string` - The metadata field name to filter by, e.g., `"author"`, `"company"`, `"url"`. Ensure this parameter before use. See [Set metadata](../guides/dataset/set_metadata.md) for details.
|
||||||
|
|||||||
@ -133,7 +133,7 @@ dependencies = [
|
|||||||
"pyicu>=2.15.3,<3.0.0",
|
"pyicu>=2.15.3,<3.0.0",
|
||||||
"flasgger>=0.9.7.1,<0.10.0",
|
"flasgger>=0.9.7.1,<0.10.0",
|
||||||
"xxhash>=3.5.0,<4.0.0",
|
"xxhash>=3.5.0,<4.0.0",
|
||||||
"trio>=0.29.0",
|
"trio>=0.17.0,<0.29.0",
|
||||||
"langfuse>=2.60.0",
|
"langfuse>=2.60.0",
|
||||||
"debugpy>=1.8.13",
|
"debugpy>=1.8.13",
|
||||||
"mcp>=1.9.4",
|
"mcp>=1.9.4",
|
||||||
@ -148,6 +148,7 @@ dependencies = [
|
|||||||
"markdownify>=1.2.0",
|
"markdownify>=1.2.0",
|
||||||
"captcha>=0.7.1",
|
"captcha>=0.7.1",
|
||||||
"pip>=25.2",
|
"pip>=25.2",
|
||||||
|
"moodlepy>=0.23.0",
|
||||||
"pypandoc>=1.16",
|
"pypandoc>=1.16",
|
||||||
"pyobvector==0.2.18",
|
"pyobvector==0.2.18",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1635,6 +1635,15 @@ class LiteLLMBase(ABC):
|
|||||||
provider_cfg["allow_fallbacks"] = False
|
provider_cfg["allow_fallbacks"] = False
|
||||||
extra_body["provider"] = provider_cfg
|
extra_body["provider"] = provider_cfg
|
||||||
completion_args.update({"extra_body": extra_body})
|
completion_args.update({"extra_body": extra_body})
|
||||||
|
|
||||||
|
# Ollama deployments commonly sit behind a reverse proxy that enforces
|
||||||
|
# Bearer auth. Ensure the Authorization header is set when an API key
|
||||||
|
# is provided, while respecting any user-supplied headers. #11350
|
||||||
|
extra_headers = deepcopy(completion_args.get("extra_headers") or {})
|
||||||
|
if self.provider == SupportedLiteLLMProvider.Ollama and self.api_key and "Authorization" not in extra_headers:
|
||||||
|
extra_headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
|
if extra_headers:
|
||||||
|
completion_args["extra_headers"] = extra_headers
|
||||||
return completion_args
|
return completion_args
|
||||||
|
|
||||||
def chat_with_tools(self, system: str, history: list, gen_conf: dict = {}):
|
def chat_with_tools(self, system: str, history: list, gen_conf: dict = {}):
|
||||||
|
|||||||
@ -608,6 +608,26 @@ def naive_merge(sections: str | list, chunk_token_num=128, delimiter="\n。;
|
|||||||
cks[-1] += t
|
cks[-1] += t
|
||||||
tk_nums[-1] += tnum
|
tk_nums[-1] += tnum
|
||||||
|
|
||||||
|
custom_delimiters = [m.group(1) for m in re.finditer(r"`([^`]+)`", delimiter)]
|
||||||
|
has_custom = bool(custom_delimiters)
|
||||||
|
if has_custom:
|
||||||
|
custom_pattern = "|".join(re.escape(t) for t in sorted(set(custom_delimiters), key=len, reverse=True))
|
||||||
|
cks, tk_nums = [], []
|
||||||
|
for sec, pos in sections:
|
||||||
|
split_sec = re.split(r"(%s)" % custom_pattern, sec, flags=re.DOTALL)
|
||||||
|
for sub_sec in split_sec:
|
||||||
|
if re.fullmatch(custom_pattern, sub_sec or ""):
|
||||||
|
continue
|
||||||
|
text = "\n" + sub_sec
|
||||||
|
local_pos = pos
|
||||||
|
if num_tokens_from_string(text) < 8:
|
||||||
|
local_pos = ""
|
||||||
|
if local_pos and text.find(local_pos) < 0:
|
||||||
|
text += local_pos
|
||||||
|
cks.append(text)
|
||||||
|
tk_nums.append(num_tokens_from_string(text))
|
||||||
|
return cks
|
||||||
|
|
||||||
dels = get_delimiters(delimiter)
|
dels = get_delimiters(delimiter)
|
||||||
for sec, pos in sections:
|
for sec, pos in sections:
|
||||||
if num_tokens_from_string(sec) < chunk_token_num:
|
if num_tokens_from_string(sec) < chunk_token_num:
|
||||||
@ -657,6 +677,29 @@ def naive_merge_with_images(texts, images, chunk_token_num=128, delimiter="\n。
|
|||||||
result_images[-1] = concat_img(result_images[-1], image)
|
result_images[-1] = concat_img(result_images[-1], image)
|
||||||
tk_nums[-1] += tnum
|
tk_nums[-1] += tnum
|
||||||
|
|
||||||
|
custom_delimiters = [m.group(1) for m in re.finditer(r"`([^`]+)`", delimiter)]
|
||||||
|
has_custom = bool(custom_delimiters)
|
||||||
|
if has_custom:
|
||||||
|
custom_pattern = "|".join(re.escape(t) for t in sorted(set(custom_delimiters), key=len, reverse=True))
|
||||||
|
cks, result_images, tk_nums = [], [], []
|
||||||
|
for text, image in zip(texts, images):
|
||||||
|
text_str = text[0] if isinstance(text, tuple) else text
|
||||||
|
text_pos = text[1] if isinstance(text, tuple) and len(text) > 1 else ""
|
||||||
|
split_sec = re.split(r"(%s)" % custom_pattern, text_str)
|
||||||
|
for sub_sec in split_sec:
|
||||||
|
if re.fullmatch(custom_pattern, sub_sec or ""):
|
||||||
|
continue
|
||||||
|
text_seg = "\n" + sub_sec
|
||||||
|
local_pos = text_pos
|
||||||
|
if num_tokens_from_string(text_seg) < 8:
|
||||||
|
local_pos = ""
|
||||||
|
if local_pos and text_seg.find(local_pos) < 0:
|
||||||
|
text_seg += local_pos
|
||||||
|
cks.append(text_seg)
|
||||||
|
result_images.append(image)
|
||||||
|
tk_nums.append(num_tokens_from_string(text_seg))
|
||||||
|
return cks, result_images
|
||||||
|
|
||||||
dels = get_delimiters(delimiter)
|
dels = get_delimiters(delimiter)
|
||||||
for text, image in zip(texts, images):
|
for text, image in zip(texts, images):
|
||||||
# if text is tuple, unpack it
|
# if text is tuple, unpack it
|
||||||
@ -748,6 +791,23 @@ def naive_merge_docx(sections, chunk_token_num=128, delimiter="\n。;!?"):
|
|||||||
images[-1] = concat_img(images[-1], image)
|
images[-1] = concat_img(images[-1], image)
|
||||||
tk_nums[-1] += tnum
|
tk_nums[-1] += tnum
|
||||||
|
|
||||||
|
custom_delimiters = [m.group(1) for m in re.finditer(r"`([^`]+)`", delimiter)]
|
||||||
|
has_custom = bool(custom_delimiters)
|
||||||
|
if has_custom:
|
||||||
|
custom_pattern = "|".join(re.escape(t) for t in sorted(set(custom_delimiters), key=len, reverse=True))
|
||||||
|
cks, images, tk_nums = [], [], []
|
||||||
|
pattern = r"(%s)" % custom_pattern
|
||||||
|
for sec, image in sections:
|
||||||
|
split_sec = re.split(pattern, sec)
|
||||||
|
for sub_sec in split_sec:
|
||||||
|
if not sub_sec or re.fullmatch(custom_pattern, sub_sec):
|
||||||
|
continue
|
||||||
|
text_seg = "\n" + sub_sec
|
||||||
|
cks.append(text_seg)
|
||||||
|
images.append(image)
|
||||||
|
tk_nums.append(num_tokens_from_string(text_seg))
|
||||||
|
return cks, images
|
||||||
|
|
||||||
dels = get_delimiters(delimiter)
|
dels = get_delimiters(delimiter)
|
||||||
pattern = r"(%s)" % dels
|
pattern = r"(%s)" % dels
|
||||||
|
|
||||||
|
|||||||
@ -37,14 +37,8 @@ from api.db.services.connector_service import ConnectorService, SyncLogsService
|
|||||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||||
from common import settings
|
from common import settings
|
||||||
from common.config_utils import show_configs
|
from common.config_utils import show_configs
|
||||||
|
from common.data_source import BlobStorageConnector, NotionConnector, DiscordConnector, GoogleDriveConnector, MoodleConnector, JiraConnector
|
||||||
from common.constants import FileSource, TaskStatus
|
from common.constants import FileSource, TaskStatus
|
||||||
from common.data_source import (
|
|
||||||
BlobStorageConnector,
|
|
||||||
DiscordConnector,
|
|
||||||
GoogleDriveConnector,
|
|
||||||
JiraConnector,
|
|
||||||
NotionConnector,
|
|
||||||
)
|
|
||||||
from common.data_source.config import INDEX_BATCH_SIZE
|
from common.data_source.config import INDEX_BATCH_SIZE
|
||||||
from common.data_source.confluence_connector import ConfluenceConnector
|
from common.data_source.confluence_connector import ConfluenceConnector
|
||||||
from common.data_source.interfaces import CheckpointOutputWrapper
|
from common.data_source.interfaces import CheckpointOutputWrapper
|
||||||
@ -418,6 +412,37 @@ class Teams(SyncBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Moodle(SyncBase):
|
||||||
|
SOURCE_NAME: str = FileSource.MOODLE
|
||||||
|
|
||||||
|
async def _generate(self, task: dict):
|
||||||
|
self.connector = MoodleConnector(
|
||||||
|
moodle_url=self.conf["moodle_url"],
|
||||||
|
batch_size=self.conf.get("batch_size", INDEX_BATCH_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.connector.load_credentials(self.conf["credentials"])
|
||||||
|
|
||||||
|
# Determine the time range for synchronization based on reindex or poll_range_start
|
||||||
|
if task["reindex"] == "1" or not task.get("poll_range_start"):
|
||||||
|
document_generator = self.connector.load_from_state()
|
||||||
|
begin_info = "totally"
|
||||||
|
else:
|
||||||
|
poll_start = task["poll_range_start"]
|
||||||
|
if poll_start is None:
|
||||||
|
document_generator = self.connector.load_from_state()
|
||||||
|
begin_info = "totally"
|
||||||
|
else:
|
||||||
|
document_generator = self.connector.poll_source(
|
||||||
|
poll_start.timestamp(),
|
||||||
|
datetime.now(timezone.utc).timestamp()
|
||||||
|
)
|
||||||
|
begin_info = "from {}".format(poll_start)
|
||||||
|
|
||||||
|
logging.info("Connect to Moodle: {} {}".format(self.conf["moodle_url"], begin_info))
|
||||||
|
return document_generator
|
||||||
|
|
||||||
|
|
||||||
func_factory = {
|
func_factory = {
|
||||||
FileSource.S3: S3,
|
FileSource.S3: S3,
|
||||||
FileSource.NOTION: Notion,
|
FileSource.NOTION: Notion,
|
||||||
@ -429,6 +454,7 @@ func_factory = {
|
|||||||
FileSource.SHAREPOINT: SharePoint,
|
FileSource.SHAREPOINT: SharePoint,
|
||||||
FileSource.SLACK: Slack,
|
FileSource.SLACK: Slack,
|
||||||
FileSource.TEAMS: Teams,
|
FileSource.TEAMS: Teams,
|
||||||
|
FileSource.MOODLE: Moodle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
web/src/assets/svg/data-source/moodle.svg
Normal file
4
web/src/assets/svg/data-source/moodle.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1230.87 315.18">
|
||||||
|
<path fill="#f98012" d="M289.61 309.77V201.51q0-33.94-28-33.95t-28.06 33.95v108.26H178.4V201.51q0-33.94-27.57-33.95-28.05 0-28 33.95v108.26H67.67V195.12q0-35.43 24.6-53.63 21.66-16.25 58.56-16.25 37.41 0 55.12 19.19 15.26-19.19 55.62-19.19 36.9 0 58.54 16.25 24.6 18.19 24.61 53.63v114.65Zm675.49-.5V0h55.16v309.27Zm-70.3 0v-18.22q-7.39 9.84-25.11 15.76a92.81 92.81 0 0 1-30.05 5.41q-39.4 0-63.28-27.09t-23.89-67c0-26.25 7.76-48.3 23.4-66 13.85-15.65 36.35-26.59 62.29-26.59 29.22 0 46.28 11 56.64 23.63V0h53.68v309.27Zm0-102.92q0-14.78-14-28.33T852 164.47q-21.16 0-33.48 17.24-10.85 15.3-10.84 37.43 0 21.68 10.84 36.94 12.3 17.75 33.48 17.73 12.81 0 27.83-12.07t15-24.86ZM648.57 314.19q-41.87 0-69.19-26.59T552 219.14q0-41.83 27.34-68.45t69.19-26.59q41.85 0 69.44 26.59t27.58 68.45q0 41.88-27.58 68.46t-69.4 26.59Zm0-145.77q-19.94 0-30.65 15.1t-10.71 35.88q0 20.78 10 35.13 11.46 16.34 31.4 16.32T680 254.53q10.46-14.34 10.46-35.13t-10-35.13q-11.46-15.86-31.89-15.85ZM449.13 314.19q-41.86 0-69.2-26.59t-27.33-68.46q0-41.83 27.33-68.45t69.2-26.59q41.83 0 69.44 26.59t27.57 68.45q0 41.88-27.57 68.46t-69.44 26.59Zm0-145.77q-19.94 0-30.66 15.1t-10.71 35.88q0 20.78 10 35.13 11.46 16.34 31.41 16.32t31.39-16.32Q491 240.19 491 219.4t-10-35.13q-11.44-15.86-31.87-15.85Zm636.45 67.47c1.18 13.13 18.25 41.37 46.31 41.37 27.31 0 40.23-15.77 40.87-22.16l58.11-.5c-6.34 19.39-32.1 60.58-100 60.58-28.24 0-54.08-8.79-72.64-26.35s-27.82-40.45-27.82-68.7q0-43.83 27.82-69.68t72.16-25.85q48.25 0 75.34 32 25.13 29.53 25.12 79.28Zm90.13-34c-2.3-11.83-7.23-21.49-14.77-29.06q-12.82-12.3-29.55-12.31-17.25 0-28.82 11.82t-15.5 29.55Z"/>
|
||||||
|
<path fill="#333" d="m174.74 116.9 54.74-40-.7-2.44C130 86.57 85.08 95.15 0 144.47l.79 2.24 6.76.07c-.62 6.81-1.7 23.64-.32 48.95-9.44 27.32-.24 45.88 8.4 66.07 1.37-21 1.23-44-5.22-66.89-1.35-25.14-.24-41.67.37-48.1l56.4.54a258 258 0 0 0 1.67 33.06c50.4 17.71 101.09-.06 128-43.72-7.47-8.37-22.11-19.79-22.11-19.79Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -356,6 +356,13 @@ const DynamicForm = {
|
|||||||
...combinedErrors,
|
...combinedErrors,
|
||||||
...fieldErrors,
|
...fieldErrors,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
console.log('combinedErrors', combinedErrors);
|
||||||
|
for (const key in combinedErrors) {
|
||||||
|
if (Array.isArray(combinedErrors[key])) {
|
||||||
|
combinedErrors[key] = combinedErrors[key][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log('combinedErrors', combinedErrors);
|
console.log('combinedErrors', combinedErrors);
|
||||||
return {
|
return {
|
||||||
values: Object.keys(combinedErrors).length ? {} : data,
|
values: Object.keys(combinedErrors).length ? {} : data,
|
||||||
@ -720,9 +727,7 @@ const DynamicForm = {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={submitLoading}
|
disabled={submitLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('form submit');
|
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log('form submit2');
|
|
||||||
try {
|
try {
|
||||||
let beValid = await form.formControl.trigger();
|
let beValid = await form.formControl.trigger();
|
||||||
console.log('form valid', beValid, form, form.formControl);
|
console.log('form valid', beValid, form, form.formControl);
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { ModelVariableType } from '@/constants/knowledge';
|
import {
|
||||||
|
ModelVariableType,
|
||||||
|
settledModelVariableMap,
|
||||||
|
} from '@/constants/knowledge';
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
import { useTranslate } from '@/hooks/common-hooks';
|
||||||
import { camelCase } from 'lodash';
|
import { camelCase, isEqual } from 'lodash';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -25,6 +28,13 @@ import { useHandleFreedomChange } from './use-watch-change';
|
|||||||
interface LlmSettingFieldItemsProps {
|
interface LlmSettingFieldItemsProps {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
options?: any[];
|
options?: any[];
|
||||||
|
showFields?: Array<
|
||||||
|
| 'temperature'
|
||||||
|
| 'top_p'
|
||||||
|
| 'presence_penalty'
|
||||||
|
| 'frequency_penalty'
|
||||||
|
| 'max_tokens'
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LLMIdFormField = {
|
export const LLMIdFormField = {
|
||||||
@ -56,6 +66,13 @@ export const LlmSettingSchema = {
|
|||||||
export function LlmSettingFieldItems({
|
export function LlmSettingFieldItems({
|
||||||
prefix,
|
prefix,
|
||||||
options,
|
options,
|
||||||
|
showFields = [
|
||||||
|
'temperature',
|
||||||
|
'top_p',
|
||||||
|
'presence_penalty',
|
||||||
|
'frequency_penalty',
|
||||||
|
'max_tokens',
|
||||||
|
],
|
||||||
}: LlmSettingFieldItemsProps) {
|
}: LlmSettingFieldItemsProps) {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const { t } = useTranslate('chat');
|
const { t } = useTranslate('chat');
|
||||||
@ -72,14 +89,53 @@ export function LlmSettingFieldItems({
|
|||||||
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
|
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
|
||||||
label: t(camelCase(x)),
|
label: t(camelCase(x)),
|
||||||
value: x,
|
value: x,
|
||||||
}));
|
})) as { label: string; value: ModelVariableType | 'Custom' }[];
|
||||||
|
|
||||||
|
parameterOptions.push({
|
||||||
|
label: t(camelCase('Custom')),
|
||||||
|
value: 'Custom',
|
||||||
|
});
|
||||||
|
const checkParameterIsEqual = () => {
|
||||||
|
const [
|
||||||
|
parameter,
|
||||||
|
topPValue,
|
||||||
|
frequencyPenaltyValue,
|
||||||
|
temperatureValue,
|
||||||
|
presencePenaltyValue,
|
||||||
|
maxTokensValue,
|
||||||
|
] = form.getValues([
|
||||||
|
getFieldWithPrefix('parameter'),
|
||||||
|
getFieldWithPrefix('temperature'),
|
||||||
|
getFieldWithPrefix('top_p'),
|
||||||
|
getFieldWithPrefix('frequency_penalty'),
|
||||||
|
getFieldWithPrefix('presence_penalty'),
|
||||||
|
getFieldWithPrefix('max_tokens'),
|
||||||
|
]);
|
||||||
|
if (parameter && parameter !== 'Custom') {
|
||||||
|
const parameterValue =
|
||||||
|
settledModelVariableMap[parameter as keyof typeof ModelVariableType];
|
||||||
|
const parameterRealValue = {
|
||||||
|
top_p: topPValue,
|
||||||
|
temperature: temperatureValue,
|
||||||
|
frequency_penalty: frequencyPenaltyValue,
|
||||||
|
presence_penalty: presencePenaltyValue,
|
||||||
|
max_tokens: maxTokensValue,
|
||||||
|
};
|
||||||
|
if (!isEqual(parameterValue, parameterRealValue)) {
|
||||||
|
form.setValue(getFieldWithPrefix('parameter'), 'Custom');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<LLMFormField options={options}></LLMFormField>
|
<LLMFormField
|
||||||
|
options={options}
|
||||||
|
name={getFieldWithPrefix('llm_id')}
|
||||||
|
></LLMFormField>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={'parameter'}
|
name={getFieldWithPrefix('parameter')}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex justify-between items-center">
|
<FormItem className="flex justify-between items-center">
|
||||||
<FormLabel className="flex-1">{t('freedom')}</FormLabel>
|
<FormLabel className="flex-1">{t('freedom')}</FormLabel>
|
||||||
@ -107,6 +163,7 @@ export function LlmSettingFieldItems({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{showFields.some((item) => item === 'temperature') && (
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('temperature')}
|
name={getFieldWithPrefix('temperature')}
|
||||||
checkName="temperatureEnabled"
|
checkName="temperatureEnabled"
|
||||||
@ -114,7 +171,12 @@ export function LlmSettingFieldItems({
|
|||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
min={0}
|
min={0}
|
||||||
|
onChange={() => {
|
||||||
|
checkParameterIsEqual();
|
||||||
|
}}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
|
)}
|
||||||
|
{showFields.some((item) => item === 'top_p') && (
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('top_p')}
|
name={getFieldWithPrefix('top_p')}
|
||||||
checkName="topPEnabled"
|
checkName="topPEnabled"
|
||||||
@ -122,7 +184,12 @@ export function LlmSettingFieldItems({
|
|||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
min={0}
|
min={0}
|
||||||
|
onChange={() => {
|
||||||
|
checkParameterIsEqual();
|
||||||
|
}}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
|
)}
|
||||||
|
{showFields.some((item) => item === 'presence_penalty') && (
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('presence_penalty')}
|
name={getFieldWithPrefix('presence_penalty')}
|
||||||
checkName="presencePenaltyEnabled"
|
checkName="presencePenaltyEnabled"
|
||||||
@ -130,7 +197,12 @@ export function LlmSettingFieldItems({
|
|||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
min={0}
|
min={0}
|
||||||
|
onChange={() => {
|
||||||
|
checkParameterIsEqual();
|
||||||
|
}}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
|
)}
|
||||||
|
{showFields.some((item) => item === 'frequency_penalty') && (
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('frequency_penalty')}
|
name={getFieldWithPrefix('frequency_penalty')}
|
||||||
checkName="frequencyPenaltyEnabled"
|
checkName="frequencyPenaltyEnabled"
|
||||||
@ -138,14 +210,24 @@ export function LlmSettingFieldItems({
|
|||||||
max={1}
|
max={1}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
min={0}
|
min={0}
|
||||||
|
onChange={() => {
|
||||||
|
checkParameterIsEqual();
|
||||||
|
}}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
|
)}
|
||||||
|
{showFields.some((item) => item === 'max_tokens') && (
|
||||||
<SliderInputSwitchFormField
|
<SliderInputSwitchFormField
|
||||||
name={getFieldWithPrefix('max_tokens')}
|
name={getFieldWithPrefix('max_tokens')}
|
||||||
checkName="maxTokensEnabled"
|
checkName="maxTokensEnabled"
|
||||||
|
numberInputClassName="w-20"
|
||||||
label="maxTokens"
|
label="maxTokens"
|
||||||
max={128000}
|
max={128000}
|
||||||
min={0}
|
min={0}
|
||||||
|
onChange={() => {
|
||||||
|
checkParameterIsEqual();
|
||||||
|
}}
|
||||||
></SliderInputSwitchFormField>
|
></SliderInputSwitchFormField>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type SliderInputSwitchFormFieldProps = {
|
|||||||
onChange?: (value: number) => void;
|
onChange?: (value: number) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
checkName: string;
|
checkName: string;
|
||||||
|
numberInputClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SliderInputSwitchFormField({
|
export function SliderInputSwitchFormField({
|
||||||
@ -34,6 +35,7 @@ export function SliderInputSwitchFormField({
|
|||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
checkName,
|
checkName,
|
||||||
|
numberInputClassName,
|
||||||
}: SliderInputSwitchFormFieldProps) {
|
}: SliderInputSwitchFormFieldProps) {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const disabled = !form.watch(checkName);
|
const disabled = !form.watch(checkName);
|
||||||
@ -81,7 +83,10 @@ export function SliderInputSwitchFormField({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-7 w-20"
|
className={cn(
|
||||||
|
'h-6 w-10 p-1 border border-border-button rounded-sm',
|
||||||
|
numberInputClassName,
|
||||||
|
)}
|
||||||
max={max}
|
max={max}
|
||||||
min={min}
|
min={min}
|
||||||
step={step}
|
step={step}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type MetadataFilterProps = {
|
|||||||
export const MetadataFilterSchema = {
|
export const MetadataFilterSchema = {
|
||||||
meta_data_filter: z
|
meta_data_filter: z
|
||||||
.object({
|
.object({
|
||||||
|
logic: z.string().optional(),
|
||||||
method: z.string().optional(),
|
method: z.string().optional(),
|
||||||
manual: z
|
manual: z
|
||||||
.array(
|
.array(
|
||||||
|
|||||||
@ -15,14 +15,17 @@ import {
|
|||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { SwitchOperatorOptions } from '@/constants/agent';
|
import { SwitchLogicOperator, SwitchOperatorOptions } from '@/constants/agent';
|
||||||
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
|
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
|
||||||
|
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||||
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
|
import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request';
|
||||||
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
|
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
|
||||||
import { Plus, X } from 'lucide-react';
|
import { Plus, X } from 'lucide-react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RAGFlowFormItem } from '../ragflow-form';
|
||||||
|
import { RAGFlowSelect } from '../ui/select';
|
||||||
|
|
||||||
export function MetadataFilterConditions({
|
export function MetadataFilterConditions({
|
||||||
kbIds,
|
kbIds,
|
||||||
@ -36,10 +39,13 @@ export function MetadataFilterConditions({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
const name = prefix + 'meta_data_filter.manual';
|
const name = prefix + 'meta_data_filter.manual';
|
||||||
|
const logic = prefix + 'meta_data_filter.logic';
|
||||||
const metadata = useFetchKnowledgeMetadata(kbIds);
|
const metadata = useFetchKnowledgeMetadata(kbIds);
|
||||||
|
|
||||||
const switchOperatorOptions = useBuildSwitchOperatorOptions();
|
const switchOperatorOptions = useBuildSwitchOperatorOptions();
|
||||||
|
|
||||||
|
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
|
||||||
|
|
||||||
const { fields, remove, append } = useFieldArray({
|
const { fields, remove, append } = useFieldArray({
|
||||||
name,
|
name,
|
||||||
control: form.control,
|
control: form.control,
|
||||||
@ -47,13 +53,14 @@ export function MetadataFilterConditions({
|
|||||||
|
|
||||||
const add = useCallback(
|
const add = useCallback(
|
||||||
(key: string) => () => {
|
(key: string) => () => {
|
||||||
|
form.setValue(logic, SwitchLogicOperator.And);
|
||||||
append({
|
append({
|
||||||
key,
|
key,
|
||||||
value: '',
|
value: '',
|
||||||
op: SwitchOperatorOptions[0].value,
|
op: SwitchOperatorOptions[0].value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[append],
|
[append, form, logic],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -77,11 +84,28 @@ export function MetadataFilterConditions({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-5">
|
<section className="flex">
|
||||||
|
{fields.length > 1 && (
|
||||||
|
<div className="relative min-w-14">
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={logic}
|
||||||
|
className="absolute top-1/2 -translate-y-1/2 right-1 left-0 z-10 bg-bg-base"
|
||||||
|
>
|
||||||
|
<RAGFlowSelect
|
||||||
|
options={switchLogicOperatorOptions}
|
||||||
|
triggerClassName="w-full text-xs px-1 py-0 h-6"
|
||||||
|
></RAGFlowSelect>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<div className="absolute border-l border-y w-5 right-0 top-4 bottom-4 rounded-l-lg"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-5 flex-1">
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const typeField = `${name}.${index}.key`;
|
const typeField = `${name}.${index}.key`;
|
||||||
return (
|
return (
|
||||||
<div key={field.id} className="flex w-full items-center gap-2">
|
<section key={field.id} className="flex gap-2">
|
||||||
|
<div className="w-full space-y-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={typeField}
|
name={typeField}
|
||||||
@ -97,7 +121,7 @@ export function MetadataFilterConditions({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Separator className="w-3 text-text-secondary" />
|
<Separator className="w-1 text-text-secondary" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`${name}.${index}.op`}
|
name={`${name}.${index}.op`}
|
||||||
@ -113,7 +137,7 @@ export function MetadataFilterConditions({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Separator className="w-3 text-text-secondary" />
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`${name}.${index}.value`}
|
name={`${name}.${index}.value`}
|
||||||
@ -137,13 +161,15 @@ export function MetadataFilterConditions({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
<X className="text-text-sub-title-invert " />
|
<X className="text-text-sub-title-invert " />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,13 @@ const Input = function ({
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-input file:text-foreground placeholder:text-muted-foreground/70 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
'border-border-button file:text-foreground placeholder:text-text-disabled flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
'focus-visible:border-border-button focus-visible:ring-text-primary/50 focus-visible:ring-1',
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
type === 'search' &&
|
type === 'search' &&
|
||||||
'[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none',
|
'[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none',
|
||||||
type === 'file' &&
|
type === 'file' &&
|
||||||
'text-muted-foreground/70 file:border-input file:text-foreground p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic',
|
'text-text-disabled file:border-input file:text-foreground p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic',
|
||||||
icon && iconPosition === 'left' && 'pl-7',
|
icon && iconPosition === 'left' && 'pl-7',
|
||||||
icon && iconPosition === 'right' && 'pr-7',
|
icon && iconPosition === 'right' && 'pr-7',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -221,10 +221,12 @@ const RaptorFormFields = ({
|
|||||||
defaultValue={0}
|
defaultValue={0}
|
||||||
type="number"
|
type="number"
|
||||||
suffix={
|
suffix={
|
||||||
|
<div className="w-7 flex justify-center items-center">
|
||||||
<Shuffle
|
<Shuffle
|
||||||
className="size-3.5 cursor-pointer"
|
className="size-3.5 cursor-pointer"
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -59,6 +59,7 @@ interface SimilaritySliderFormFieldProps {
|
|||||||
similarityName?: string;
|
similarityName?: string;
|
||||||
vectorSimilarityWeightName?: string;
|
vectorSimilarityWeightName?: string;
|
||||||
isTooltipShown?: boolean;
|
isTooltipShown?: boolean;
|
||||||
|
numberInputClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialSimilarityThresholdValue = {
|
export const initialSimilarityThresholdValue = {
|
||||||
@ -86,6 +87,7 @@ export function SimilaritySliderFormField({
|
|||||||
similarityName = 'similarity_threshold',
|
similarityName = 'similarity_threshold',
|
||||||
vectorSimilarityWeightName = 'vector_similarity_weight',
|
vectorSimilarityWeightName = 'vector_similarity_weight',
|
||||||
isTooltipShown,
|
isTooltipShown,
|
||||||
|
numberInputClassName,
|
||||||
}: SimilaritySliderFormFieldProps) {
|
}: SimilaritySliderFormFieldProps) {
|
||||||
const { t } = useTranslate('knowledgeDetails');
|
const { t } = useTranslate('knowledgeDetails');
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
@ -101,6 +103,7 @@ export function SimilaritySliderFormField({
|
|||||||
step={0.01}
|
step={0.01}
|
||||||
layout={FormLayout.Vertical}
|
layout={FormLayout.Vertical}
|
||||||
tooltip={isTooltipShown && t('similarityThresholdTip')}
|
tooltip={isTooltipShown && t('similarityThresholdTip')}
|
||||||
|
numberInputClassName={numberInputClassName}
|
||||||
></SliderInputFormField>
|
></SliderInputFormField>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -124,7 +127,7 @@ export function SimilaritySliderFormField({
|
|||||||
isVector ? 'vectorSimilarityWeight' : 'keywordSimilarityWeight',
|
isVector ? 'vectorSimilarityWeight' : 'keywordSimilarityWeight',
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className={cn('flex items-end gap-14 justify-between')}>
|
<div className={cn('flex items-end gap-4 justify-between')}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex flex-col flex-1 gap-2">
|
<div className="flex flex-col flex-1 gap-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -158,6 +161,7 @@ export function SimilaritySliderFormField({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'h-6 w-10 p-0 text-center bg-bg-input border-border-default border text-text-secondary',
|
'h-6 w-10 p-0 text-center bg-bg-input border-border-default border text-text-secondary',
|
||||||
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
||||||
|
numberInputClassName,
|
||||||
)}
|
)}
|
||||||
max={1}
|
max={1}
|
||||||
min={0}
|
min={0}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type SliderInputFormFieldProps = {
|
|||||||
tooltip?: ReactNode;
|
tooltip?: ReactNode;
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
numberInputClassName?: string;
|
||||||
} & FormLayoutType;
|
} & FormLayoutType;
|
||||||
|
|
||||||
export function SliderInputFormField({
|
export function SliderInputFormField({
|
||||||
@ -36,6 +37,7 @@ export function SliderInputFormField({
|
|||||||
tooltip,
|
tooltip,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
className,
|
className,
|
||||||
|
numberInputClassName,
|
||||||
layout = FormLayout.Horizontal,
|
layout = FormLayout.Horizontal,
|
||||||
}: SliderInputFormFieldProps) {
|
}: SliderInputFormFieldProps) {
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
@ -61,7 +63,7 @@ export function SliderInputFormField({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-14 justify-between',
|
'flex items-center gap-4 justify-between',
|
||||||
{ 'w-3/4': isHorizontal },
|
{ 'w-3/4': isHorizontal },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@ -82,6 +84,7 @@ export function SliderInputFormField({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'h-6 w-10 p-0 text-center bg-bg-input border border-border-default text-text-secondary',
|
'h-6 w-10 p-0 text-center bg-bg-input border border-border-default text-text-secondary',
|
||||||
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
||||||
|
numberInputClassName,
|
||||||
)}
|
)}
|
||||||
max={max}
|
max={max}
|
||||||
min={min}
|
min={min}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ const CommandInput = React.forwardRef<
|
|||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
'flex min-h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-text-secondary disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const Divider: React.FC<DividerProps> = ({
|
|||||||
direction = 'horizontal',
|
direction = 'horizontal',
|
||||||
type = 'horizontal',
|
type = 'horizontal',
|
||||||
text,
|
text,
|
||||||
color = 'border-muted-foreground/50',
|
color = 'border-border-button',
|
||||||
margin = 'my-4',
|
margin = 'my-4',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Eye, EyeOff, Search } from 'lucide-react';
|
import { Eye, EyeOff, Search } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
export interface InputProps
|
export interface InputProps
|
||||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||||
@ -17,6 +17,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
const { defaultValue, ...restProps } = props;
|
const { defaultValue, ...restProps } = props;
|
||||||
const inputValue = isControlled ? value : defaultValue;
|
const inputValue = isControlled ? value : defaultValue;
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [prefixWidth, setPrefixWidth] = useState(0);
|
||||||
|
const [suffixWidth, setSuffixWidth] = useState(0);
|
||||||
|
|
||||||
|
const prefixRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const suffixRef = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prefixRef.current) {
|
||||||
|
setPrefixWidth(prefixRef.current.offsetWidth);
|
||||||
|
}
|
||||||
|
if (suffixRef.current) {
|
||||||
|
setSuffixWidth(suffixRef.current.offsetWidth);
|
||||||
|
}
|
||||||
|
}, [prefix, suffix, prefixRef, suffixRef]);
|
||||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
const numValue = e.target.value === '' ? '' : Number(e.target.value);
|
const numValue = e.target.value === '' ? '' : Number(e.target.value);
|
||||||
@ -34,7 +48,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
|
|
||||||
const isPasswordInput = type === 'password';
|
const isPasswordInput = type === 'password';
|
||||||
|
|
||||||
const inputEl = (
|
const inputEl = useMemo(
|
||||||
|
() => (
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type={isPasswordInput && showPassword ? 'text' : type}
|
type={isPasswordInput && showPassword ? 'text' : type}
|
||||||
@ -44,32 +59,49 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
'file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled',
|
'file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled',
|
||||||
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary',
|
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
|
'disabled:cursor-not-allowed disabled:opacity-50 transition-colors',
|
||||||
{
|
|
||||||
'pl-[calc(1em+1.25rem)]': !!prefix,
|
|
||||||
'pr-[calc(1em+1.25rem)]': !!suffix || isPasswordInput,
|
|
||||||
'pr-[calc(2em+2rem)]': !!suffix && isPasswordInput,
|
|
||||||
},
|
|
||||||
type === 'number' &&
|
type === 'number' &&
|
||||||
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
paddingLeft: !!prefix ? `${prefixWidth}px` : '',
|
||||||
|
paddingRight: isPasswordInput
|
||||||
|
? '40px'
|
||||||
|
: !!suffix
|
||||||
|
? `${suffixWidth}px`
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
value={inputValue ?? ''}
|
value={inputValue ?? ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
prefixWidth,
|
||||||
|
suffixWidth,
|
||||||
|
isPasswordInput,
|
||||||
|
inputValue,
|
||||||
|
className,
|
||||||
|
handleChange,
|
||||||
|
restProps,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (prefix || suffix || isPasswordInput) {
|
if (prefix || suffix || isPasswordInput) {
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
<span className="absolute left-0 top-[50%] translate-y-[-50%]">
|
<span
|
||||||
|
ref={prefixRef}
|
||||||
|
className="absolute left-0 top-[50%] translate-y-[-50%]"
|
||||||
|
>
|
||||||
{prefix}
|
{prefix}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{inputEl}
|
{inputEl}
|
||||||
{suffix && (
|
{suffix && (
|
||||||
<span
|
<span
|
||||||
|
ref={suffixRef}
|
||||||
className={cn('absolute right-0 top-[50%] translate-y-[-50%]', {
|
className={cn('absolute right-0 top-[50%] translate-y-[-50%]', {
|
||||||
'right-14': isPasswordInput,
|
'right-14': isPasswordInput,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -289,12 +289,12 @@ export const MultiSelect = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
onClick={handleTogglePopover}
|
onClick={handleTogglePopover}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto',
|
'flex w-full p-1 rounded-md border border-border-button min-h-10 h-auto placeholder:text-text-disabled items-center justify-between bg-bg-input hover:bg-bg-input [&_svg]:pointer-events-auto',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedValues.length > 0 ? (
|
{selectedValues.length > 0 ? (
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full group">
|
||||||
<div className="flex flex-wrap items-center">
|
<div className="flex flex-wrap items-center">
|
||||||
{selectedValues?.slice(0, maxCount)?.map((value) => {
|
{selectedValues?.slice(0, maxCount)?.map((value) => {
|
||||||
const option = flatOptions.find((o) => o.value === value);
|
const option = flatOptions.find((o) => o.value === value);
|
||||||
@ -350,7 +350,7 @@ export const MultiSelect = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between ">
|
<div className="flex items-center justify-between ">
|
||||||
<XIcon
|
<XIcon
|
||||||
className="h-4 mx-2 cursor-pointer text-muted-foreground"
|
className="h-4 mx-2 cursor-pointer text-text-secondary hidden group-hover:block"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleClear();
|
handleClear();
|
||||||
@ -358,17 +358,17 @@ export const MultiSelect = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
<Separator
|
<Separator
|
||||||
orientation="vertical"
|
orientation="vertical"
|
||||||
className="flex min-h-6 h-full"
|
className="min-h-6 h-full hidden group-hover:flex"
|
||||||
/>
|
/>
|
||||||
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
|
<ChevronDown className="h-4 mx-2 cursor-pointer text-text-secondary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between w-full mx-auto">
|
<div className="flex items-center justify-between w-full mx-auto">
|
||||||
<span className="text-sm text-muted-foreground mx-3">
|
<span className="text-sm text-text-secondary mx-3">
|
||||||
{placeholder}
|
{placeholder}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
|
<ChevronDown className="h-4 cursor-pointer text-text-secondary mx-2" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@ -379,14 +379,16 @@ export const MultiSelect = React.forwardRef<
|
|||||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||||
>
|
>
|
||||||
<Command className="p-5 pb-8">
|
<Command className="p-5 pb-8">
|
||||||
|
{options && options.length > 0 && (
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={t('common.search') + '...'}
|
placeholder={t('common.search') + '...'}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<CommandList className="mt-2">
|
<CommandList className="mt-2">
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{showSelectAll && (
|
{showSelectAll && options && options.length > 0 && (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key="all"
|
key="all"
|
||||||
onSelect={toggleAll}
|
onSelect={toggleAll}
|
||||||
@ -454,12 +456,14 @@ export const MultiSelect = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{options && options.length > 0 && (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => setIsPopoverOpen(false)}
|
onSelect={() => setIsPopoverOpen(false)}
|
||||||
className="flex-1 justify-center cursor-pointer max-w-full"
|
className="flex-1 justify-center cursor-pointer max-w-full"
|
||||||
>
|
>
|
||||||
{t('common.close')}
|
{t('common.close')}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
|
|||||||
@ -179,3 +179,8 @@ export enum JsonSchemaDataType {
|
|||||||
Array = 'array',
|
Array = 'array',
|
||||||
Object = 'object',
|
Object = 'object',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SwitchLogicOperator {
|
||||||
|
And = 'and',
|
||||||
|
Or = 'or',
|
||||||
|
}
|
||||||
|
|||||||
12
web/src/hooks/logic-hooks/use-build-options.ts
Normal file
12
web/src/hooks/logic-hooks/use-build-options.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { SwitchLogicOperator } from '@/constants/agent';
|
||||||
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function useBuildSwitchLogicOperatorOptions() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return buildOptions(
|
||||||
|
SwitchLogicOperator,
|
||||||
|
t,
|
||||||
|
'flow.switchLogicOperatorOptions',
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -736,9 +736,15 @@ Example: https://fsn1.your-objectstorage.com`,
|
|||||||
google_drivePrimaryAdminTip:
|
google_drivePrimaryAdminTip:
|
||||||
'Email address that has access to the Drive content being synced.',
|
'Email address that has access to the Drive content being synced.',
|
||||||
google_driveMyDriveEmailsTip:
|
google_driveMyDriveEmailsTip:
|
||||||
'Comma-separated emails whose “My Drive” contents should be indexed (include the primary admin).',
|
'Comma-separated emails whose "My Drive" contents should be indexed (include the primary admin).',
|
||||||
google_driveSharedFoldersTip:
|
google_driveSharedFoldersTip:
|
||||||
'Comma-separated Google Drive folder links to crawl.',
|
'Comma-separated Google Drive folder links to crawl.',
|
||||||
|
moodleDescription:
|
||||||
|
'Connect to your Moodle LMS to sync course content, forums, and resources.',
|
||||||
|
moodleUrlTip:
|
||||||
|
'The base URL of your Moodle instance (e.g., https://moodle.university.edu). Do not include /webservice or /login.',
|
||||||
|
moodleTokenTip:
|
||||||
|
'Generate a web service token in Moodle: Go to Site administration → Server → Web services → Manage tokens. The user must be enrolled in the courses you want to sync.',
|
||||||
jiraDescription:
|
jiraDescription:
|
||||||
'Connect your Jira workspace to sync issues, comments, and attachments.',
|
'Connect your Jira workspace to sync issues, comments, and attachments.',
|
||||||
jiraBaseUrlTip:
|
jiraBaseUrlTip:
|
||||||
@ -1046,7 +1052,7 @@ Example: https://fsn1.your-objectstorage.com`,
|
|||||||
downloadFileType: 'Download file type',
|
downloadFileType: 'Download file type',
|
||||||
formatTypeError: 'Format or type error',
|
formatTypeError: 'Format or type error',
|
||||||
variableNameMessage:
|
variableNameMessage:
|
||||||
'Variable name can only contain letters and underscores',
|
'Variable name can only contain letters and underscores and numbers',
|
||||||
variableDescription: 'Variable Description',
|
variableDescription: 'Variable Description',
|
||||||
defaultValue: 'Default Value',
|
defaultValue: 'Default Value',
|
||||||
conversationVariable: 'Conversation variable',
|
conversationVariable: 'Conversation variable',
|
||||||
|
|||||||
@ -980,7 +980,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
|||||||
downloadFileTypeTip: '文件下载的类型',
|
downloadFileTypeTip: '文件下载的类型',
|
||||||
downloadFileType: '文件类型',
|
downloadFileType: '文件类型',
|
||||||
formatTypeError: '格式或类型错误',
|
formatTypeError: '格式或类型错误',
|
||||||
variableNameMessage: '名称只能包含字母和下划线',
|
variableNameMessage: '名称只能包含字母,数字和下划线',
|
||||||
variableDescription: '变量的描述',
|
variableDescription: '变量的描述',
|
||||||
defaultValue: '默认值',
|
defaultValue: '默认值',
|
||||||
conversationVariable: '会话变量',
|
conversationVariable: '会话变量',
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
JsonSchemaDataType,
|
JsonSchemaDataType,
|
||||||
Operator,
|
Operator,
|
||||||
ProgrammingLanguage,
|
ProgrammingLanguage,
|
||||||
|
SwitchLogicOperator,
|
||||||
SwitchOperatorOptions,
|
SwitchOperatorOptions,
|
||||||
initialLlmBaseValues,
|
initialLlmBaseValues,
|
||||||
} from '@/constants/agent';
|
} from '@/constants/agent';
|
||||||
@ -51,8 +52,6 @@ import {
|
|||||||
|
|
||||||
export const BeginId = 'begin';
|
export const BeginId = 'begin';
|
||||||
|
|
||||||
export const SwitchLogicOperatorOptions = ['and', 'or'];
|
|
||||||
|
|
||||||
export const CommonOperatorList = Object.values(Operator).filter(
|
export const CommonOperatorList = Object.values(Operator).filter(
|
||||||
(x) => x !== Operator.Note,
|
(x) => x !== Operator.Note,
|
||||||
);
|
);
|
||||||
@ -308,7 +307,7 @@ export const initialExeSqlValues = {
|
|||||||
export const initialSwitchValues = {
|
export const initialSwitchValues = {
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
logical_operator: SwitchLogicOperatorOptions[0],
|
logical_operator: SwitchLogicOperator.And,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
operator: SwitchOperatorOptions[0].value,
|
operator: SwitchOperatorOptions[0].value,
|
||||||
|
|||||||
@ -11,16 +11,17 @@ import {
|
|||||||
import { RAGFlowSelect } from '@/components/ui/select';
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { SwitchLogicOperator } from '@/constants/agent';
|
||||||
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
|
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
|
||||||
|
import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { SwitchLogicOperatorOptions } from '../../constant';
|
|
||||||
import { IOperatorForm } from '../../interface';
|
import { IOperatorForm } from '../../interface';
|
||||||
import { FormWrapper } from '../components/form-wrapper';
|
import { FormWrapper } from '../components/form-wrapper';
|
||||||
import { QueryVariable } from '../components/query-variable';
|
import { QueryVariable } from '../components/query-variable';
|
||||||
@ -185,12 +186,7 @@ function SwitchForm({ node }: IOperatorForm) {
|
|||||||
control: form.control,
|
control: form.control,
|
||||||
});
|
});
|
||||||
|
|
||||||
const switchLogicOperatorOptions = useMemo(() => {
|
const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions();
|
||||||
return SwitchLogicOperatorOptions.map((x) => ({
|
|
||||||
value: x,
|
|
||||||
label: t(`flow.switchLogicOperatorOptions.${x}`),
|
|
||||||
}));
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
useWatchFormChange(node?.id, form);
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
@ -253,7 +249,7 @@ function SwitchForm({ node }: IOperatorForm) {
|
|||||||
<BlockButton
|
<BlockButton
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
append({
|
append({
|
||||||
logical_operator: SwitchLogicOperatorOptions[0],
|
logical_operator: SwitchLogicOperator.And,
|
||||||
[ItemKey]: [
|
[ItemKey]: [
|
||||||
{
|
{
|
||||||
operator: switchOperatorOptions[0].value,
|
operator: switchOperatorOptions[0].value,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const GlobalFormFields = [
|
|||||||
placeholder: t('common.namePlaceholder'),
|
placeholder: t('common.namePlaceholder'),
|
||||||
required: true,
|
required: true,
|
||||||
validation: {
|
validation: {
|
||||||
pattern: /^[a-zA-Z_]+$/,
|
pattern: /^[a-zA-Z_0-9]+$/,
|
||||||
message: t('flow.variableNameMessage'),
|
message: t('flow.variableNameMessage'),
|
||||||
},
|
},
|
||||||
type: FormFieldType.Text,
|
type: FormFieldType.Text,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { BlockButton, Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Segmented } from '@/components/ui/segmented';
|
import { Segmented } from '@/components/ui/segmented';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import { Trash2, X } from 'lucide-react';
|
import { Trash2, X } from 'lucide-react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FieldValues } from 'react-hook-form';
|
import { FieldValues } from 'react-hook-form';
|
||||||
@ -36,14 +37,19 @@ export const useObjectFields = () => {
|
|||||||
path: (string | number)[] = [],
|
path: (string | number)[] = [],
|
||||||
): Array<{ path: (string | number)[]; message: string }> => {
|
): Array<{ path: (string | number)[]; message: string }> => {
|
||||||
const errors: Array<{ path: (string | number)[]; message: string }> = [];
|
const errors: Array<{ path: (string | number)[]; message: string }> = [];
|
||||||
|
if (typeof obj === 'object' && !Array.isArray(obj)) {
|
||||||
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
if (isEmpty(obj)) {
|
||||||
|
errors.push({
|
||||||
|
path: [...path],
|
||||||
|
message: 'No empty parameters are allowed.',
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj.hasOwnProperty(key)) {
|
if (obj.hasOwnProperty(key)) {
|
||||||
if (!/^[a-zA-Z_]+$/.test(key)) {
|
if (!/^[a-zA-Z_0-9]+$/.test(key)) {
|
||||||
errors.push({
|
errors.push({
|
||||||
path: [...path, key],
|
path: [...path, key],
|
||||||
message: `Key "${key}" is invalid. Keys can only contain letters and underscores.`,
|
message: `Key "${key}" is invalid. Keys can only contain letters and underscores and numbers.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const nestedErrors = validateKeys(obj[key], [...path, key]);
|
const nestedErrors = validateKeys(obj[key], [...path, key]);
|
||||||
@ -108,6 +114,21 @@ export const useObjectFields = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const arrayObjectValidate = useCallback((value: any) => {
|
||||||
|
try {
|
||||||
|
if (validateKeys(value, [])?.length > 0) {
|
||||||
|
throw new Error(t('flow.formatTypeError'));
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'string' && !JSON.parse(value)) {
|
||||||
|
throw new Error(t('flow.formatTypeError'));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('object-render-error', e, value);
|
||||||
|
throw new Error(t('flow.formatTypeError'));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const arrayStringRender = useCallback((field: FieldValues, type = 'text') => {
|
const arrayStringRender = useCallback((field: FieldValues, type = 'text') => {
|
||||||
const values = Array.isArray(field.value)
|
const values = Array.isArray(field.value)
|
||||||
? field.value
|
? field.value
|
||||||
@ -253,8 +274,9 @@ export const useObjectFields = () => {
|
|||||||
const handleCustomValidate = (value: TypesWithArray) => {
|
const handleCustomValidate = (value: TypesWithArray) => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case TypesWithArray.Object:
|
case TypesWithArray.Object:
|
||||||
case TypesWithArray.ArrayObject:
|
|
||||||
return objectValidate;
|
return objectValidate;
|
||||||
|
case TypesWithArray.ArrayObject:
|
||||||
|
return arrayObjectValidate;
|
||||||
case TypesWithArray.ArrayString:
|
case TypesWithArray.ArrayString:
|
||||||
return arrayStringValidate;
|
return arrayStringValidate;
|
||||||
case TypesWithArray.ArrayNumber:
|
case TypesWithArray.ArrayNumber:
|
||||||
@ -284,6 +306,7 @@ export const useObjectFields = () => {
|
|||||||
return {
|
return {
|
||||||
objectRender,
|
objectRender,
|
||||||
objectValidate,
|
objectValidate,
|
||||||
|
arrayObjectValidate,
|
||||||
arrayStringRender,
|
arrayStringRender,
|
||||||
arrayStringValidate,
|
arrayStringValidate,
|
||||||
arrayNumberRender,
|
arrayNumberRender,
|
||||||
|
|||||||
@ -1,236 +0,0 @@
|
|||||||
import { SliderInputSwitchFormField } from '@/components/llm-setting-items/slider';
|
|
||||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@/components/ui/form';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import {
|
|
||||||
LlmModelType,
|
|
||||||
ModelVariableType,
|
|
||||||
settledModelVariableMap,
|
|
||||||
} from '@/constants/knowledge';
|
|
||||||
import { useTranslate } from '@/hooks/common-hooks';
|
|
||||||
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
|
|
||||||
import { camelCase, isEqual } from 'lodash';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useFormContext } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
interface LlmSettingFieldItemsProps {
|
|
||||||
prefix?: string;
|
|
||||||
options?: any[];
|
|
||||||
}
|
|
||||||
const LlmSettingEnableSchema = {
|
|
||||||
temperatureEnabled: z.boolean(),
|
|
||||||
topPEnabled: z.boolean(),
|
|
||||||
presencePenaltyEnabled: z.boolean(),
|
|
||||||
frequencyPenaltyEnabled: z.boolean(),
|
|
||||||
};
|
|
||||||
export const LlmSettingSchema = {
|
|
||||||
llm_id: z.string(),
|
|
||||||
parameter: z.string().optional(),
|
|
||||||
temperature: z.coerce.number().optional(),
|
|
||||||
top_p: z.coerce.number().optional(),
|
|
||||||
presence_penalty: z.coerce.number().optional(),
|
|
||||||
frequency_penalty: z.coerce.number().optional(),
|
|
||||||
...LlmSettingEnableSchema,
|
|
||||||
// maxTokensEnabled: z.boolean(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LlmSettingFieldItems({
|
|
||||||
prefix,
|
|
||||||
options,
|
|
||||||
}: LlmSettingFieldItemsProps) {
|
|
||||||
const form = useFormContext();
|
|
||||||
const { t } = useTranslate('chat');
|
|
||||||
|
|
||||||
const modelOptions = useComposeLlmOptionsByModelTypes([
|
|
||||||
LlmModelType.Chat,
|
|
||||||
LlmModelType.Image2text,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(parameter: string) => {
|
|
||||||
const values =
|
|
||||||
settledModelVariableMap[
|
|
||||||
parameter as keyof typeof settledModelVariableMap
|
|
||||||
];
|
|
||||||
const enabledKeys = Object.keys(LlmSettingEnableSchema);
|
|
||||||
|
|
||||||
for (const key in values) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
|
||||||
const element = values[key as keyof typeof values];
|
|
||||||
form.setValue(`${prefix}.${key}`, element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (enabledKeys && enabledKeys.length) {
|
|
||||||
for (const key of enabledKeys) {
|
|
||||||
form.setValue(`${prefix}.${key}`, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[form, prefix],
|
|
||||||
);
|
|
||||||
|
|
||||||
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
|
|
||||||
label: t(camelCase(x)),
|
|
||||||
value: x,
|
|
||||||
})) as unknown as { label: string; value: ModelVariableType | 'Custom' }[];
|
|
||||||
parameterOptions.push({
|
|
||||||
label: t(camelCase('Custom')),
|
|
||||||
value: 'Custom',
|
|
||||||
});
|
|
||||||
|
|
||||||
const getFieldWithPrefix = useCallback(
|
|
||||||
(name: string) => {
|
|
||||||
return prefix ? `${prefix}.${name}` : name;
|
|
||||||
},
|
|
||||||
[prefix],
|
|
||||||
);
|
|
||||||
|
|
||||||
const checkParameterIsEquel = () => {
|
|
||||||
const [
|
|
||||||
parameter,
|
|
||||||
topPValue,
|
|
||||||
frequencyPenaltyValue,
|
|
||||||
temperatureValue,
|
|
||||||
presencePenaltyValue,
|
|
||||||
] = form.getValues([
|
|
||||||
getFieldWithPrefix('parameter'),
|
|
||||||
getFieldWithPrefix('temperature'),
|
|
||||||
getFieldWithPrefix('top_p'),
|
|
||||||
getFieldWithPrefix('frequency_penalty'),
|
|
||||||
getFieldWithPrefix('presence_penalty'),
|
|
||||||
]);
|
|
||||||
if (parameter && parameter !== 'Custom') {
|
|
||||||
const parameterValue =
|
|
||||||
settledModelVariableMap[parameter as keyof typeof ModelVariableType];
|
|
||||||
const parameterRealValue = {
|
|
||||||
top_p: topPValue,
|
|
||||||
temperature: temperatureValue,
|
|
||||||
frequency_penalty: frequencyPenaltyValue,
|
|
||||||
presence_penalty: presencePenaltyValue,
|
|
||||||
};
|
|
||||||
if (!isEqual(parameterValue, parameterRealValue)) {
|
|
||||||
form.setValue(getFieldWithPrefix('parameter'), 'Custom');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-5">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={getFieldWithPrefix('llm_id')}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<span className="text-destructive mr-1"> *</span>
|
|
||||||
{t('model')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<SelectWithSearch
|
|
||||||
options={options || modelOptions}
|
|
||||||
triggerClassName="!bg-bg-input"
|
|
||||||
{...field}
|
|
||||||
></SelectWithSearch>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={getFieldWithPrefix('parameter')}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex justify-between gap-4 items-center">
|
|
||||||
<FormLabel>{t('freedom')}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="w-28">
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
handleChange(val);
|
|
||||||
field.onChange(val);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{parameterOptions.map((x) => (
|
|
||||||
<SelectItem value={x.value} key={x.value}>
|
|
||||||
{x.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<SliderInputSwitchFormField
|
|
||||||
name={getFieldWithPrefix('temperature')}
|
|
||||||
checkName={getFieldWithPrefix('temperatureEnabled')}
|
|
||||||
label="temperature"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
step={0.01}
|
|
||||||
onChange={() => {
|
|
||||||
checkParameterIsEquel();
|
|
||||||
}}
|
|
||||||
></SliderInputSwitchFormField>
|
|
||||||
<SliderInputSwitchFormField
|
|
||||||
name={getFieldWithPrefix('top_p')}
|
|
||||||
checkName={getFieldWithPrefix('topPEnabled')}
|
|
||||||
label="topP"
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
min={0}
|
|
||||||
onChange={() => {
|
|
||||||
checkParameterIsEquel();
|
|
||||||
}}
|
|
||||||
></SliderInputSwitchFormField>
|
|
||||||
<SliderInputSwitchFormField
|
|
||||||
name={getFieldWithPrefix('presence_penalty')}
|
|
||||||
checkName={getFieldWithPrefix('presencePenaltyEnabled')}
|
|
||||||
label="presencePenalty"
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
min={0}
|
|
||||||
onChange={() => {
|
|
||||||
checkParameterIsEquel();
|
|
||||||
}}
|
|
||||||
></SliderInputSwitchFormField>
|
|
||||||
<SliderInputSwitchFormField
|
|
||||||
name={getFieldWithPrefix('frequency_penalty')}
|
|
||||||
checkName={getFieldWithPrefix('frequencyPenaltyEnabled')}
|
|
||||||
label="frequencyPenalty"
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
min={0}
|
|
||||||
onChange={() => {
|
|
||||||
checkParameterIsEquel();
|
|
||||||
}}
|
|
||||||
></SliderInputSwitchFormField>
|
|
||||||
{/* <SliderInputSwitchFormField
|
|
||||||
name={getFieldWithPrefix('max_tokens')}
|
|
||||||
checkName="maxTokensEnabled"
|
|
||||||
label="maxTokens"
|
|
||||||
max={128000}
|
|
||||||
></SliderInputSwitchFormField> */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +1,10 @@
|
|||||||
// src/pages/next-search/search-setting.tsx
|
// src/pages/next-search/search-setting.tsx
|
||||||
|
|
||||||
import { AvatarUpload } from '@/components/avatar-upload';
|
import { AvatarUpload } from '@/components/avatar-upload';
|
||||||
|
import {
|
||||||
|
LlmSettingFieldItems,
|
||||||
|
LlmSettingSchema,
|
||||||
|
} from '@/components/llm-setting-items/next';
|
||||||
import {
|
import {
|
||||||
MetadataFilter,
|
MetadataFilter,
|
||||||
MetadataFilterSchema,
|
MetadataFilterSchema,
|
||||||
@ -46,10 +50,10 @@ import {
|
|||||||
IllmSettingProps,
|
IllmSettingProps,
|
||||||
useUpdateSearch,
|
useUpdateSearch,
|
||||||
} from '../next-searches/hooks';
|
} from '../next-searches/hooks';
|
||||||
import {
|
// import {
|
||||||
LlmSettingFieldItems,
|
// LlmSettingFieldItems,
|
||||||
LlmSettingSchema,
|
// LlmSettingSchema,
|
||||||
} from './search-setting-aisummery-config';
|
// } from './search-setting-aisummery-config';
|
||||||
|
|
||||||
interface SearchSettingProps {
|
interface SearchSettingProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -397,6 +401,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
isTooltipShown
|
isTooltipShown
|
||||||
similarityName="search_config.similarity_threshold"
|
similarityName="search_config.similarity_threshold"
|
||||||
vectorSimilarityWeightName="search_config.vector_similarity_weight"
|
vectorSimilarityWeightName="search_config.vector_similarity_weight"
|
||||||
|
numberInputClassName="rounded-sm"
|
||||||
></SimilaritySliderFormField>
|
></SimilaritySliderFormField>
|
||||||
{/* Rerank Model */}
|
{/* Rerank Model */}
|
||||||
<FormField
|
<FormField
|
||||||
@ -462,7 +467,7 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type={'number'}
|
type={'number'}
|
||||||
className="h-7 w-20 bg-bg-card"
|
className="h-7 w-20 bg-bg-card border border-border-button rounded-sm"
|
||||||
max={2048}
|
max={2048}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
@ -493,9 +498,19 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{aiSummaryDisabled && (
|
{aiSummaryDisabled && (
|
||||||
|
// <LlmSettingFieldItems
|
||||||
|
// prefix="search_config.llm_setting"
|
||||||
|
// options={aiSummeryModelOptions}
|
||||||
|
// ></LlmSettingFieldItems>
|
||||||
<LlmSettingFieldItems
|
<LlmSettingFieldItems
|
||||||
prefix="search_config.llm_setting"
|
prefix="search_config.llm_setting"
|
||||||
options={aiSummeryModelOptions}
|
options={aiSummeryModelOptions}
|
||||||
|
showFields={[
|
||||||
|
'temperature',
|
||||||
|
'top_p',
|
||||||
|
'presence_penalty',
|
||||||
|
'frequency_penalty',
|
||||||
|
]}
|
||||||
></LlmSettingFieldItems>
|
></LlmSettingFieldItems>
|
||||||
)}
|
)}
|
||||||
{/* Feature Controls */}
|
{/* Feature Controls */}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export enum DataSourceKey {
|
|||||||
NOTION = 'notion',
|
NOTION = 'notion',
|
||||||
DISCORD = 'discord',
|
DISCORD = 'discord',
|
||||||
GOOGLE_DRIVE = 'google_drive',
|
GOOGLE_DRIVE = 'google_drive',
|
||||||
|
MOODLE = 'moodle',
|
||||||
// GMAIL = 'gmail',
|
// GMAIL = 'gmail',
|
||||||
JIRA = 'jira',
|
JIRA = 'jira',
|
||||||
// SHAREPOINT = 'sharepoint',
|
// SHAREPOINT = 'sharepoint',
|
||||||
@ -42,6 +43,11 @@ export const DataSourceInfo = {
|
|||||||
description: t(`setting.${DataSourceKey.GOOGLE_DRIVE}Description`),
|
description: t(`setting.${DataSourceKey.GOOGLE_DRIVE}Description`),
|
||||||
icon: <SvgIcon name={'data-source/google-drive'} width={38} />,
|
icon: <SvgIcon name={'data-source/google-drive'} width={38} />,
|
||||||
},
|
},
|
||||||
|
[DataSourceKey.MOODLE]: {
|
||||||
|
name: 'Moodle',
|
||||||
|
description: t(`setting.${DataSourceKey.MOODLE}Description`),
|
||||||
|
icon: <SvgIcon name={'data-source/moodle'} width={38} />,
|
||||||
|
},
|
||||||
[DataSourceKey.JIRA]: {
|
[DataSourceKey.JIRA]: {
|
||||||
name: 'Jira',
|
name: 'Jira',
|
||||||
description: t(`setting.${DataSourceKey.JIRA}Description`),
|
description: t(`setting.${DataSourceKey.JIRA}Description`),
|
||||||
@ -116,7 +122,7 @@ export const DataSourceFormFields = {
|
|||||||
required: false,
|
required: false,
|
||||||
placeholder: 'https://fsn1.your-objectstorage.com',
|
placeholder: 'https://fsn1.your-objectstorage.com',
|
||||||
tooltip: t('setting.S3CompatibleEndpointUrlTip'),
|
tooltip: t('setting.S3CompatibleEndpointUrlTip'),
|
||||||
shouldRender: (formValues) => {
|
shouldRender: (formValues: any) => {
|
||||||
return formValues?.config?.bucket_type === 's3_compatible';
|
return formValues?.config?.bucket_type === 's3_compatible';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -287,6 +293,21 @@ export const DataSourceFormFields = {
|
|||||||
defaultValue: 'uploaded',
|
defaultValue: 'uploaded',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[DataSourceKey.MOODLE]: [
|
||||||
|
{
|
||||||
|
label: 'Moodle URL',
|
||||||
|
name: 'config.moodle_url',
|
||||||
|
type: FormFieldType.Text,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'https://moodle.example.com',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API Token',
|
||||||
|
name: 'config.credentials.moodle_token',
|
||||||
|
type: FormFieldType.Password,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
[DataSourceKey.JIRA]: [
|
[DataSourceKey.JIRA]: [
|
||||||
{
|
{
|
||||||
label: 'Jira Base URL',
|
label: 'Jira Base URL',
|
||||||
@ -456,6 +477,16 @@ export const DataSourceFormDefaultValues = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[DataSourceKey.MOODLE]: {
|
||||||
|
name: '',
|
||||||
|
source: DataSourceKey.MOODLE,
|
||||||
|
config: {
|
||||||
|
moodle_url: '',
|
||||||
|
credentials: {
|
||||||
|
moodle_token: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
[DataSourceKey.JIRA]: {
|
[DataSourceKey.JIRA]: {
|
||||||
name: '',
|
name: '',
|
||||||
source: DataSourceKey.JIRA,
|
source: DataSourceKey.JIRA,
|
||||||
|
|||||||
@ -44,6 +44,12 @@ const dataSourceTemplates = [
|
|||||||
description: DataSourceInfo[DataSourceKey.NOTION].description,
|
description: DataSourceInfo[DataSourceKey.NOTION].description,
|
||||||
icon: DataSourceInfo[DataSourceKey.NOTION].icon,
|
icon: DataSourceInfo[DataSourceKey.NOTION].icon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: DataSourceKey.MOODLE,
|
||||||
|
name: DataSourceInfo[DataSourceKey.MOODLE].name,
|
||||||
|
description: DataSourceInfo[DataSourceKey.MOODLE].description,
|
||||||
|
icon: DataSourceInfo[DataSourceKey.MOODLE].icon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: DataSourceKey.JIRA,
|
id: DataSourceKey.JIRA,
|
||||||
name: DataSourceInfo[DataSourceKey.JIRA].name,
|
name: DataSourceInfo[DataSourceKey.JIRA].name,
|
||||||
|
|||||||
Reference in New Issue
Block a user