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:
buua436
2026-04-22 10:49:11 +08:00
committed by GitHub
parent bfac0195df
commit 6baf74afc1
14 changed files with 362 additions and 160 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
```

View File

@@ -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."
}
```

View File

@@ -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"))

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
],
);

View File

@@ -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',

View File

@@ -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`,