Merge branch 'main' into feature/OND211-2329-Check-existing-REST-endponts-and-extend-with-new-requested-endpoints
This commit is contained in:
commit
28029bdbb8
23 changed files with 3708 additions and 3881 deletions
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
|
|
@ -12,7 +12,7 @@ on:
|
|||
# The only difference between pull_request and pull_request_target is the context in which the workflow runs:
|
||||
# — pull_request_target workflows use the workflow files from the default branch, and secrets are available.
|
||||
# — pull_request workflows use the workflow files from the pull request branch, and secrets are unavailable.
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types: [ synchronize, ready_for_review ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
name: ragflow_tests
|
||||
# https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution
|
||||
# https://github.com/orgs/community/discussions/26261
|
||||
if: ${{ github.event_name != 'pull_request_target' || (contains(github.event.pull_request.labels.*.name, 'ci') && github.event.pull_request.mergeable != false) }}
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'ci')) }}
|
||||
runs-on: [ "self-hosted", "ragflow-test" ]
|
||||
steps:
|
||||
# https://github.com/hmarr/debug-action
|
||||
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
- name: Check workflow duplication
|
||||
if: ${{ !cancelled() && !failure() }}
|
||||
run: |
|
||||
if [[ ${GITHUB_EVENT_NAME} != "pull_request_target" && ${GITHUB_EVENT_NAME} != "schedule" ]]; then
|
||||
if [[ ${GITHUB_EVENT_NAME} != "pull_request" && ${GITHUB_EVENT_NAME} != "schedule" ]]; then
|
||||
HEAD=$(git rev-parse HEAD)
|
||||
# Find a PR that introduced a given commit
|
||||
gh auth login --with-token <<< "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
|
@ -78,7 +78,7 @@ jobs:
|
|||
fi
|
||||
fi
|
||||
fi
|
||||
elif [[ ${GITHUB_EVENT_NAME} == "pull_request_target" ]]; then
|
||||
elif [[ ${GITHUB_EVENT_NAME} == "pull_request" ]]; then
|
||||
PR_NUMBER=${{ github.event.pull_request.number }}
|
||||
PR_SHA_FP=${RUNNER_WORKSPACE_PREFIX}/artifacts/${GITHUB_REPOSITORY}/PR_${PR_NUMBER}
|
||||
# Calculate the hash of the current workspace content
|
||||
|
|
@ -98,7 +98,7 @@ jobs:
|
|||
- name: Check comments of changed Python files
|
||||
if: ${{ false }}
|
||||
run: |
|
||||
if [[ ${{ github.event_name }} == 'pull_request_target' ]]; then
|
||||
if [[ ${{ github.event_name }} == 'pull_request' || ${{ github.event_name }} == 'pull_request_target' ]]; then
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} \
|
||||
| grep -E '\.(py)$' || true)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class MessageParam(ComponentParamBase):
|
|||
self.content = []
|
||||
self.stream = True
|
||||
self.output_format = None # default output format
|
||||
self.auto_play = False
|
||||
self.outputs = {
|
||||
"content": {
|
||||
"type": "str"
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
import tempfile
|
||||
from quart import Response, request
|
||||
from api.apps import current_user, login_required
|
||||
from api.db.db_models import APIToken
|
||||
|
|
@ -248,6 +250,64 @@ async def completion():
|
|||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
@manager.route("/sequence2txt", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def sequence2txt():
|
||||
req = await request.form
|
||||
stream_mode = req.get("stream", "false").lower() == "true"
|
||||
files = await request.files
|
||||
if "file" not in files:
|
||||
return get_data_error_result(message="Missing 'file' in multipart form-data")
|
||||
|
||||
uploaded = files["file"]
|
||||
|
||||
ALLOWED_EXTS = {
|
||||
".wav", ".mp3", ".m4a", ".aac",
|
||||
".flac", ".ogg", ".webm",
|
||||
".opus", ".wma"
|
||||
}
|
||||
|
||||
filename = uploaded.filename or ""
|
||||
suffix = os.path.splitext(filename)[-1].lower()
|
||||
if suffix not in ALLOWED_EXTS:
|
||||
return get_data_error_result(message=
|
||||
f"Unsupported audio format: {suffix}. "
|
||||
f"Allowed: {', '.join(sorted(ALLOWED_EXTS))}"
|
||||
)
|
||||
fd, temp_audio_path = tempfile.mkstemp(suffix=suffix)
|
||||
os.close(fd)
|
||||
await uploaded.save(temp_audio_path)
|
||||
|
||||
tenants = TenantService.get_info_by(current_user.id)
|
||||
if not tenants:
|
||||
return get_data_error_result(message="Tenant not found!")
|
||||
|
||||
asr_id = tenants[0]["asr_id"]
|
||||
if not asr_id:
|
||||
return get_data_error_result(message="No default ASR model is set")
|
||||
|
||||
asr_mdl=LLMBundle(tenants[0]["tenant_id"], LLMType.SPEECH2TEXT, asr_id)
|
||||
if not stream_mode:
|
||||
text = asr_mdl.transcription(temp_audio_path)
|
||||
try:
|
||||
os.remove(temp_audio_path)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to remove temp audio file: {str(e)}")
|
||||
return get_json_result(data={"text": text})
|
||||
async def event_stream():
|
||||
try:
|
||||
for evt in asr_mdl.stream_transcription(temp_audio_path):
|
||||
yield f"data: {json.dumps(evt, ensure_ascii=False)}\n\n"
|
||||
except Exception as e:
|
||||
err = {"event": "error", "text": str(e)}
|
||||
yield f"data: {json.dumps(err, ensure_ascii=False)}\n\n"
|
||||
finally:
|
||||
try:
|
||||
os.remove(temp_audio_path)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to remove temp audio file: {str(e)}")
|
||||
|
||||
return Response(event_stream(), content_type="text/event-stream")
|
||||
|
||||
@manager.route("/tts", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ async def convert():
|
|||
doc = DocumentService.insert({
|
||||
"id": get_uuid(),
|
||||
"kb_id": kb.id,
|
||||
"parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
|
||||
"parser_id": kb.parser_id,
|
||||
"pipeline_id": kb.pipeline_id,
|
||||
"parser_config": kb.parser_config,
|
||||
"created_by": current_user.id,
|
||||
"type": file.type,
|
||||
|
|
|
|||
|
|
@ -179,6 +179,9 @@ class DialogService(CommonService):
|
|||
return res
|
||||
|
||||
def chat_solo(dialog, messages, stream=True):
|
||||
attachments = ""
|
||||
if "files" in messages[-1]:
|
||||
attachments = "\n\n".join(FileService.get_files(messages[-1]["files"]))
|
||||
if TenantLLMService.llm_id2llm_type(dialog.llm_id) == "image2text":
|
||||
chat_mdl = LLMBundle(dialog.tenant_id, LLMType.IMAGE2TEXT, dialog.llm_id)
|
||||
else:
|
||||
|
|
@ -189,6 +192,8 @@ def chat_solo(dialog, messages, stream=True):
|
|||
if prompt_config.get("tts"):
|
||||
tts_mdl = LLMBundle(dialog.tenant_id, LLMType.TTS)
|
||||
msg = [{"role": m["role"], "content": re.sub(r"##\d+\$\$", "", m["content"])} for m in messages if m["role"] != "system"]
|
||||
if attachments and msg:
|
||||
msg[-1]["content"] += attachments
|
||||
if stream:
|
||||
last_ans = ""
|
||||
delta_ans = ""
|
||||
|
|
|
|||
|
|
@ -185,6 +185,66 @@ class LLMBundle(LLM4Tenant):
|
|||
|
||||
return txt
|
||||
|
||||
def stream_transcription(self, audio):
|
||||
mdl = self.mdl
|
||||
supports_stream = hasattr(mdl, "stream_transcription") and callable(getattr(mdl, "stream_transcription"))
|
||||
if supports_stream:
|
||||
if self.langfuse:
|
||||
generation = self.langfuse.start_generation(
|
||||
trace_context=self.trace_context,
|
||||
name="stream_transcription",
|
||||
metadata={"model": self.llm_name}
|
||||
)
|
||||
final_text = ""
|
||||
used_tokens = 0
|
||||
|
||||
try:
|
||||
for evt in mdl.stream_transcription(audio):
|
||||
if evt.get("event") == "final":
|
||||
final_text = evt.get("text", "")
|
||||
|
||||
yield evt
|
||||
|
||||
except Exception as e:
|
||||
err = {"event": "error", "text": str(e)}
|
||||
yield err
|
||||
final_text = final_text or ""
|
||||
finally:
|
||||
if final_text:
|
||||
used_tokens = num_tokens_from_string(final_text)
|
||||
TenantLLMService.increase_usage(self.tenant_id, self.llm_type, used_tokens)
|
||||
|
||||
if self.langfuse:
|
||||
generation.update(
|
||||
output={"output": final_text},
|
||||
usage_details={"total_tokens": used_tokens}
|
||||
)
|
||||
generation.end()
|
||||
|
||||
return
|
||||
|
||||
if self.langfuse:
|
||||
generation = self.langfuse.start_generation(trace_context=self.trace_context, name="stream_transcription", metadata={"model": self.llm_name})
|
||||
full_text, used_tokens = mdl.transcription(audio)
|
||||
if not TenantLLMService.increase_usage(
|
||||
self.tenant_id, self.llm_type, used_tokens
|
||||
):
|
||||
logging.error(
|
||||
f"LLMBundle.stream_transcription can't update token usage for {self.tenant_id}/SEQUENCE2TXT used_tokens: {used_tokens}"
|
||||
)
|
||||
if self.langfuse:
|
||||
generation.update(
|
||||
output={"output": full_text},
|
||||
usage_details={"total_tokens": used_tokens}
|
||||
)
|
||||
generation.end()
|
||||
|
||||
yield {
|
||||
"event": "final",
|
||||
"text": full_text,
|
||||
"streaming": False
|
||||
}
|
||||
|
||||
def tts(self, text: str) -> Generator[bytes, None, None]:
|
||||
if self.langfuse:
|
||||
generation = self.langfuse.start_generation(trace_context=self.trace_context, name="tts", input={"text": text})
|
||||
|
|
|
|||
|
|
@ -714,19 +714,13 @@
|
|||
"model_type": "rerank"
|
||||
},
|
||||
{
|
||||
"llm_name": "qwen-audio-asr",
|
||||
"llm_name": "qwen3-asr-flash",
|
||||
"tags": "SPEECH2TEXT,8k",
|
||||
"max_tokens": 8000,
|
||||
"model_type": "speech2text"
|
||||
},
|
||||
{
|
||||
"llm_name": "qwen-audio-asr-latest",
|
||||
"tags": "SPEECH2TEXT,8k",
|
||||
"max_tokens": 8000,
|
||||
"model_type": "speech2text"
|
||||
},
|
||||
{
|
||||
"llm_name": "qwen-audio-asr-1204",
|
||||
"llm_name": "qwen3-asr-flash-2025-09-08",
|
||||
"tags": "SPEECH2TEXT,8k",
|
||||
"max_tokens": 8000,
|
||||
"model_type": "speech2text"
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@ dependencies = [
|
|||
"pypandoc>=1.16",
|
||||
"pyobvector==0.2.18",
|
||||
"exceptiongroup>=1.3.0,<2.0.0",
|
||||
"crypto>=1.4.1",
|
||||
"ffmpeg-python>=0.2.0",
|
||||
"imageio-ffmpeg>=0.6.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
|
@ -169,6 +170,9 @@ test = [
|
|||
"requests-toolbelt>=1.0.0",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://mirrors.aliyun.com/pypi/simple"
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import json
|
|||
import os
|
||||
import re
|
||||
from abc import ABC
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
from openai import OpenAI
|
||||
|
|
@ -68,32 +69,80 @@ class QWenSeq2txt(Base):
|
|||
self.model_name = model_name
|
||||
|
||||
def transcription(self, audio_path):
|
||||
if "paraformer" in self.model_name or "sensevoice" in self.model_name:
|
||||
return f"**ERROR**: model {self.model_name} is not suppported yet.", 0
|
||||
import dashscope
|
||||
|
||||
from dashscope import MultiModalConversation
|
||||
if audio_path.startswith("http"):
|
||||
audio_input = audio_path
|
||||
else:
|
||||
audio_input = f"file://{audio_path}"
|
||||
|
||||
audio_path = f"file://{audio_path}"
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{"text": ""}]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"audio": audio_path}],
|
||||
"content": [{"audio": audio_input}]
|
||||
}
|
||||
]
|
||||
|
||||
response = None
|
||||
full_content = ""
|
||||
try:
|
||||
response = MultiModalConversation.call(model="qwen-audio-asr", messages=messages, result_format="message", stream=True)
|
||||
for response in response:
|
||||
try:
|
||||
full_content += response["output"]["choices"][0]["message"].content[0]["text"]
|
||||
except Exception:
|
||||
pass
|
||||
return full_content, num_tokens_from_string(full_content)
|
||||
except Exception as e:
|
||||
return "**ERROR**: " + str(e), 0
|
||||
resp = dashscope.MultiModalConversation.call(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
result_format="message",
|
||||
asr_options={
|
||||
"enable_lid": True,
|
||||
"enable_itn": False
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
text = resp["output"]["choices"][0]["message"].content[0]["text"]
|
||||
except Exception as e:
|
||||
text = "**ERROR**: " + str(e)
|
||||
return text, num_tokens_from_string(text)
|
||||
|
||||
def stream_transcription(self, audio_path):
|
||||
import dashscope
|
||||
|
||||
if audio_path.startswith("http"):
|
||||
audio_input = audio_path
|
||||
else:
|
||||
audio_input = f"file://{audio_path}"
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": [{"text": ""}]
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"audio": audio_input}]
|
||||
}
|
||||
]
|
||||
|
||||
stream = dashscope.MultiModalConversation.call(
|
||||
model=self.model_name,
|
||||
messages=messages,
|
||||
result_format="message",
|
||||
stream=True,
|
||||
asr_options={
|
||||
"enable_lid": True,
|
||||
"enable_itn": False
|
||||
}
|
||||
)
|
||||
|
||||
full = ""
|
||||
for chunk in stream:
|
||||
try:
|
||||
piece = chunk["output"]["choices"][0]["message"].content[0]["text"]
|
||||
full = piece
|
||||
yield {"event": "delta", "text": piece}
|
||||
except Exception as e:
|
||||
yield {"event": "error", "text": str(e)}
|
||||
|
||||
yield {"event": "final", "text": full}
|
||||
|
||||
class AzureSeq2txt(Base):
|
||||
_FACTORY_NAME = "Azure-OpenAI"
|
||||
|
|
@ -268,6 +317,27 @@ class ZhipuSeq2txt(Base):
|
|||
self.gen_conf = kwargs.get("gen_conf", {})
|
||||
self.stream = kwargs.get("stream", False)
|
||||
|
||||
def _convert_to_wav(self, input_path):
|
||||
ext = os.path.splitext(input_path)[1].lower()
|
||||
if ext in [".wav", ".mp3"]:
|
||||
return input_path
|
||||
fd, out_path = tempfile.mkstemp(suffix=".wav")
|
||||
os.close(fd)
|
||||
try:
|
||||
import ffmpeg
|
||||
import imageio_ffmpeg as ffmpeg_exe
|
||||
ffmpeg_path = ffmpeg_exe.get_ffmpeg_exe()
|
||||
(
|
||||
ffmpeg
|
||||
.input(input_path)
|
||||
.output(out_path, ar=16000, ac=1)
|
||||
.overwrite_output()
|
||||
.run(cmd=ffmpeg_path,quiet=True)
|
||||
)
|
||||
return out_path
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"audio convert failed: {e}")
|
||||
|
||||
def transcription(self, audio_path):
|
||||
payload = {
|
||||
"model": self.model_name,
|
||||
|
|
@ -276,7 +346,9 @@ class ZhipuSeq2txt(Base):
|
|||
}
|
||||
|
||||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||||
with open(audio_path, "rb") as audio_file:
|
||||
converted = self._convert_to_wav(audio_path)
|
||||
|
||||
with open(converted, "rb") as audio_file:
|
||||
files = {"file": audio_file}
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -645,7 +645,7 @@ class Dealer:
|
|||
if not chunks:
|
||||
return []
|
||||
idx_nms = [index_name(tid) for tid in tenant_ids]
|
||||
mom_chunks = defaultdict([])
|
||||
mom_chunks = defaultdict(list)
|
||||
i = 0
|
||||
while i < len(chunks):
|
||||
ck = chunks[i]
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class SyncBase:
|
|||
failed_docs += len(docs)
|
||||
continue
|
||||
|
||||
prefix = "[Jira] " if self.SOURCE_NAME == FileSource.JIRA else ""
|
||||
prefix = self._get_source_prefix()
|
||||
if failed_docs > 0:
|
||||
logging.info(f"{prefix}{doc_num} docs synchronized till {next_update} ({failed_docs} skipped)")
|
||||
else:
|
||||
|
|
@ -128,6 +128,9 @@ class SyncBase:
|
|||
|
||||
async def _generate(self, task: dict):
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_source_prefix(self):
|
||||
return ""
|
||||
|
||||
|
||||
class S3(SyncBase):
|
||||
|
|
@ -402,6 +405,9 @@ class GoogleDrive(SyncBase):
|
|||
class Jira(SyncBase):
|
||||
SOURCE_NAME: str = FileSource.JIRA
|
||||
|
||||
def _get_source_prefix(self):
|
||||
return "[Jira]"
|
||||
|
||||
async def _generate(self, task: dict):
|
||||
connector_kwargs = {
|
||||
"jira_base_url": self.conf["base_url"],
|
||||
|
|
|
|||
|
|
@ -1,372 +0,0 @@
|
|||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useDeleteDocument,
|
||||
useFetchDocumentInfosByIds,
|
||||
useRemoveNextDocument,
|
||||
useUploadAndParseDocument,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { formatBytes } from '@/utils/file-util';
|
||||
import {
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { GetProp, UploadFile } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Flex,
|
||||
Input,
|
||||
List,
|
||||
Space,
|
||||
Spin,
|
||||
Typography,
|
||||
Upload,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import get from 'lodash/get';
|
||||
import { CircleStop, Paperclip, SendHorizontal } from 'lucide-react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import FileIcon from '../file-icon';
|
||||
import styles from './index.less';
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
const { Text } = Typography;
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const getFileId = (file: UploadFile) => get(file, 'response.data.0');
|
||||
|
||||
const getFileIds = (fileList: UploadFile[]) => {
|
||||
const ids = fileList.reduce((pre, cur) => {
|
||||
return pre.concat(get(cur, 'response.data', []));
|
||||
}, []);
|
||||
|
||||
return ids;
|
||||
};
|
||||
|
||||
const isUploadSuccess = (file: UploadFile) => {
|
||||
const code = get(file, 'response.code');
|
||||
return typeof code === 'number' && code === 0;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
disabled: boolean;
|
||||
value: string;
|
||||
sendDisabled: boolean;
|
||||
sendLoading: boolean;
|
||||
onPressEnter(documentIds: string[]): void;
|
||||
onInputChange: ChangeEventHandler<HTMLTextAreaElement>;
|
||||
conversationId: string;
|
||||
uploadMethod?: string;
|
||||
isShared?: boolean;
|
||||
showUploadIcon?: boolean;
|
||||
createConversationBeforeUploadDocument?(message: string): Promise<any>;
|
||||
stopOutputMessage?(): void;
|
||||
}
|
||||
|
||||
const getBase64 = (file: FileType): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file as any);
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
const MessageInput = ({
|
||||
isShared = false,
|
||||
disabled,
|
||||
value,
|
||||
onPressEnter,
|
||||
sendDisabled,
|
||||
sendLoading,
|
||||
onInputChange,
|
||||
conversationId,
|
||||
showUploadIcon = true,
|
||||
createConversationBeforeUploadDocument,
|
||||
uploadMethod = 'upload_and_parse',
|
||||
stopOutputMessage,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const { removeDocument } = useRemoveNextDocument();
|
||||
const { deleteDocument } = useDeleteDocument();
|
||||
const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
|
||||
const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod);
|
||||
const conversationIdRef = useRef(conversationId);
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64(file.originFileObj as FileType);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = async ({
|
||||
// fileList: newFileList,
|
||||
file,
|
||||
}) => {
|
||||
let nextConversationId: string = conversationId;
|
||||
if (createConversationBeforeUploadDocument) {
|
||||
const creatingRet = await createConversationBeforeUploadDocument(
|
||||
file.name,
|
||||
);
|
||||
if (creatingRet?.code === 0) {
|
||||
nextConversationId = creatingRet.data.id;
|
||||
}
|
||||
}
|
||||
setFileList((list) => {
|
||||
list.push({
|
||||
...file,
|
||||
status: 'uploading',
|
||||
originFileObj: file as any,
|
||||
});
|
||||
return [...list];
|
||||
});
|
||||
const ret = await uploadAndParseDocument({
|
||||
conversationId: nextConversationId,
|
||||
fileList: [file],
|
||||
});
|
||||
setFileList((list) => {
|
||||
const nextList = list.filter((x) => x.uid !== file.uid);
|
||||
nextList.push({
|
||||
...file,
|
||||
originFileObj: file as any,
|
||||
response: ret,
|
||||
percent: 100,
|
||||
status: ret?.code === 0 ? 'done' : 'error',
|
||||
});
|
||||
return nextList;
|
||||
});
|
||||
};
|
||||
|
||||
const isUploadingFile = fileList.some((x) => x.status === 'uploading');
|
||||
|
||||
const handlePressEnter = useCallback(async () => {
|
||||
if (isUploadingFile) return;
|
||||
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
|
||||
|
||||
onPressEnter(ids);
|
||||
setFileList([]);
|
||||
}, [fileList, onPressEnter, isUploadingFile]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// check if it was shift + enter
|
||||
if (event.key === 'Enter' && event.shiftKey) return;
|
||||
if (event.key !== 'Enter') return;
|
||||
if (sendDisabled || isUploadingFile || sendLoading) return;
|
||||
|
||||
event.preventDefault();
|
||||
handlePressEnter();
|
||||
},
|
||||
[sendDisabled, isUploadingFile, sendLoading, handlePressEnter],
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
async (file: UploadFile) => {
|
||||
const ids = get(file, 'response.data', []);
|
||||
// Upload Successfully
|
||||
if (Array.isArray(ids) && ids.length) {
|
||||
if (isShared) {
|
||||
await deleteDocument(ids);
|
||||
} else {
|
||||
await removeDocument(ids[0]);
|
||||
}
|
||||
setFileList((preList) => {
|
||||
return preList.filter((x) => getFileId(x) !== ids[0]);
|
||||
});
|
||||
} else {
|
||||
// Upload failed
|
||||
setFileList((preList) => {
|
||||
return preList.filter((x) => x.uid !== file.uid);
|
||||
});
|
||||
}
|
||||
},
|
||||
[removeDocument, deleteDocument, isShared],
|
||||
);
|
||||
|
||||
const handleStopOutputMessage = useCallback(() => {
|
||||
stopOutputMessage?.();
|
||||
}, [stopOutputMessage]);
|
||||
|
||||
const getDocumentInfoById = useCallback(
|
||||
(id: string) => {
|
||||
return documentInfos.find((x) => x.id === id);
|
||||
},
|
||||
[documentInfos],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const ids = getFileIds(fileList);
|
||||
setDocumentIds(ids);
|
||||
}, [fileList, setDocumentIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
conversationIdRef.current &&
|
||||
conversationId !== conversationIdRef.current
|
||||
) {
|
||||
setFileList([]);
|
||||
}
|
||||
conversationIdRef.current = conversationId;
|
||||
}, [conversationId, setFileList]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={1}
|
||||
vertical
|
||||
className={cn(styles.messageInputWrapper, 'dark:bg-black')}
|
||||
>
|
||||
<TextArea
|
||||
size="large"
|
||||
placeholder={t('sendPlaceholder')}
|
||||
value={value}
|
||||
allowClear
|
||||
disabled={disabled}
|
||||
style={{
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
padding: '0px 10px',
|
||||
marginTop: 10,
|
||||
}}
|
||||
autoSize={{ minRows: 2, maxRows: 10 }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<Divider style={{ margin: '5px 30px 10px 0px' }} />
|
||||
<Flex justify="space-between" align="center">
|
||||
{fileList.length > 0 && (
|
||||
<List
|
||||
grid={{
|
||||
gutter: 16,
|
||||
xs: 1,
|
||||
sm: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 2,
|
||||
xxl: 4,
|
||||
}}
|
||||
dataSource={fileList}
|
||||
className={styles.listWrapper}
|
||||
renderItem={(item) => {
|
||||
const id = getFileId(item);
|
||||
const documentInfo = getDocumentInfoById(id);
|
||||
const fileExtension = getExtension(documentInfo?.name ?? '');
|
||||
const fileName = item.originFileObj?.name ?? '';
|
||||
|
||||
return (
|
||||
<List.Item>
|
||||
<Card className={styles.documentCard}>
|
||||
<Flex gap={10} align="center">
|
||||
{item.status === 'uploading' ? (
|
||||
<Spin
|
||||
indicator={
|
||||
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
||||
}
|
||||
/>
|
||||
) : item.status === 'error' ? (
|
||||
<InfoCircleOutlined size={30}></InfoCircleOutlined>
|
||||
) : (
|
||||
<FileIcon id={id} name={fileName}></FileIcon>
|
||||
)}
|
||||
<Flex vertical style={{ width: '90%' }}>
|
||||
<Text
|
||||
ellipsis={{ tooltip: fileName }}
|
||||
className={styles.nameText}
|
||||
>
|
||||
<b> {fileName}</b>
|
||||
</Text>
|
||||
{item.status === 'error' ? (
|
||||
t('uploadFailed')
|
||||
) : (
|
||||
<>
|
||||
{item.percent !== 100 ? (
|
||||
t('uploading')
|
||||
) : !item.response ? (
|
||||
t('parsing')
|
||||
) : (
|
||||
<Space>
|
||||
<span>{fileExtension?.toUpperCase()},</span>
|
||||
<span>
|
||||
{formatBytes(
|
||||
getDocumentInfoById(id)?.size ?? 0,
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{item.status !== 'uploading' && (
|
||||
<span className={styles.deleteIcon}>
|
||||
<CloseCircleOutlined
|
||||
onClick={() => handleRemove(item)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
gap={5}
|
||||
align="center"
|
||||
justify="flex-end"
|
||||
style={{
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
width: fileList.length > 0 ? '50%' : '100%',
|
||||
}}
|
||||
>
|
||||
{showUploadIcon && (
|
||||
<Upload
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
multiple={false}
|
||||
onRemove={handleRemove}
|
||||
showUploadList={false}
|
||||
beforeUpload={() => {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button type={'primary'} disabled={disabled}>
|
||||
<Paperclip className="size-4" />
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
{sendLoading ? (
|
||||
<Button onClick={handleStopOutputMessage}>
|
||||
<CircleStop className="size-5" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handlePressEnter}
|
||||
loading={sendLoading}
|
||||
disabled={sendDisabled || isUploadingFile || sendLoading}
|
||||
>
|
||||
<SendHorizontal className="size-5" />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MessageInput);
|
||||
|
|
@ -4,19 +4,16 @@ import {
|
|||
IMessage,
|
||||
IReference,
|
||||
IReferenceChunk,
|
||||
UploadResponseDataType,
|
||||
} from '@/interfaces/database/chat';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
useFetchDocumentInfosByIds,
|
||||
useFetchDocumentThumbnailsByIds,
|
||||
} from '@/hooks/document-hooks';
|
||||
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import MarkdownContent from '../markdown-content';
|
||||
import { ReferenceDocumentList } from '../next-message-item/reference-document-list';
|
||||
import { InnerUploadedMessageFiles } from '../next-message-item/uploaded-message-files';
|
||||
import { UploadedMessageFiles } from '../next-message-item/uploaded-message-files';
|
||||
import { RAGFlowAvatar } from '../ragflow-avatar';
|
||||
import { useTheme } from '../theme-provider';
|
||||
import { AssistantGroupButton, UserGroupButton } from './group-button';
|
||||
|
|
@ -55,9 +52,10 @@ const MessageItem = ({
|
|||
const { theme } = useTheme();
|
||||
const isAssistant = item.role === MessageType.Assistant;
|
||||
const isUser = item.role === MessageType.User;
|
||||
const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
|
||||
const { data: documentThumbnails, setDocumentIds: setIds } =
|
||||
useFetchDocumentThumbnailsByIds();
|
||||
|
||||
const uploadedFiles = useMemo(() => {
|
||||
return item?.files ?? [];
|
||||
}, [item?.files]);
|
||||
|
||||
const referenceDocumentList = useMemo(() => {
|
||||
return reference?.doc_aggs ?? [];
|
||||
|
|
@ -67,17 +65,6 @@ const MessageItem = ({
|
|||
regenerateMessage?.(item);
|
||||
}, [regenerateMessage, item]);
|
||||
|
||||
useEffect(() => {
|
||||
const ids = item?.doc_ids ?? [];
|
||||
if (ids.length) {
|
||||
setDocumentIds(ids);
|
||||
const documentIds = ids.filter((x) => !(x in documentThumbnails));
|
||||
if (documentIds.length) {
|
||||
setIds(documentIds);
|
||||
}
|
||||
}
|
||||
}, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.messageItem, {
|
||||
|
|
@ -157,11 +144,13 @@ const MessageItem = ({
|
|||
list={referenceDocumentList}
|
||||
></ReferenceDocumentList>
|
||||
)}
|
||||
{isUser && documentList.length > 0 && (
|
||||
<InnerUploadedMessageFiles
|
||||
files={documentList}
|
||||
></InnerUploadedMessageFiles>
|
||||
)}
|
||||
{isUser &&
|
||||
Array.isArray(uploadedFiles) &&
|
||||
uploadedFiles.length > 0 && (
|
||||
<UploadedMessageFiles
|
||||
files={uploadedFiles as UploadResponseDataType[]}
|
||||
></UploadedMessageFiles>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { UploadResponseDataType } from '@/interfaces/database/chat';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { formatBytes } from '@/utils/file-util';
|
||||
import { memo } from 'react';
|
||||
import FileIcon from '../file-icon';
|
||||
import NewDocumentLink from '../new-document-link';
|
||||
import SvgIcon from '../svg-icon';
|
||||
|
||||
interface IProps {
|
||||
files?: File[] | IDocumentInfo[];
|
||||
files?: File[] | IDocumentInfo[] | UploadResponseDataType[];
|
||||
}
|
||||
|
||||
type NameWidgetType = {
|
||||
|
|
@ -15,16 +15,16 @@ type NameWidgetType = {
|
|||
size: number;
|
||||
id?: string;
|
||||
};
|
||||
function NameWidget({ name, size, id }: NameWidgetType) {
|
||||
function NameWidget({ name, size }: NameWidgetType) {
|
||||
return (
|
||||
<div className="text-xs max-w-20">
|
||||
{id ? (
|
||||
{/* {id ? (
|
||||
<NewDocumentLink documentId={id} documentName={name} prefix="document">
|
||||
{name}
|
||||
</NewDocumentLink>
|
||||
) : (
|
||||
<div className="truncate">{name}</div>
|
||||
)}
|
||||
)} */}
|
||||
<div className="truncate">{name}</div>
|
||||
<p className="text-text-secondary pt-1">{formatBytes(size)}</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
IDocumentMetaRequestBody,
|
||||
} from '@/interfaces/request/document';
|
||||
import i18n from '@/locales/config';
|
||||
import chatService from '@/services/chat-service';
|
||||
import kbService, { listDocument } from '@/services/knowledge-service';
|
||||
import api, { api_host } from '@/utils/api';
|
||||
import { buildChunkHighlights } from '@/utils/document-util';
|
||||
|
|
@ -428,39 +427,6 @@ export const useDeleteDocument = () => {
|
|||
return { data, loading, deleteDocument: mutateAsync };
|
||||
};
|
||||
|
||||
export const useUploadAndParseDocument = (uploadMethod: string) => {
|
||||
const {
|
||||
data,
|
||||
isPending: loading,
|
||||
mutateAsync,
|
||||
} = useMutation({
|
||||
mutationKey: ['uploadAndParseDocument'],
|
||||
mutationFn: async ({
|
||||
conversationId,
|
||||
fileList,
|
||||
}: {
|
||||
conversationId: string;
|
||||
fileList: UploadFile[];
|
||||
}) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('conversation_id', conversationId);
|
||||
fileList.forEach((file: UploadFile) => {
|
||||
formData.append('file', file as any);
|
||||
});
|
||||
if (uploadMethod === 'upload_and_parse') {
|
||||
const data = await kbService.upload_and_parse(formData);
|
||||
return data?.data;
|
||||
}
|
||||
const data = await chatService.uploadAndParseExternal(formData);
|
||||
return data?.data;
|
||||
} catch (error) {}
|
||||
},
|
||||
});
|
||||
|
||||
return { data, loading, uploadAndParseDocument: mutateAsync };
|
||||
};
|
||||
|
||||
export const useParseDocument = () => {
|
||||
const {
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { IAskRequestBody } from '@/interfaces/request/chat';
|
|||
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
|
||||
import { isConversationIdExist } from '@/pages/next-chats/utils';
|
||||
import chatService from '@/services/next-chat-service';
|
||||
import api from '@/utils/api';
|
||||
import { buildMessageListWithUuid, getConversationId } from '@/utils/chat';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useDebounce } from 'ahooks';
|
||||
|
|
@ -427,6 +428,7 @@ export function useUploadAndParseFile() {
|
|||
|
||||
const { data } = await chatService.uploadAndParse(
|
||||
{
|
||||
url: api.upload_and_parse(conversationId || id),
|
||||
signal: controller.current.signal,
|
||||
data: formData,
|
||||
onUploadProgress: ({ progress }) => {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export interface Message {
|
|||
id?: string;
|
||||
audio_binary?: string;
|
||||
data?: any;
|
||||
files?: File[];
|
||||
files?: (File | UploadResponseDataType)[];
|
||||
chatBoxId?: string;
|
||||
attachment?: IAttachment;
|
||||
}
|
||||
|
|
@ -192,3 +192,14 @@ export interface IMessage extends Message {
|
|||
export interface IClientConversation extends IConversation {
|
||||
message: IMessage[];
|
||||
}
|
||||
|
||||
export interface UploadResponseDataType {
|
||||
created_at: number;
|
||||
created_by: string;
|
||||
extension: string;
|
||||
id: string;
|
||||
mime_type: string;
|
||||
name: string;
|
||||
preview_url: null;
|
||||
size: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export const useSendMessage = (controller: AbortController) => {
|
|||
const { conversationId, isNew } = useGetChatSearchParams();
|
||||
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
||||
|
||||
const { handleUploadFile, fileIds, clearFileIds, isUploading, removeFile } =
|
||||
const { handleUploadFile, isUploading, removeFile, files, clearFiles } =
|
||||
useUploadFile();
|
||||
|
||||
const { send, answer, done } = useSendMessageWithSse(
|
||||
|
|
@ -208,7 +208,7 @@ export const useSendMessage = (controller: AbortController) => {
|
|||
|
||||
addNewestQuestion({
|
||||
content: value,
|
||||
doc_ids: fileIds,
|
||||
files: files,
|
||||
id,
|
||||
role: MessageType.User,
|
||||
});
|
||||
|
|
@ -218,16 +218,16 @@ export const useSendMessage = (controller: AbortController) => {
|
|||
id,
|
||||
content: value.trim(),
|
||||
role: MessageType.User,
|
||||
doc_ids: fileIds,
|
||||
files: files,
|
||||
});
|
||||
}
|
||||
clearFileIds();
|
||||
clearFiles();
|
||||
}, [
|
||||
value,
|
||||
addNewestQuestion,
|
||||
fileIds,
|
||||
files,
|
||||
done,
|
||||
clearFileIds,
|
||||
clearFiles,
|
||||
setValue,
|
||||
handleSendMessage,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function useSendMultipleChatMessage(
|
|||
api.completeConversation,
|
||||
);
|
||||
|
||||
const { handleUploadFile, fileIds, clearFileIds } = useUploadFile();
|
||||
const { handleUploadFile, files, clearFiles } = useUploadFile();
|
||||
|
||||
const { setFormRef, getLLMConfigById, isLLMConfigEmpty } =
|
||||
useBuildFormRefs(chatBoxIds);
|
||||
|
|
@ -181,7 +181,7 @@ export function useSendMultipleChatMessage(
|
|||
id,
|
||||
role: MessageType.User,
|
||||
chatBoxId,
|
||||
doc_ids: fileIds,
|
||||
files,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -195,22 +195,22 @@ export function useSendMultipleChatMessage(
|
|||
id,
|
||||
content: value.trim(),
|
||||
role: MessageType.User,
|
||||
doc_ids: fileIds,
|
||||
files,
|
||||
},
|
||||
chatBoxId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
clearFileIds();
|
||||
clearFiles();
|
||||
}, [
|
||||
value,
|
||||
chatBoxIds,
|
||||
allDone,
|
||||
clearFileIds,
|
||||
clearFiles,
|
||||
isLLMConfigEmpty,
|
||||
addNewestQuestion,
|
||||
fileIds,
|
||||
files,
|
||||
setValue,
|
||||
sendMessage,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import { useCallback, useState } from 'react';
|
|||
|
||||
export function useUploadFile() {
|
||||
const { uploadAndParseFile, loading, cancel } = useUploadAndParseFile();
|
||||
const [fileIds, setFileIds] = useState<string[]>([]);
|
||||
const [fileMap, setFileMap] = useState<Map<File, string>>(new Map());
|
||||
const [currentFiles, setCurrentFiles] = useState<Record<string, any>[]>([]);
|
||||
const [fileMap, setFileMap] = useState<Map<File, Record<string, any>>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
type FileUploadParameters = Parameters<
|
||||
NonNullable<FileUploadProps['onUpload']>
|
||||
|
|
@ -20,10 +22,11 @@ export function useUploadFile() {
|
|||
if (Array.isArray(files) && files.length) {
|
||||
const file = files[0];
|
||||
const ret = await uploadAndParseFile({ file, options, conversationId });
|
||||
if (ret?.code === 0 && Array.isArray(ret?.data)) {
|
||||
setFileIds((list) => [...list, ...ret.data]);
|
||||
if (ret?.code === 0) {
|
||||
const data = ret.data;
|
||||
setCurrentFiles((list) => [...list, data]);
|
||||
setFileMap((map) => {
|
||||
map.set(files[0], ret.data[0]);
|
||||
map.set(files[0], data);
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
|
@ -32,8 +35,8 @@ export function useUploadFile() {
|
|||
[uploadAndParseFile],
|
||||
);
|
||||
|
||||
const clearFileIds = useCallback(() => {
|
||||
setFileIds([]);
|
||||
const clearFiles = useCallback(() => {
|
||||
setCurrentFiles([]);
|
||||
setFileMap(new Map());
|
||||
}, []);
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ export function useUploadFile() {
|
|||
}
|
||||
const id = fileMap.get(file);
|
||||
if (id) {
|
||||
setFileIds((list) => list.filter((item) => item !== id));
|
||||
setCurrentFiles((list) => list.filter((item) => item !== id));
|
||||
}
|
||||
},
|
||||
[cancel, fileMap, loading],
|
||||
|
|
@ -53,9 +56,9 @@ export function useUploadFile() {
|
|||
|
||||
return {
|
||||
handleUploadFile,
|
||||
clearFileIds,
|
||||
fileIds,
|
||||
files: currentFiles,
|
||||
isUploading: loading,
|
||||
removeFile,
|
||||
clearFiles: clearFiles,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ const {
|
|||
web_crawl,
|
||||
knowledge_graph,
|
||||
document_infos,
|
||||
upload_and_parse,
|
||||
listTagByKnowledgeIds,
|
||||
setMeta,
|
||||
getMeta,
|
||||
|
|
@ -158,10 +157,6 @@ const methods = {
|
|||
url: document_delete,
|
||||
method: 'delete',
|
||||
},
|
||||
upload_and_parse: {
|
||||
url: upload_and_parse,
|
||||
method: 'post',
|
||||
},
|
||||
listTagByKnowledgeIds: {
|
||||
url: listTagByKnowledgeIds,
|
||||
method: 'get',
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export default {
|
|||
document_upload: `${api_host}/document/upload`,
|
||||
web_crawl: `${api_host}/document/web_crawl`,
|
||||
document_infos: `${api_host}/document/infos`,
|
||||
upload_and_parse: `${api_host}/document/upload_and_parse`,
|
||||
upload_and_parse: (id: string) => `${api_host}/document/upload_info`,
|
||||
parse: `${api_host}/document/parse`,
|
||||
setMeta: `${api_host}/document/set_meta`,
|
||||
get_dataset_filter: `${api_host}/document/filter`,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue