mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-07 03:48:44 +08:00
Refa: align chat and search restful APIs (#14229)
### What problem does this PR solve? Refactor /api/v1/chats to be more RESTful. ### Type of change - [x] Refactoring --------- Co-authored-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import os
|
||||
import re
|
||||
import tempfile
|
||||
from copy import deepcopy
|
||||
from types import SimpleNamespace
|
||||
|
||||
from quart import Response, request
|
||||
|
||||
@@ -30,7 +31,7 @@ from api.db.joint_services.tenant_model_service import (
|
||||
)
|
||||
from api.db.services.chunk_feedback_service import ChunkFeedbackService
|
||||
from api.db.services.conversation_service import ConversationService, structure_answer
|
||||
from api.db.services.dialog_service import DialogService, async_ask, async_chat, gen_mindmap
|
||||
from api.db.services.dialog_service import DialogService, async_chat, gen_mindmap
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from api.db.services.search_service import SearchService
|
||||
@@ -67,6 +68,15 @@ _DEFAULT_PROMPT_CONFIG = {
|
||||
"tts": False,
|
||||
"refine_multiturn": True,
|
||||
}
|
||||
_DEFAULT_DIRECT_CHAT_PROMPT_CONFIG = {
|
||||
"system": "",
|
||||
"prologue": "",
|
||||
"parameters": [],
|
||||
"empty_response": "",
|
||||
"quote": False,
|
||||
"tts": False,
|
||||
"refine_multiturn": True,
|
||||
}
|
||||
_DEFAULT_RERANK_MODELS = {"BAAI/bge-reranker-v2-m3", "maidalun1020/bce-reranker-base_v1"}
|
||||
_READONLY_FIELDS = {"id", "tenant_id", "created_by", "create_time", "create_date", "update_time", "update_date"}
|
||||
_PERSISTED_FIELDS = set(DialogService.model._meta.fields)
|
||||
@@ -124,6 +134,39 @@ def _ensure_owned_chat(chat_id):
|
||||
)
|
||||
|
||||
|
||||
def _build_default_completion_dialog():
|
||||
return SimpleNamespace(
|
||||
tenant_id=current_user.id,
|
||||
llm_id="",
|
||||
tenant_llm_id=None,
|
||||
llm_setting={},
|
||||
prompt_config=deepcopy(_DEFAULT_DIRECT_CHAT_PROMPT_CONFIG),
|
||||
kb_ids=[],
|
||||
top_n=6,
|
||||
top_k=1024,
|
||||
rerank_id="",
|
||||
similarity_threshold=0.1,
|
||||
vector_similarity_weight=0.3,
|
||||
meta_data_filter=None,
|
||||
)
|
||||
|
||||
|
||||
def _create_session_for_completion(chat_id, dialog, user_id):
|
||||
conv = {
|
||||
"id": get_uuid(),
|
||||
"dialog_id": chat_id,
|
||||
"name": "New session",
|
||||
"message": [{"role": "assistant", "content": dialog.prompt_config.get("prologue", "")}],
|
||||
"user_id": user_id,
|
||||
"reference": [],
|
||||
}
|
||||
ConversationService.save(**conv)
|
||||
ok, conv_obj = ConversationService.get_by_id(conv["id"])
|
||||
if not ok:
|
||||
raise LookupError("Fail to create a session!")
|
||||
return conv_obj
|
||||
|
||||
|
||||
def _validate_llm_id(llm_id, tenant_id, llm_setting=None):
|
||||
if not llm_id:
|
||||
return None
|
||||
@@ -671,7 +714,7 @@ async def get_session(chat_id, session_id):
|
||||
return server_error_response(ex)
|
||||
|
||||
|
||||
@manager.route("/chats/<chat_id>/sessions/<session_id>", methods=["PUT"]) # noqa: F821
|
||||
@manager.route("/chats/<chat_id>/sessions/<session_id>", methods=["PATCH"]) # noqa: F821
|
||||
@login_required
|
||||
async def update_session(chat_id, session_id):
|
||||
if not _ensure_owned_chat(chat_id):
|
||||
@@ -829,7 +872,7 @@ async def update_message_feedback(chat_id, session_id, msg_id):
|
||||
return server_error_response(ex)
|
||||
|
||||
|
||||
@manager.route("/chats/tts", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/chat/audio/speech", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def tts():
|
||||
req = await get_request_json()
|
||||
@@ -857,9 +900,9 @@ async def tts():
|
||||
return resp
|
||||
|
||||
|
||||
@manager.route("/chats/transcriptions", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/chat/audio/transcription", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def transcriptions():
|
||||
async def transcription():
|
||||
req = await request.form
|
||||
stream_mode = req.get("stream", "false").lower() == "true"
|
||||
files = await request.files
|
||||
@@ -915,7 +958,7 @@ async def transcriptions():
|
||||
return Response(event_stream(), content_type="text/event-stream")
|
||||
|
||||
|
||||
@manager.route("/chats/mindmap", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/chat/mindmap", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("question", "kb_ids")
|
||||
async def mindmap():
|
||||
@@ -933,10 +976,10 @@ async def mindmap():
|
||||
return get_json_result(data=mind_map)
|
||||
|
||||
|
||||
@manager.route("/chats/related_questions", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/chat/recommendation", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("question")
|
||||
async def related_questions():
|
||||
async def recommendation():
|
||||
req = await get_request_json()
|
||||
|
||||
search_id = req.get("search_id", "")
|
||||
@@ -971,10 +1014,10 @@ async def related_questions():
|
||||
return get_json_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])
|
||||
|
||||
|
||||
@manager.route("/chats/<chat_id>/sessions/<session_id>/completions", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/chat/completions", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("messages")
|
||||
async def session_completion(chat_id, session_id):
|
||||
async def session_completion():
|
||||
req = await get_request_json()
|
||||
msg = []
|
||||
for m in req["messages"]:
|
||||
@@ -984,6 +1027,8 @@ async def session_completion(chat_id, session_id):
|
||||
continue
|
||||
msg.append(m)
|
||||
message_id = msg[-1].get("id") if msg else None
|
||||
chat_id = req.pop("chat_id", "") or ""
|
||||
session_id = req.pop("session_id", "") or ""
|
||||
chat_model_id = req.pop("llm_id", "")
|
||||
|
||||
chat_model_config = {}
|
||||
@@ -993,21 +1038,41 @@ async def session_completion(chat_id, session_id):
|
||||
chat_model_config[model_config] = config
|
||||
|
||||
try:
|
||||
e, conv = ConversationService.get_by_id(session_id)
|
||||
if not e:
|
||||
return get_data_error_result(message="Session not found!")
|
||||
if conv.dialog_id != chat_id:
|
||||
return get_data_error_result(message="Session does not belong to this chat!")
|
||||
conv.message = deepcopy(req["messages"])
|
||||
e, dia = DialogService.get_by_id(chat_id)
|
||||
if not e:
|
||||
return get_data_error_result(message="Chat not found!")
|
||||
conv = None
|
||||
if session_id and not chat_id:
|
||||
return get_data_error_result(message="`chat_id` is required when `session_id` is provided.")
|
||||
|
||||
if chat_id:
|
||||
if not _ensure_owned_chat(chat_id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="No authorization.",
|
||||
code=RetCode.AUTHENTICATION_ERROR,
|
||||
)
|
||||
e, dia = DialogService.get_by_id(chat_id)
|
||||
if not e:
|
||||
return get_data_error_result(message="Chat not found!")
|
||||
if session_id:
|
||||
e, conv = ConversationService.get_by_id(session_id)
|
||||
if not e:
|
||||
return get_data_error_result(message="Session not found!")
|
||||
if conv.dialog_id != chat_id:
|
||||
return get_data_error_result(message="Session does not belong to this chat!")
|
||||
else:
|
||||
conv = _create_session_for_completion(chat_id, dia, req.get("user_id", current_user.id))
|
||||
session_id = conv.id
|
||||
conv.message = deepcopy(req["messages"])
|
||||
else:
|
||||
dia = _build_default_completion_dialog()
|
||||
dia.llm_setting = chat_model_config
|
||||
|
||||
del req["messages"]
|
||||
|
||||
if not conv.reference:
|
||||
conv.reference = []
|
||||
conv.reference = [r for r in conv.reference if r]
|
||||
conv.reference.append({"chunks": [], "doc_aggs": []})
|
||||
if conv is not None:
|
||||
if not conv.reference:
|
||||
conv.reference = []
|
||||
conv.reference = [r for r in conv.reference if r]
|
||||
conv.reference.append({"chunks": [], "doc_aggs": []})
|
||||
|
||||
if chat_model_id:
|
||||
if not TenantLLMService.get_api_key(tenant_id=dia.tenant_id, model_name=chat_model_id):
|
||||
@@ -1015,16 +1080,21 @@ async def session_completion(chat_id, session_id):
|
||||
dia.llm_id = chat_model_id
|
||||
dia.llm_setting = chat_model_config
|
||||
|
||||
is_embedded = bool(chat_model_id)
|
||||
stream_mode = req.pop("stream", True)
|
||||
|
||||
def _format_answer(ans):
|
||||
formatted = structure_answer(conv, ans, message_id, session_id)
|
||||
if chat_id:
|
||||
formatted["chat_id"] = chat_id
|
||||
return formatted
|
||||
|
||||
async def stream():
|
||||
nonlocal dia, msg, req, conv
|
||||
try:
|
||||
async for ans in async_chat(dia, msg, True, **req):
|
||||
ans = structure_answer(conv, ans, message_id, conv.id)
|
||||
ans = _format_answer(ans)
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
if not is_embedded:
|
||||
if conv is not None:
|
||||
ConversationService.update_by_id(conv.id, conv.to_dict())
|
||||
except Exception as ex:
|
||||
logging.exception(ex)
|
||||
@@ -1041,40 +1111,10 @@ async def session_completion(chat_id, session_id):
|
||||
|
||||
answer = None
|
||||
async for ans in async_chat(dia, msg, **req):
|
||||
answer = structure_answer(conv, ans, message_id, conv.id)
|
||||
if not is_embedded:
|
||||
answer = _format_answer(ans)
|
||||
if conv is not None:
|
||||
ConversationService.update_by_id(conv.id, conv.to_dict())
|
||||
break
|
||||
return get_json_result(data=answer)
|
||||
except Exception as ex:
|
||||
return server_error_response(ex)
|
||||
|
||||
|
||||
@manager.route("/chats/ask", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("question", "kb_ids")
|
||||
async def ask():
|
||||
req = await get_request_json()
|
||||
uid = current_user.id
|
||||
|
||||
search_id = req.get("search_id", "")
|
||||
search_config = {}
|
||||
if search_id:
|
||||
if search_app := SearchService.get_detail(search_id):
|
||||
search_config = search_app.get("search_config", {})
|
||||
|
||||
async def stream():
|
||||
nonlocal req, uid
|
||||
try:
|
||||
async for ans in async_ask(req["question"], req["kb_ids"], uid, search_config=search_config):
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
except Exception as ex:
|
||||
yield "data:" + json.dumps({"code": 500, "message": str(ex), "data": {"answer": "**ERROR**: " + str(ex), "reference": []}}, ensure_ascii=False) + "\n\n"
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
|
||||
|
||||
resp = Response(stream(), mimetype="text/event-stream")
|
||||
resp.headers.add_header("Cache-control", "no-cache")
|
||||
resp.headers.add_header("Connection", "keep-alive")
|
||||
resp.headers.add_header("X-Accel-Buffering", "no")
|
||||
resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
return resp
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from quart import request
|
||||
import json
|
||||
|
||||
from quart import Response, request
|
||||
from api.db.services.dialog_service import async_ask
|
||||
from api.apps import current_user, login_required
|
||||
|
||||
from api.constants import DATASET_NAME_LIMIT
|
||||
@@ -168,3 +171,45 @@ def delete_search(search_id):
|
||||
return get_json_result(data=True)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/searches/<search_id>/completion", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("question")
|
||||
async def completion(search_id):
|
||||
if not SearchService.accessible4deletion(search_id, current_user.id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message="No authorization.",
|
||||
code=RetCode.AUTHENTICATION_ERROR,
|
||||
)
|
||||
|
||||
req = await get_request_json()
|
||||
uid = current_user.id
|
||||
search_app = SearchService.get_detail(search_id)
|
||||
if not search_app:
|
||||
return get_data_error_result(message=f"Cannot find search {search_id}")
|
||||
|
||||
search_config = search_app.get("search_config", {})
|
||||
kb_ids = search_config.get("kb_ids") or req.get("kb_ids") or []
|
||||
if not kb_ids:
|
||||
return get_data_error_result(message="`kb_ids` is required.")
|
||||
|
||||
async def stream():
|
||||
nonlocal req, uid, kb_ids, search_config
|
||||
try:
|
||||
async for ans in async_ask(req["question"], kb_ids, uid, search_config=search_config):
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
except Exception as ex:
|
||||
yield "data:" + json.dumps(
|
||||
{"code": 500, "message": str(ex), "data": {"answer": "**ERROR**: " + str(ex), "reference": []}},
|
||||
ensure_ascii=False,
|
||||
) + "\n\n"
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
|
||||
|
||||
resp = Response(stream(), mimetype="text/event-stream")
|
||||
resp.headers.add_header("Cache-control", "no-cache")
|
||||
resp.headers.add_header("Connection", "keep-alive")
|
||||
resp.headers.add_header("X-Accel-Buffering", "no")
|
||||
resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
return resp
|
||||
|
||||
@@ -72,13 +72,19 @@ See [Converse with chat assistant](../../references/http_api_reference.md#conver
|
||||
|
||||
```json {9}
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/{chat_id}/completions \
|
||||
--url http://{address}/api/v1/chat/completions \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||
--data-binary '
|
||||
{
|
||||
"question": "xxxxxxxxx",
|
||||
"chat_id": "{chat_id}",
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "xxxxxxxxx"
|
||||
}
|
||||
],
|
||||
"style":"hilarious"
|
||||
}'
|
||||
```
|
||||
@@ -109,4 +115,3 @@ while True:
|
||||
print(ans.content[len(cont):], end='', flush=True)
|
||||
cont = ans.content
|
||||
```
|
||||
|
||||
|
||||
@@ -3470,13 +3470,13 @@ Failure:
|
||||
|
||||
### Update chat assistant's session
|
||||
|
||||
**PUT** `/api/v1/chats/{chat_id}/sessions/{session_id}`
|
||||
**PATCH** `/api/v1/chats/{chat_id}/sessions/{session_id}`
|
||||
|
||||
Updates a session of a specified chat assistant.
|
||||
|
||||
#### Request
|
||||
|
||||
- Method: PUT
|
||||
- Method: PATCH
|
||||
- URL: `/api/v1/chats/{chat_id}/sessions/{session_id}`
|
||||
- Headers:
|
||||
- `'content-Type: application/json'`
|
||||
@@ -3487,7 +3487,7 @@ Updates a session of a specified chat assistant.
|
||||
##### Request example
|
||||
|
||||
```bash
|
||||
curl --request PUT \
|
||||
curl --request PATCH \
|
||||
--url http://{address}/api/v1/chats/{chat_id}/sessions/{session_id} \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||
@@ -3895,9 +3895,13 @@ Failure:
|
||||
|
||||
### Converse with chat assistant
|
||||
|
||||
**POST** `/api/v1/chats/{chat_id}/completions`
|
||||
**POST** `/api/v1/chat/completions`
|
||||
|
||||
Asks a specified chat assistant a question to start an AI-powered conversation.
|
||||
Starts a chat completion request. The same endpoint supports three modes:
|
||||
|
||||
- No `chat_id`: talk directly with the tenant's default chat model.
|
||||
- With `chat_id` but no `session_id`: use that chat's configuration and automatically create a new session.
|
||||
- With both `chat_id` and `session_id`: continue an existing chat session.
|
||||
|
||||
:::tip NOTE
|
||||
|
||||
@@ -3917,88 +3921,87 @@ Asks a specified chat assistant a question to start an AI-powered conversation.
|
||||
#### Request
|
||||
|
||||
- Method: POST
|
||||
- URL: `/api/v1/chats/{chat_id}/completions`
|
||||
- URL: `/api/v1/chat/completions`
|
||||
- Headers:
|
||||
- `'content-Type: application/json'`
|
||||
- `'Authorization: Bearer <YOUR_API_KEY>'`
|
||||
- Body:
|
||||
- `"question"`: `string`
|
||||
- `"messages"`: `list[object]`
|
||||
- `"stream"`: `boolean`
|
||||
- `"chat_id"`: `string` (optional)
|
||||
- `"session_id"`: `string` (optional)
|
||||
- `"user_id`: `string` (optional)
|
||||
- `"metadata_condition"`: `object` (optional)
|
||||
- `"llm_id"`: `string` (optional)
|
||||
|
||||
##### Request example
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/{chat_id}/completions \
|
||||
--url http://{address}/api/v1/chat/completions \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||
--data-binary '
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/{chat_id}/completions \
|
||||
--url http://{address}/api/v1/chat/completions \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||
--data-binary '
|
||||
{
|
||||
"question": "Who are you",
|
||||
"chat_id": "{chat_id}",
|
||||
"stream": true,
|
||||
"session_id":"9fa7691cb85c11ef9c5f0242ac120005",
|
||||
"metadata_condition": {
|
||||
"logic": "and",
|
||||
"conditions": [
|
||||
"messages": [
|
||||
{
|
||||
"name": "author",
|
||||
"comparison_operator": "is",
|
||||
"value": "bob"
|
||||
"role": "assistant",
|
||||
"content": "Hi! I'\''m your assistant. What can I do for you?"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
##### Request Parameters
|
||||
|
||||
- `chat_id`: (*Path parameter*)
|
||||
The ID of the associated chat assistant.
|
||||
- `"question"`: (*Body Parameter*), `string`, *Required*
|
||||
The question to start an AI-powered conversation.
|
||||
- `"messages"`: (*Body Parameter*), `list[object]`, *Required*
|
||||
The conversation messages sent to the model.
|
||||
- `"stream"`: (*Body Parameter*), `boolean`
|
||||
Indicates whether to output responses in a streaming way:
|
||||
- `true`: Enable streaming (default).
|
||||
- `false`: Disable streaming.
|
||||
- `"chat_id"`: (*Body Parameter*)
|
||||
Optional chat assistant ID. If omitted, the tenant's default chat model is used directly.
|
||||
- `"session_id"`: (*Body Parameter*)
|
||||
The ID of session. If it is not provided, a new session will be generated.
|
||||
- `"user_id"`: (*Body parameter*), `string`
|
||||
The optional user-defined ID. Valid *only* when no `session_id` is provided.
|
||||
- `"metadata_condition"`: (*Body parameter*), `object`
|
||||
Optional metadata filter conditions applied to retrieval results.
|
||||
- `logic`: `string`, one of `and` / `or`
|
||||
- `conditions`: `list[object]` where each condition contains:
|
||||
- `name`: `string` metadata key
|
||||
- `comparison_operator`: `string` (e.g. `is`, `not is`, `contains`, `not contains`, `start with`, `end with`, `empty`, `not empty`, `>`, `<`, `≥`, `≤`)
|
||||
- `value`: `string|number|boolean` (optional for `empty`/`not empty`)
|
||||
Optional session ID. If `chat_id` is provided but `session_id` is omitted, a new session will be generated automatically.
|
||||
- `"llm_id"`: (*Body Parameter*), `string`
|
||||
Optional model override when a specific chat model should be used for this request.
|
||||
|
||||
#### Response
|
||||
|
||||
Success without `session_id`:
|
||||
Success without `chat_id` or `session_id`:
|
||||
|
||||
```json
|
||||
data:{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"answer": "Hi! I'm your assistant. What can I do for you?",
|
||||
"answer": "I am an assistant powered by the tenant's default chat model.",
|
||||
"reference": {},
|
||||
"audio_binary": null,
|
||||
"id": null,
|
||||
"session_id": "b01eed84b85611efa0e90242ac120005"
|
||||
"id": "b01eed84b85611efa0e90242ac120005",
|
||||
"session_id": ""
|
||||
}
|
||||
}
|
||||
data:{
|
||||
@@ -4008,7 +4011,7 @@ data:{
|
||||
}
|
||||
```
|
||||
|
||||
Success with `session_id`:
|
||||
Success with `chat_id` and `session_id`:
|
||||
|
||||
```json
|
||||
data:{
|
||||
@@ -5276,14 +5279,14 @@ Failure:
|
||||
|
||||
### Text-to-speech
|
||||
|
||||
**POST** `/api/v1/chats/tts`
|
||||
**POST** `/api/v1/chat/audio/speech`
|
||||
|
||||
Converts text to speech audio using the tenant's default TTS model, returning a streaming audio response.
|
||||
|
||||
#### Request
|
||||
|
||||
- Method: POST
|
||||
- URL: `/api/v1/chats/tts`
|
||||
- URL: `/api/v1/chat/audio/speech`
|
||||
- Headers:
|
||||
- `'Content-Type: application/json'`
|
||||
- `'Authorization: Bearer <YOUR_LOGIN_TOKEN>'`
|
||||
@@ -5294,7 +5297,7 @@ Converts text to speech audio using the tenant's default TTS model, returning a
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/tts \
|
||||
--url http://{address}/api/v1/chat/audio/speech \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_LOGIN_TOKEN>' \
|
||||
--output audio.mp3 \
|
||||
@@ -5318,14 +5321,14 @@ Failure:
|
||||
|
||||
### Speech-to-text
|
||||
|
||||
**POST** `/api/v1/chats/transcriptions`
|
||||
**POST** `/api/v1/chat/audio/transcription`
|
||||
|
||||
Transcribes an audio file using the tenant's default ASR (automatic speech recognition) model.
|
||||
|
||||
#### Request
|
||||
|
||||
- Method: POST
|
||||
- URL: `/api/v1/chats/transcriptions`
|
||||
- URL: `/api/v1/chat/audio/transcription`
|
||||
- Headers:
|
||||
- `'Authorization: Bearer <YOUR_LOGIN_TOKEN>'`
|
||||
- Body (multipart/form-data):
|
||||
@@ -5336,7 +5339,7 @@ Transcribes an audio file using the tenant's default ASR (automatic speech recog
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/transcriptions \
|
||||
--url http://{address}/api/v1/chat/audio/transcription \
|
||||
--header 'Authorization: Bearer <YOUR_LOGIN_TOKEN>' \
|
||||
--form file=@recording.wav \
|
||||
--form stream=false
|
||||
@@ -5370,14 +5373,14 @@ Failure:
|
||||
|
||||
### Generate mind map
|
||||
|
||||
**POST** `/api/v1/chats/mindmap`
|
||||
**POST** `/api/v1/chat/mindmap`
|
||||
|
||||
Generates a mind map from a question and a set of knowledge base IDs.
|
||||
|
||||
#### Request
|
||||
|
||||
- Method: POST
|
||||
- URL: `/api/v1/chats/mindmap`
|
||||
- URL: `/api/v1/chat/mindmap`
|
||||
- Headers:
|
||||
- `'Content-Type: application/json'`
|
||||
- `'Authorization: Bearer <YOUR_LOGIN_TOKEN>'`
|
||||
@@ -5390,7 +5393,7 @@ Generates a mind map from a question and a set of knowledge base IDs.
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/mindmap \
|
||||
--url http://{address}/api/v1/chat/mindmap \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_LOGIN_TOKEN>' \
|
||||
--data '{
|
||||
@@ -5426,7 +5429,7 @@ Failure:
|
||||
|
||||
### Generate related questions
|
||||
|
||||
**POST** `/api/v1/chats/related_questions`
|
||||
**POST** `/api/v1/chat/recommandation`
|
||||
|
||||
Generates five to ten alternative question strings from the user's original query to retrieve more relevant search results.
|
||||
|
||||
@@ -5441,7 +5444,7 @@ The chat model autonomously determines the number of questions to generate based
|
||||
#### Request
|
||||
|
||||
- Method: POST
|
||||
- URL: `/api/v1/chats/related_questions`
|
||||
- URL: `/api/v1/chat/recommandation`
|
||||
- Headers:
|
||||
- `'content-Type: application/json'`
|
||||
- `'Authorization: Bearer <YOUR_LOGIN_TOKEN>'`
|
||||
@@ -5453,7 +5456,7 @@ The chat model autonomously determines the number of questions to generate based
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/chats/related_questions \
|
||||
--url http://{address}/api/v1/chat/recommandation \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_LOGIN_TOKEN>' \
|
||||
--data '{
|
||||
@@ -7947,3 +7950,62 @@ Failure:
|
||||
"message": "No authorization."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Search completion
|
||||
|
||||
**POST** `/api/v1/searches/{search_id}/completion`
|
||||
|
||||
Generates an answer using the saved search app configuration and returns the result as a Server-Sent Events stream.
|
||||
|
||||
#### Request
|
||||
|
||||
- Method: POST
|
||||
- URL: `/api/v1/searches/{search_id}/completion`
|
||||
- Headers:
|
||||
- `'Content-Type: application/json'`
|
||||
- `'Authorization: Bearer <YOUR_LOGIN_TOKEN>'`
|
||||
- Body:
|
||||
- `"question"`: `string` *(Required)* The user question.
|
||||
- `"kb_ids"`: `list[string]` *(Optional)* Fallback dataset IDs. Used only when the search app config does not already define `kb_ids`.
|
||||
|
||||
##### Request example
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url http://{address}/api/v1/searches/{search_id}/completion \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <YOUR_LOGIN_TOKEN>' \
|
||||
--data '{
|
||||
"question": "What is retrieval-augmented generation?"
|
||||
}'
|
||||
```
|
||||
|
||||
##### Request parameters
|
||||
|
||||
- `search_id`: (*Path parameter*), `string`, *Required*
|
||||
The ID of the search app.
|
||||
- `"question"`: (*Body parameter*), `string`, *Required*
|
||||
The user question.
|
||||
- `"kb_ids"`: (*Body parameter*), `list[string]`
|
||||
Optional fallback dataset IDs when the search app config does not define them.
|
||||
|
||||
#### Response
|
||||
|
||||
Success (streaming):
|
||||
|
||||
```text
|
||||
data: {"code": 0, "message": "", "data": {"answer": "...", "reference": {...}}}
|
||||
|
||||
data: {"code": 0, "message": "", "data": true}
|
||||
```
|
||||
|
||||
Failure:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 109,
|
||||
"message": "No authorization."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -115,8 +115,8 @@ class Session(Base):
|
||||
return res
|
||||
|
||||
def update(self, update_message):
|
||||
res = self.put(f"/chats/{self.chat_id}/sessions/{self.id}",
|
||||
update_message)
|
||||
res = self.patch(f"/chats/{self.chat_id}/sessions/{self.id}",
|
||||
update_message)
|
||||
res = res.json()
|
||||
if res.get("code") != 0:
|
||||
raise Exception(res.get("message"))
|
||||
|
||||
@@ -267,7 +267,7 @@ def list_session_with_chat_assistants(auth, chat_assistant_id, params=None):
|
||||
|
||||
def update_session_with_chat_assistant(auth, chat_assistant_id, session_id, payload=None):
|
||||
url = f"{HOST_ADDRESS}{SESSION_WITH_CHAT_ASSISTANT_API_URL}/{session_id}".format(chat_id=chat_assistant_id)
|
||||
res = requests.put(url=url, headers=HEADERS, auth=auth, json=payload)
|
||||
res = requests.patch(url=url, headers=HEADERS, auth=auth, json=payload)
|
||||
return res.json()
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ def agent_completions(auth, agent_id, payload=None):
|
||||
return res.json()
|
||||
|
||||
|
||||
def chat_completions(auth, chat_id, payload=None):
|
||||
def chat_completions(auth, chat_id=None, payload=None):
|
||||
"""
|
||||
Send a question/message to a chat assistant and get completion.
|
||||
|
||||
@@ -403,14 +403,19 @@ def chat_completions(auth, chat_id, payload=None):
|
||||
auth: Authentication object
|
||||
chat_id: Chat assistant ID
|
||||
payload: Dictionary containing:
|
||||
- question: str (required) - The question to ask
|
||||
- messages: list (required) - Conversation messages
|
||||
- stream: bool (optional) - Whether to stream responses, default False
|
||||
- session_id: str (optional) - Session ID for conversation context
|
||||
|
||||
Returns:
|
||||
Response JSON with answer data
|
||||
"""
|
||||
url = f"{HOST_ADDRESS}/api/{VERSION}/chats/{chat_id}/completions"
|
||||
url = f"{HOST_ADDRESS}/api/{VERSION}/chat/completions"
|
||||
payload = dict(payload or {})
|
||||
if chat_id:
|
||||
payload.setdefault("chat_id", chat_id)
|
||||
if "question" in payload and "messages" not in payload:
|
||||
payload["messages"] = [{"role": "user", "content": payload.pop("question")}]
|
||||
res = requests.post(url=url, headers=HEADERS, auth=auth, json=payload)
|
||||
return res.json()
|
||||
|
||||
|
||||
@@ -62,7 +62,11 @@ class TestChatCompletions:
|
||||
res = chat_completions(
|
||||
HttpApiAuth,
|
||||
chat_id,
|
||||
{"question": "hello", "stream": False, "session_id": session_id},
|
||||
{
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": False,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
assert res["code"] == 0, res
|
||||
assert isinstance(res["data"], dict), res
|
||||
@@ -75,10 +79,14 @@ class TestChatCompletions:
|
||||
res = chat_completions(
|
||||
HttpApiAuth,
|
||||
"invalid_chat_id",
|
||||
{"question": "hello", "stream": False, "session_id": "invalid_session"},
|
||||
{
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": False,
|
||||
"session_id": "invalid_session",
|
||||
},
|
||||
)
|
||||
assert res["code"] == 102, res
|
||||
assert "You don't own the chat" in res.get("message", ""), res
|
||||
assert res["code"] == 109, res
|
||||
assert "No authorization." in res.get("message", ""), res
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_completion_invalid_session(self, HttpApiAuth, request):
|
||||
@@ -91,32 +99,44 @@ class TestChatCompletions:
|
||||
res = chat_completions(
|
||||
HttpApiAuth,
|
||||
chat_id,
|
||||
{"question": "hello", "stream": False, "session_id": "invalid_session"},
|
||||
{
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": False,
|
||||
"session_id": "invalid_session",
|
||||
},
|
||||
)
|
||||
assert res["code"] == 102, res
|
||||
assert "You don't own the session" in res.get("message", ""), res
|
||||
assert "Session not found!" in res.get("message", ""), res
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_completion_invalid_metadata_condition(self, HttpApiAuth, request):
|
||||
def test_chat_completion_stream_false_with_chat_without_session(self, HttpApiAuth, request):
|
||||
res = create_chat_assistant(HttpApiAuth, {"name": "chat_completion_invalid_meta", "dataset_ids": []})
|
||||
assert res["code"] == 0, res
|
||||
chat_id = res["data"]["id"]
|
||||
request.addfinalizer(lambda: delete_all_chat_assistants(HttpApiAuth))
|
||||
request.addfinalizer(lambda: delete_all_sessions_with_chat_assistant(HttpApiAuth, chat_id))
|
||||
|
||||
res = create_session_with_chat_assistant(HttpApiAuth, chat_id, {"name": "session_for_meta"})
|
||||
assert res["code"] == 0, res
|
||||
session_id = res["data"]["id"]
|
||||
|
||||
res = chat_completions(
|
||||
HttpApiAuth,
|
||||
chat_id,
|
||||
{
|
||||
"question": "hello",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": False,
|
||||
"session_id": session_id,
|
||||
"metadata_condition": "invalid",
|
||||
},
|
||||
)
|
||||
assert res["code"] == 102, res
|
||||
assert "metadata_condition" in res.get("message", ""), res
|
||||
assert res["code"] == 0, res
|
||||
assert res["data"]["session_id"], res
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_chat_completion_stream_false_without_chat(self, HttpApiAuth):
|
||||
res = chat_completions(
|
||||
HttpApiAuth,
|
||||
None,
|
||||
{
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
assert res["code"] == 0, res
|
||||
assert isinstance(res["data"], dict), res
|
||||
assert "answer" in res["data"], res
|
||||
|
||||
@@ -40,6 +40,13 @@ class _DummyAtomic:
|
||||
return False
|
||||
|
||||
|
||||
class _StubResponse:
|
||||
def __init__(self, data=None, mimetype=None):
|
||||
self.data = data
|
||||
self.mimetype = mimetype
|
||||
self.headers = {}
|
||||
|
||||
|
||||
class _Args(dict):
|
||||
def get(self, key, default=None):
|
||||
return super().get(key, default)
|
||||
@@ -111,6 +118,7 @@ def _load_search_api(monkeypatch):
|
||||
|
||||
quart_mod = ModuleType("quart")
|
||||
quart_mod.request = SimpleNamespace(args=_Args())
|
||||
quart_mod.Response = _StubResponse
|
||||
monkeypatch.setitem(sys.modules, "quart", quart_mod)
|
||||
|
||||
common_pkg = ModuleType("common")
|
||||
@@ -201,6 +209,15 @@ def _load_search_api(monkeypatch):
|
||||
search_service_mod.SearchService = _SearchService
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.search_service", search_service_mod)
|
||||
|
||||
dialog_service_mod = ModuleType("api.db.services.dialog_service")
|
||||
|
||||
async def _async_ask(*_args, **_kwargs):
|
||||
if False:
|
||||
yield None
|
||||
|
||||
dialog_service_mod.async_ask = _async_ask
|
||||
monkeypatch.setitem(sys.modules, "api.db.services.dialog_service", dialog_service_mod)
|
||||
|
||||
user_service_mod = ModuleType("api.db.services.user_service")
|
||||
|
||||
class _TenantService:
|
||||
|
||||
@@ -295,18 +295,17 @@ export const useSendMessageWithSse = () => {
|
||||
return {
|
||||
...d,
|
||||
answer: newAnswer,
|
||||
conversationId: body?.conversation_id,
|
||||
conversationId: body?.session_id ?? body?.conversation_id,
|
||||
chatBoxId: body.chatBoxId,
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Swallow parse errors silently
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === 'AbortError') {
|
||||
console.log('Request was aborted by user or logic.');
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -314,7 +313,7 @@ export const useSendMessageWithSse = () => {
|
||||
setDoneValue(body, true);
|
||||
resetAnswer();
|
||||
return { data: await res, response };
|
||||
} catch (e) {
|
||||
} catch {
|
||||
setDoneValue(body, true);
|
||||
|
||||
resetAnswer();
|
||||
@@ -357,7 +356,7 @@ export const useSpeechWithSse = (url: string = api.chatsTts) => {
|
||||
if (res?.code !== 0) {
|
||||
message.error(res?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Swallow errors silently
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -98,8 +98,10 @@ export const useSendMessage = (controller: AbortController) => {
|
||||
} & NextMessageInputOnPressEnterParameter) => {
|
||||
const sessionId = currentConversationId ?? conversationId;
|
||||
const res = await send(
|
||||
api.completionUrl(chatId!, sessionId),
|
||||
api.completionUrl,
|
||||
{
|
||||
chat_id: chatId,
|
||||
session_id: sessionId,
|
||||
messages: [
|
||||
...(Array.isArray(messages) && messages?.length > 0
|
||||
? messages
|
||||
|
||||
@@ -67,8 +67,10 @@ export function useSendSingleMessage({
|
||||
} & NextMessageInputOnPressEnterParameter) => {
|
||||
const sessionId = currentConversationId ?? conversationId;
|
||||
const res = await send(
|
||||
api.completionUrl(chatId!, sessionId),
|
||||
api.completionUrl,
|
||||
{
|
||||
chat_id: chatId,
|
||||
session_id: sessionId,
|
||||
messages: [
|
||||
...(Array.isArray(messages) && messages?.length > 0
|
||||
? messages
|
||||
@@ -92,6 +94,7 @@ export function useSendSingleMessage({
|
||||
[
|
||||
derivedMessages,
|
||||
conversationId,
|
||||
chatId,
|
||||
removeLatestMessage,
|
||||
setValue,
|
||||
send,
|
||||
|
||||
@@ -308,7 +308,11 @@ export const useSendQuestion = (
|
||||
related_search: boolean = false,
|
||||
) => {
|
||||
const { sharedId } = useGetSharedSearchParams();
|
||||
const askUrl = sharedId ? api.askShare : api.ask;
|
||||
const askUrl = sharedId
|
||||
? api.askShare
|
||||
: searchId
|
||||
? api.searchCompletion(searchId)
|
||||
: '';
|
||||
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse();
|
||||
|
||||
const { testChunk, loading } = useTestChunkRetrieval(tenantId);
|
||||
@@ -331,12 +335,15 @@ export const useSendQuestion = (
|
||||
setIsFirstRender(false);
|
||||
setCurrentAnswer({} as IAnswer);
|
||||
if (enableAI) {
|
||||
if (!sharedId && !searchId) {
|
||||
message.error('Search ID is required.');
|
||||
return;
|
||||
}
|
||||
setSendingLoading(true);
|
||||
send(askUrl, {
|
||||
kb_ids: kbIds,
|
||||
question: q,
|
||||
tenantId,
|
||||
search_id: searchId,
|
||||
});
|
||||
}
|
||||
testChunk({
|
||||
@@ -355,12 +362,14 @@ export const useSendQuestion = (
|
||||
[
|
||||
send,
|
||||
testChunk,
|
||||
askUrl,
|
||||
kbIds,
|
||||
fetchRelatedQuestions,
|
||||
setPagination,
|
||||
pagination.pageSize,
|
||||
tenantId,
|
||||
searchId,
|
||||
sharedId,
|
||||
related_search,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ const {
|
||||
deleteMessage,
|
||||
thumbup,
|
||||
chatsTts,
|
||||
ask,
|
||||
chatsMindmap,
|
||||
chatsRelatedQuestions,
|
||||
uploadAndParse,
|
||||
@@ -67,7 +66,7 @@ const methods = {
|
||||
},
|
||||
updateSession: {
|
||||
url: updateSession,
|
||||
method: 'put',
|
||||
method: 'patch',
|
||||
},
|
||||
removeSessions: {
|
||||
url: removeSessions,
|
||||
@@ -79,16 +78,12 @@ const methods = {
|
||||
},
|
||||
thumbup: {
|
||||
url: thumbup,
|
||||
method: 'put',
|
||||
method: 'patch',
|
||||
},
|
||||
chatsTts: {
|
||||
url: chatsTts,
|
||||
method: 'post',
|
||||
},
|
||||
ask: {
|
||||
url: ask,
|
||||
method: 'post',
|
||||
},
|
||||
chatsMindmap: {
|
||||
url: chatsMindmap,
|
||||
method: 'post',
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
// plugin
|
||||
llmTools: `${webAPI}/plugin/llm_tools`,
|
||||
|
||||
chatsTranscriptions: `${restAPIv1}/chats/transcriptions`,
|
||||
chatsTranscriptions: `${restAPIv1}/chat/audio/transcription`,
|
||||
|
||||
// knowledge base
|
||||
|
||||
@@ -147,12 +147,12 @@ export default {
|
||||
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}/messages/${msgId}`,
|
||||
thumbup: (chatId: string, sessionId: string, msgId: string) =>
|
||||
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}/messages/${msgId}/feedback`,
|
||||
completionUrl: (chatId: string, sessionId: string) =>
|
||||
`${restAPIv1}/chats/${chatId}/sessions/${sessionId}/completions`,
|
||||
chatsTts: `${restAPIv1}/chats/tts`,
|
||||
ask: `${restAPIv1}/chats/ask`,
|
||||
chatsMindmap: `${restAPIv1}/chats/mindmap`,
|
||||
chatsRelatedQuestions: `${restAPIv1}/chats/related_questions`,
|
||||
completionUrl: `${restAPIv1}/chat/completions`,
|
||||
chatsTts: `${restAPIv1}/chat/audio/speech`,
|
||||
searchCompletion: (searchId: string) =>
|
||||
`${restAPIv1}/searches/${searchId}/completion`,
|
||||
chatsMindmap: `${restAPIv1}/chat/mindmap`,
|
||||
chatsRelatedQuestions: `${restAPIv1}/chat/recommandation`,
|
||||
|
||||
// next chat
|
||||
fetchExternalChatInfo: (id: string) => `${restAPIv1}/chatbots/${id}/info`,
|
||||
|
||||
Reference in New Issue
Block a user