mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-02 22:07:31 +08:00
Refa: restore openai-compatible chat completions api (#14380)
### What problem does this PR solve? restore openai-compatible chat completions api ### Type of change - [x] Refactoring
This commit is contained in:
309
api/apps/restful_apis/openai_api.py
Normal file
309
api/apps/restful_apis/openai_api.py
Normal file
@@ -0,0 +1,309 @@
|
||||
#
|
||||
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
from quart import Response, jsonify
|
||||
|
||||
from api.apps import current_user, login_required
|
||||
from api.db.services.dialog_service import DialogService, async_chat
|
||||
from api.db.services.doc_metadata_service import DocMetadataService
|
||||
from api.db.services.tenant_llm_service import TenantLLMService
|
||||
from api.utils.api_utils import get_error_data_result, get_request_json, validate_request
|
||||
from common.constants import RetCode, StatusEnum
|
||||
from common.metadata_utils import convert_conditions, meta_filter
|
||||
from common.token_utils import num_tokens_from_string
|
||||
from rag.prompts.generator import chunks_format
|
||||
|
||||
def _validate_llm_id(llm_id, tenant_id, llm_setting=None):
|
||||
if not llm_id:
|
||||
return None
|
||||
|
||||
llm_name, llm_factory = TenantLLMService.split_model_name_and_factory(llm_id)
|
||||
model_type = (llm_setting or {}).get("model_type")
|
||||
if model_type not in {"chat", "image2text"}:
|
||||
model_type = "chat"
|
||||
|
||||
if not TenantLLMService.query(
|
||||
tenant_id=tenant_id,
|
||||
llm_name=llm_name,
|
||||
llm_factory=llm_factory,
|
||||
model_type=model_type,
|
||||
):
|
||||
return f"`llm_id` {llm_id} doesn't exist"
|
||||
return None
|
||||
|
||||
|
||||
def _build_reference_chunks(reference, include_metadata=False, metadata_fields=None):
|
||||
chunks = chunks_format(reference)
|
||||
if not include_metadata:
|
||||
return chunks
|
||||
|
||||
doc_ids_by_kb = {}
|
||||
for chunk in chunks:
|
||||
kb_id = chunk.get("dataset_id")
|
||||
doc_id = chunk.get("document_id")
|
||||
if not kb_id or not doc_id:
|
||||
continue
|
||||
doc_ids_by_kb.setdefault(kb_id, set()).add(doc_id)
|
||||
|
||||
if not doc_ids_by_kb:
|
||||
return chunks
|
||||
|
||||
meta_by_doc = {}
|
||||
for kb_id, doc_ids in doc_ids_by_kb.items():
|
||||
meta_map = DocMetadataService.get_metadata_for_documents(list(doc_ids), kb_id)
|
||||
if meta_map:
|
||||
meta_by_doc.update(meta_map)
|
||||
|
||||
if metadata_fields is not None:
|
||||
metadata_fields = {f for f in metadata_fields if isinstance(f, str)}
|
||||
if not metadata_fields:
|
||||
return chunks
|
||||
|
||||
for chunk in chunks:
|
||||
doc_id = chunk.get("document_id")
|
||||
if not doc_id:
|
||||
continue
|
||||
meta = meta_by_doc.get(doc_id)
|
||||
if not meta:
|
||||
continue
|
||||
if metadata_fields is not None:
|
||||
meta = {k: v for k, v in meta.items() if k in metadata_fields}
|
||||
if meta:
|
||||
chunk["document_metadata"] = meta
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
def _build_sse_response(body):
|
||||
resp = Response(body, 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
|
||||
|
||||
|
||||
@manager.route("/openai/<chat_id>/chat/completions", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("model", "messages")
|
||||
async def openai_chat_completions(chat_id):
|
||||
req = await get_request_json()
|
||||
|
||||
extra_body = req.get("extra_body") or {}
|
||||
if extra_body and not isinstance(extra_body, dict):
|
||||
return get_error_data_result("extra_body must be an object.")
|
||||
|
||||
need_reference = bool(extra_body.get("reference", False))
|
||||
reference_metadata = extra_body.get("reference_metadata") or {}
|
||||
if reference_metadata and not isinstance(reference_metadata, dict):
|
||||
return get_error_data_result("reference_metadata must be an object.")
|
||||
include_reference_metadata = bool(reference_metadata.get("include", False))
|
||||
metadata_fields = reference_metadata.get("fields")
|
||||
if metadata_fields is not None and not isinstance(metadata_fields, list):
|
||||
return get_error_data_result("reference_metadata.fields must be an array.")
|
||||
|
||||
messages = req.get("messages", [])
|
||||
if len(messages) < 1:
|
||||
return get_error_data_result("You have to provide messages.")
|
||||
if messages[-1]["role"] != "user":
|
||||
return get_error_data_result("The last content of this conversation is not from user.")
|
||||
|
||||
prompt = messages[-1]["content"]
|
||||
context_token_used = sum(num_tokens_from_string(message["content"]) for message in messages)
|
||||
requested_model = req.get("model", "") or ""
|
||||
completion_id = f"chatcmpl-{chat_id}"
|
||||
|
||||
dia = DialogService.query(tenant_id=current_user.id, id=chat_id, status=StatusEnum.VALID.value)
|
||||
if not dia:
|
||||
return get_error_data_result(f"You don't own the chat {chat_id}")
|
||||
dia = dia[0]
|
||||
|
||||
using_placeholder_model = requested_model == "model"
|
||||
if using_placeholder_model:
|
||||
requested_model = dia.llm_id or requested_model
|
||||
else:
|
||||
llm_id_error = _validate_llm_id(requested_model, current_user.id, {"model_type": "chat"})
|
||||
if llm_id_error:
|
||||
return get_error_data_result(message=llm_id_error, code=RetCode.ARGUMENT_ERROR)
|
||||
dia.llm_id = requested_model
|
||||
if not TenantLLMService.get_api_key(tenant_id=dia.tenant_id, model_name=requested_model):
|
||||
return get_error_data_result(message=f"Cannot use specified model {requested_model}.")
|
||||
|
||||
metadata_condition = extra_body.get("metadata_condition") or {}
|
||||
if metadata_condition and not isinstance(metadata_condition, dict):
|
||||
return get_error_data_result(message="metadata_condition must be an object.")
|
||||
|
||||
doc_ids_str = None
|
||||
if metadata_condition:
|
||||
metas = DocMetadataService.get_flatted_meta_by_kbs(dia.kb_ids or [])
|
||||
filtered_doc_ids = meta_filter(
|
||||
metas,
|
||||
convert_conditions(metadata_condition),
|
||||
metadata_condition.get("logic", "and"),
|
||||
)
|
||||
if metadata_condition.get("conditions") and not filtered_doc_ids:
|
||||
filtered_doc_ids = ["-999"]
|
||||
doc_ids_str = ",".join(filtered_doc_ids) if filtered_doc_ids else None
|
||||
|
||||
msg = []
|
||||
for message in messages:
|
||||
if message["role"] == "system":
|
||||
continue
|
||||
if message["role"] == "assistant" and not msg:
|
||||
continue
|
||||
msg.append(message)
|
||||
|
||||
tools = None
|
||||
toolcall_session = None
|
||||
stream_mode = req.get("stream", True)
|
||||
|
||||
if stream_mode:
|
||||
async def streamed_response_generator():
|
||||
token_used = 0
|
||||
last_ans = {}
|
||||
full_content = ""
|
||||
final_answer = None
|
||||
final_reference = None
|
||||
in_think = False
|
||||
response = {
|
||||
"id": completion_id,
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "",
|
||||
"role": "assistant",
|
||||
"function_call": None,
|
||||
"tool_calls": None,
|
||||
"reasoning_content": "",
|
||||
},
|
||||
"finish_reason": None,
|
||||
"index": 0,
|
||||
"logprobs": None,
|
||||
}
|
||||
],
|
||||
"created": int(time.time()),
|
||||
"model": requested_model,
|
||||
"object": "chat.completion.chunk",
|
||||
"system_fingerprint": "",
|
||||
"usage": None,
|
||||
}
|
||||
|
||||
try:
|
||||
chat_kwargs = {"toolcall_session": toolcall_session, "tools": tools, "quote": need_reference}
|
||||
if doc_ids_str:
|
||||
chat_kwargs["doc_ids"] = doc_ids_str
|
||||
async for ans in async_chat(dia, msg, True, **chat_kwargs):
|
||||
last_ans = ans
|
||||
if ans.get("final"):
|
||||
if ans.get("answer"):
|
||||
full_content = ans["answer"]
|
||||
response["choices"][0]["delta"]["content"] = full_content
|
||||
response["choices"][0]["delta"]["reasoning_content"] = None
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
final_answer = full_content
|
||||
final_reference = ans.get("reference", {})
|
||||
continue
|
||||
if ans.get("start_to_think"):
|
||||
in_think = True
|
||||
continue
|
||||
if ans.get("end_to_think"):
|
||||
in_think = False
|
||||
continue
|
||||
delta = ans.get("answer") or ""
|
||||
if not delta:
|
||||
continue
|
||||
token_used += num_tokens_from_string(delta)
|
||||
if in_think:
|
||||
response["choices"][0]["delta"]["reasoning_content"] = delta
|
||||
response["choices"][0]["delta"]["content"] = None
|
||||
else:
|
||||
full_content += delta
|
||||
response["choices"][0]["delta"]["content"] = delta
|
||||
response["choices"][0]["delta"]["reasoning_content"] = None
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
except Exception as e:
|
||||
response["choices"][0]["delta"]["content"] = "**ERROR**: " + str(e)
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
|
||||
response["choices"][0]["delta"]["content"] = None
|
||||
response["choices"][0]["delta"]["reasoning_content"] = None
|
||||
response["choices"][0]["finish_reason"] = "stop"
|
||||
prompt_tokens = num_tokens_from_string(prompt)
|
||||
response["usage"] = {
|
||||
"prompt_tokens": prompt_tokens,
|
||||
"completion_tokens": token_used,
|
||||
"total_tokens": prompt_tokens + token_used,
|
||||
}
|
||||
if need_reference:
|
||||
reference_payload = final_reference if final_reference is not None else last_ans.get("reference", [])
|
||||
response["choices"][0]["delta"]["reference"] = _build_reference_chunks(
|
||||
reference_payload,
|
||||
include_metadata=include_reference_metadata,
|
||||
metadata_fields=metadata_fields,
|
||||
)
|
||||
response["choices"][0]["delta"]["final_content"] = final_answer if final_answer is not None else full_content
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
yield "data:[DONE]\n\n"
|
||||
|
||||
return _build_sse_response(streamed_response_generator())
|
||||
|
||||
answer = None
|
||||
chat_kwargs = {"toolcall_session": toolcall_session, "tools": tools, "quote": need_reference}
|
||||
if doc_ids_str:
|
||||
chat_kwargs["doc_ids"] = doc_ids_str
|
||||
async for ans in async_chat(dia, msg, False, **chat_kwargs):
|
||||
answer = ans
|
||||
break
|
||||
|
||||
content = answer["answer"]
|
||||
response = {
|
||||
"id": completion_id,
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": requested_model,
|
||||
"usage": {
|
||||
"prompt_tokens": num_tokens_from_string(prompt),
|
||||
"completion_tokens": num_tokens_from_string(content),
|
||||
"total_tokens": num_tokens_from_string(prompt) + num_tokens_from_string(content),
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": context_token_used,
|
||||
"accepted_prediction_tokens": num_tokens_from_string(content),
|
||||
"rejected_prediction_tokens": 0,
|
||||
},
|
||||
},
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
},
|
||||
"logprobs": None,
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
if need_reference:
|
||||
response["choices"][0]["message"]["reference"] = _build_reference_chunks(
|
||||
answer.get("reference", {}),
|
||||
include_metadata=include_reference_metadata,
|
||||
metadata_fields=metadata_fields,
|
||||
)
|
||||
|
||||
return jsonify(response)
|
||||
@@ -15,30 +15,23 @@
|
||||
#
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import logging
|
||||
|
||||
from quart import Response, jsonify, request
|
||||
|
||||
from common.token_utils import num_tokens_from_string
|
||||
from quart import Response, request
|
||||
|
||||
from agent.canvas import Canvas
|
||||
from api.db.db_models import APIToken
|
||||
from api.db.services.api_service import API4ConversationService
|
||||
from api.db.services.canvas_service import UserCanvasService
|
||||
from api.db.services.canvas_service import completion as agent_completion
|
||||
from api.db.services.conversation_service import ConversationService
|
||||
from api.db.services.user_canvas_version import UserCanvasVersionService
|
||||
from api.db.services.conversation_service import async_iframe_completion as iframe_completion
|
||||
from api.db.services.conversation_service import async_completion as rag_completion
|
||||
from api.db.services.dialog_service import DialogService, async_ask, async_chat, gen_mindmap
|
||||
from api.db.services.dialog_service import DialogService, async_ask, gen_mindmap
|
||||
from api.db.services.doc_metadata_service import DocMetadataService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from common.metadata_utils import apply_meta_data_filter, convert_conditions, meta_filter
|
||||
from common.metadata_utils import apply_meta_data_filter
|
||||
from api.db.services.search_service import SearchService
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type, get_model_config_by_id, \
|
||||
@@ -48,8 +41,8 @@ from api.utils.api_utils import check_duplicate_ids, get_error_data_result, get_
|
||||
get_result, get_request_json, server_error_response, token_required, validate_request
|
||||
from rag.app.tag import label_question
|
||||
from rag.prompts.template import load_prompt
|
||||
from rag.prompts.generator import cross_languages, keyword_extraction, chunks_format
|
||||
from common.constants import RetCode, LLMType, StatusEnum
|
||||
from rag.prompts.generator import cross_languages, keyword_extraction
|
||||
from common.constants import RetCode, LLMType
|
||||
from common import settings
|
||||
|
||||
|
||||
@@ -90,349 +83,6 @@ async def create_agent_session(tenant_id, agent_id):
|
||||
return get_result(data=conv)
|
||||
|
||||
|
||||
@manager.route("/chats/<chat_id>/completions", methods=["POST"]) # noqa: F821
|
||||
@token_required
|
||||
async def chat_completion(tenant_id, chat_id):
|
||||
req = await get_request_json()
|
||||
if not req:
|
||||
req = {"question": ""}
|
||||
if not req.get("session_id"):
|
||||
req["question"] = ""
|
||||
dia = DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value)
|
||||
if not dia:
|
||||
return get_error_data_result(f"You don't own the chat {chat_id}")
|
||||
dia = dia[0]
|
||||
if req.get("session_id"):
|
||||
if not ConversationService.query(id=req["session_id"], dialog_id=chat_id):
|
||||
return get_error_data_result(f"You don't own the session {req['session_id']}")
|
||||
|
||||
metadata_condition = req.get("metadata_condition") or {}
|
||||
if metadata_condition and not isinstance(metadata_condition, dict):
|
||||
return get_error_data_result(message="metadata_condition must be an object.")
|
||||
|
||||
if metadata_condition and req.get("question"):
|
||||
metas = DocMetadataService.get_flatted_meta_by_kbs(dia.kb_ids or [])
|
||||
filtered_doc_ids = meta_filter(
|
||||
metas,
|
||||
convert_conditions(metadata_condition),
|
||||
metadata_condition.get("logic", "and"),
|
||||
)
|
||||
if metadata_condition.get("conditions") and not filtered_doc_ids:
|
||||
filtered_doc_ids = ["-999"]
|
||||
|
||||
if filtered_doc_ids:
|
||||
req["doc_ids"] = ",".join(filtered_doc_ids)
|
||||
else:
|
||||
req.pop("doc_ids", None)
|
||||
|
||||
if req.get("stream", True):
|
||||
resp = Response(rag_completion(tenant_id, chat_id, **req), 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
|
||||
else:
|
||||
answer = None
|
||||
async for ans in rag_completion(tenant_id, chat_id, **req):
|
||||
answer = ans
|
||||
break
|
||||
return get_result(data=answer)
|
||||
|
||||
|
||||
@manager.route("/chats_openai/<chat_id>/chat/completions", methods=["POST"]) # noqa: F821
|
||||
@validate_request("model", "messages") # noqa: F821
|
||||
@token_required
|
||||
async def chat_completion_openai_like(tenant_id, chat_id):
|
||||
"""
|
||||
OpenAI-like chat completion API that simulates the behavior of OpenAI's completions endpoint.
|
||||
|
||||
This function allows users to interact with a model and receive responses based on a series of historical messages.
|
||||
If `stream` is set to True (by default), the response will be streamed in chunks, mimicking the OpenAI-style API.
|
||||
Set `stream` to False explicitly, the response will be returned in a single complete answer.
|
||||
|
||||
Reference:
|
||||
|
||||
- If `stream` is True, the final answer and reference information will appear in the **last chunk** of the stream.
|
||||
- If `stream` is False, the reference will be included in `choices[0].message.reference`.
|
||||
- If `extra_body.reference_metadata.include` is True, each reference chunk may include `document_metadata` in both streaming and non-streaming responses.
|
||||
|
||||
Example usage:
|
||||
|
||||
curl -X POST https://ragflow_address.com/api/v1/chats_openai/<chat_id>/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $RAGFLOW_API_KEY" \
|
||||
-d '{
|
||||
"model": "model",
|
||||
"messages": [{"role": "user", "content": "Say this is a test!"}],
|
||||
"stream": true
|
||||
}'
|
||||
|
||||
Alternatively, you can use Python's `OpenAI` client:
|
||||
|
||||
NOTE: Streaming via `client.chat.completions.create(stream=True, ...)` does
|
||||
not return `reference` currently. The only way to return `reference` is
|
||||
non-stream mode with `with_raw_response`.
|
||||
|
||||
from openai import OpenAI
|
||||
import json
|
||||
|
||||
model = "model"
|
||||
client = OpenAI(api_key="ragflow-api-key", base_url=f"http://ragflow_address/api/v1/chats_openai/<chat_id>")
|
||||
|
||||
stream = True
|
||||
reference = True
|
||||
|
||||
request_kwargs = dict(
|
||||
model="model",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Who are you?"},
|
||||
{"role": "assistant", "content": "I am an AI assistant named..."},
|
||||
{"role": "user", "content": "Can you tell me how to install neovim"},
|
||||
],
|
||||
extra_body={
|
||||
"reference": reference,
|
||||
"reference_metadata": {
|
||||
"include": True,
|
||||
"fields": ["author", "year", "source"],
|
||||
},
|
||||
"metadata_condition": {
|
||||
"logic": "and",
|
||||
"conditions": [
|
||||
{
|
||||
"name": "author",
|
||||
"comparison_operator": "is",
|
||||
"value": "bob"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if stream:
|
||||
completion = client.chat.completions.create(stream=True, **request_kwargs)
|
||||
for chunk in completion:
|
||||
print(chunk)
|
||||
else:
|
||||
resp = client.chat.completions.with_raw_response.create(
|
||||
stream=False, **request_kwargs
|
||||
)
|
||||
print("status:", resp.http_response.status_code)
|
||||
raw_text = resp.http_response.text
|
||||
print("raw:", raw_text)
|
||||
|
||||
data = json.loads(raw_text)
|
||||
print("assistant:", data["choices"][0]["message"].get("content"))
|
||||
print("reference:", data["choices"][0]["message"].get("reference"))
|
||||
|
||||
"""
|
||||
req = await get_request_json()
|
||||
|
||||
extra_body = req.get("extra_body") or {}
|
||||
if extra_body and not isinstance(extra_body, dict):
|
||||
return get_error_data_result("extra_body must be an object.")
|
||||
|
||||
need_reference = bool(extra_body.get("reference", False))
|
||||
reference_metadata = extra_body.get("reference_metadata") or {}
|
||||
if reference_metadata and not isinstance(reference_metadata, dict):
|
||||
return get_error_data_result("reference_metadata must be an object.")
|
||||
include_reference_metadata = bool(reference_metadata.get("include", False))
|
||||
metadata_fields = reference_metadata.get("fields")
|
||||
if metadata_fields is not None and not isinstance(metadata_fields, list):
|
||||
return get_error_data_result("reference_metadata.fields must be an array.")
|
||||
|
||||
messages = req.get("messages", [])
|
||||
# To prevent empty [] input
|
||||
if len(messages) < 1:
|
||||
return get_error_data_result("You have to provide messages.")
|
||||
if messages[-1]["role"] != "user":
|
||||
return get_error_data_result("The last content of this conversation is not from user.")
|
||||
|
||||
prompt = messages[-1]["content"]
|
||||
# Treat context tokens as reasoning tokens
|
||||
context_token_used = sum(num_tokens_from_string(message["content"]) for message in messages)
|
||||
|
||||
dia = DialogService.query(tenant_id=tenant_id, id=chat_id, status=StatusEnum.VALID.value)
|
||||
if not dia:
|
||||
return get_error_data_result(f"You don't own the chat {chat_id}")
|
||||
dia = dia[0]
|
||||
|
||||
metadata_condition = extra_body.get("metadata_condition") or {}
|
||||
if metadata_condition and not isinstance(metadata_condition, dict):
|
||||
return get_error_data_result(message="metadata_condition must be an object.")
|
||||
|
||||
doc_ids_str = None
|
||||
if metadata_condition:
|
||||
metas = DocMetadataService.get_flatted_meta_by_kbs(dia.kb_ids or [])
|
||||
filtered_doc_ids = meta_filter(
|
||||
metas,
|
||||
convert_conditions(metadata_condition),
|
||||
metadata_condition.get("logic", "and"),
|
||||
)
|
||||
if metadata_condition.get("conditions") and not filtered_doc_ids:
|
||||
filtered_doc_ids = ["-999"]
|
||||
doc_ids_str = ",".join(filtered_doc_ids) if filtered_doc_ids else None
|
||||
|
||||
# Filter system and non-sense assistant messages
|
||||
msg = []
|
||||
for m in messages:
|
||||
if m["role"] == "system":
|
||||
continue
|
||||
if m["role"] == "assistant" and not msg:
|
||||
continue
|
||||
msg.append(m)
|
||||
|
||||
# tools = get_tools()
|
||||
# toolcall_session = SimpleFunctionCallServer()
|
||||
tools = None
|
||||
toolcall_session = None
|
||||
|
||||
if req.get("stream", True):
|
||||
# The value for the usage field on all chunks except for the last one will be null.
|
||||
# The usage field on the last chunk contains token usage statistics for the entire request.
|
||||
# The choices field on the last chunk will always be an empty array [].
|
||||
async def streamed_response_generator(chat_id, dia, msg):
|
||||
token_used = 0
|
||||
last_ans = {}
|
||||
full_content = ""
|
||||
full_reasoning = ""
|
||||
final_answer = None
|
||||
final_reference = None
|
||||
in_think = False
|
||||
response = {
|
||||
"id": f"chatcmpl-{chat_id}",
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "",
|
||||
"role": "assistant",
|
||||
"function_call": None,
|
||||
"tool_calls": None,
|
||||
"reasoning_content": "",
|
||||
},
|
||||
"finish_reason": None,
|
||||
"index": 0,
|
||||
"logprobs": None,
|
||||
}
|
||||
],
|
||||
"created": int(time.time()),
|
||||
"model": "model",
|
||||
"object": "chat.completion.chunk",
|
||||
"system_fingerprint": "",
|
||||
"usage": None,
|
||||
}
|
||||
|
||||
try:
|
||||
chat_kwargs = {"toolcall_session": toolcall_session, "tools": tools, "quote": need_reference}
|
||||
if doc_ids_str:
|
||||
chat_kwargs["doc_ids"] = doc_ids_str
|
||||
async for ans in async_chat(dia, msg, True, **chat_kwargs):
|
||||
last_ans = ans
|
||||
if ans.get("final"):
|
||||
if ans.get("answer"):
|
||||
full_content = ans["answer"]
|
||||
response["choices"][0]["delta"]["content"] = full_content
|
||||
response["choices"][0]["delta"]["reasoning_content"] = None
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
final_answer = full_content
|
||||
final_reference = ans.get("reference", {})
|
||||
continue
|
||||
if ans.get("start_to_think"):
|
||||
in_think = True
|
||||
continue
|
||||
if ans.get("end_to_think"):
|
||||
in_think = False
|
||||
continue
|
||||
delta = ans.get("answer") or ""
|
||||
if not delta:
|
||||
continue
|
||||
token_used += num_tokens_from_string(delta)
|
||||
if in_think:
|
||||
full_reasoning += delta
|
||||
response["choices"][0]["delta"]["reasoning_content"] = delta
|
||||
response["choices"][0]["delta"]["content"] = None
|
||||
else:
|
||||
full_content += delta
|
||||
response["choices"][0]["delta"]["content"] = delta
|
||||
response["choices"][0]["delta"]["reasoning_content"] = None
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
except Exception as e:
|
||||
response["choices"][0]["delta"]["content"] = "**ERROR**: " + str(e)
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
|
||||
# The last chunk
|
||||
response["choices"][0]["delta"]["content"] = None
|
||||
response["choices"][0]["delta"]["reasoning_content"] = None
|
||||
response["choices"][0]["finish_reason"] = "stop"
|
||||
prompt_tokens = num_tokens_from_string(prompt)
|
||||
response["usage"] = {"prompt_tokens": prompt_tokens, "completion_tokens": token_used, "total_tokens": prompt_tokens + token_used}
|
||||
if need_reference:
|
||||
reference_payload = final_reference if final_reference is not None else last_ans.get("reference", [])
|
||||
response["choices"][0]["delta"]["reference"] = _build_reference_chunks(
|
||||
reference_payload,
|
||||
include_metadata=include_reference_metadata,
|
||||
metadata_fields=metadata_fields,
|
||||
)
|
||||
response["choices"][0]["delta"]["final_content"] = final_answer if final_answer is not None else full_content
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
yield "data:[DONE]\n\n"
|
||||
|
||||
resp = Response(streamed_response_generator(chat_id, dia, msg), 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
|
||||
else:
|
||||
answer = None
|
||||
chat_kwargs = {"toolcall_session": toolcall_session, "tools": tools, "quote": need_reference}
|
||||
if doc_ids_str:
|
||||
chat_kwargs["doc_ids"] = doc_ids_str
|
||||
async for ans in async_chat(dia, msg, False, **chat_kwargs):
|
||||
# focus answer content only
|
||||
answer = ans
|
||||
break
|
||||
content = answer["answer"]
|
||||
|
||||
response = {
|
||||
"id": f"chatcmpl-{chat_id}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": req.get("model", ""),
|
||||
"usage": {
|
||||
"prompt_tokens": num_tokens_from_string(prompt),
|
||||
"completion_tokens": num_tokens_from_string(content),
|
||||
"total_tokens": num_tokens_from_string(prompt) + num_tokens_from_string(content),
|
||||
"completion_tokens_details": {
|
||||
"reasoning_tokens": context_token_used,
|
||||
"accepted_prediction_tokens": num_tokens_from_string(content),
|
||||
"rejected_prediction_tokens": 0, # 0 for simplicity
|
||||
},
|
||||
},
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
},
|
||||
"logprobs": None,
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
if need_reference:
|
||||
response["choices"][0]["message"]["reference"] = _build_reference_chunks(
|
||||
answer.get("reference", {}),
|
||||
include_metadata=include_reference_metadata,
|
||||
metadata_fields=metadata_fields,
|
||||
)
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@manager.route("/agents/<agent_id>/sessions", methods=["DELETE"]) # noqa: F821
|
||||
@token_required
|
||||
async def delete_agent_session(tenant_id, agent_id):
|
||||
@@ -486,97 +136,6 @@ async def delete_agent_session(tenant_id, agent_id):
|
||||
return get_result()
|
||||
|
||||
|
||||
@manager.route("/sessions/ask", methods=["POST"]) # noqa: F821
|
||||
@token_required
|
||||
async def ask_about(tenant_id):
|
||||
req = await get_request_json()
|
||||
if not req.get("question"):
|
||||
return get_error_data_result("`question` is required.")
|
||||
if not req.get("dataset_ids"):
|
||||
return get_error_data_result("`dataset_ids` is required.")
|
||||
if not isinstance(req.get("dataset_ids"), list):
|
||||
return get_error_data_result("`dataset_ids` should be a list.")
|
||||
req["kb_ids"] = req.pop("dataset_ids")
|
||||
for kb_id in req["kb_ids"]:
|
||||
if not KnowledgebaseService.accessible(kb_id, tenant_id):
|
||||
return get_error_data_result(f"You don't own the dataset {kb_id}.")
|
||||
kbs = KnowledgebaseService.query(id=kb_id)
|
||||
kb = kbs[0]
|
||||
if kb.chunk_num == 0:
|
||||
return get_error_data_result(f"The dataset {kb_id} doesn't own parsed file")
|
||||
uid = tenant_id
|
||||
|
||||
async def stream():
|
||||
nonlocal req, uid
|
||||
try:
|
||||
async for ans in async_ask(req["question"], req["kb_ids"], uid):
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
except Exception as e:
|
||||
yield "data:" + json.dumps(
|
||||
{"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e), "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
|
||||
|
||||
|
||||
@manager.route("/sessions/related_questions", methods=["POST"]) # noqa: F821
|
||||
@token_required
|
||||
async def related_questions(tenant_id):
|
||||
req = await get_request_json()
|
||||
if not req.get("question"):
|
||||
return get_error_data_result("`question` is required.")
|
||||
question = req["question"]
|
||||
industry = req.get("industry", "")
|
||||
chat_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.CHAT)
|
||||
chat_mdl = LLMBundle(tenant_id, chat_model_config)
|
||||
prompt = """
|
||||
Objective: To generate search terms related to the user's search keywords, helping users find more valuable information.
|
||||
Instructions:
|
||||
- Based on the keywords provided by the user, generate 5-10 related search terms.
|
||||
- Each search term should be directly or indirectly related to the keyword, guiding the user to find more valuable information.
|
||||
- Use common, general terms as much as possible, avoiding obscure words or technical jargon.
|
||||
- Keep the term length between 2-4 words, concise and clear.
|
||||
- DO NOT translate, use the language of the original keywords.
|
||||
"""
|
||||
if industry:
|
||||
prompt += f" - Ensure all search terms are relevant to the industry: {industry}.\n"
|
||||
prompt += """
|
||||
### Example:
|
||||
Keywords: Chinese football
|
||||
Related search terms:
|
||||
1. Current status of Chinese football
|
||||
2. Reform of Chinese football
|
||||
3. Youth training of Chinese football
|
||||
4. Chinese football in the Asian Cup
|
||||
5. Chinese football in the World Cup
|
||||
|
||||
Reason:
|
||||
- When searching, users often only use one or two keywords, making it difficult to fully express their information needs.
|
||||
- Generating related search terms can help users dig deeper into relevant information and improve search efficiency.
|
||||
- At the same time, related terms can also help search engines better understand user needs and return more accurate search results.
|
||||
|
||||
"""
|
||||
ans = await chat_mdl.async_chat(
|
||||
prompt,
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"""
|
||||
Keywords: {question}
|
||||
Related search terms:
|
||||
""",
|
||||
}
|
||||
],
|
||||
{"temperature": 0.9},
|
||||
)
|
||||
return get_result(data=[re.sub(r"^[0-9]\. ", "", a) for a in ans.split("\n") if re.match(r"^[0-9]\. ", a)])
|
||||
|
||||
|
||||
@manager.route("/chatbots/<dialog_id>/completions", methods=["POST"]) # noqa: F821
|
||||
async def chatbot_completions(dialog_id):
|
||||
@@ -968,126 +527,3 @@ async def mindmap():
|
||||
return server_error_response(Exception(mind_map["error"]))
|
||||
return get_json_result(data=mind_map)
|
||||
|
||||
@manager.route("/sequence2txt", methods=["POST"]) # noqa: F821
|
||||
@token_required
|
||||
async def sequence2txt(tenant_id):
|
||||
req = await request.form
|
||||
stream_mode = req.get("stream", "false").lower() == "true"
|
||||
files = await request.files
|
||||
if "file" not in files:
|
||||
return get_error_data_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_error_data_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)
|
||||
|
||||
try:
|
||||
default_asr_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.SPEECH2TEXT)
|
||||
except Exception as e:
|
||||
return get_error_data_result(message=str(e))
|
||||
asr_mdl=LLMBundle(tenant_id, default_asr_model_config)
|
||||
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
|
||||
@token_required
|
||||
async def tts(tenant_id):
|
||||
req = await get_request_json()
|
||||
text = req["text"]
|
||||
|
||||
try:
|
||||
default_tts_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.TTS)
|
||||
except Exception as e:
|
||||
return get_error_data_result(message=str(e))
|
||||
tts_mdl = LLMBundle(tenant_id, default_tts_model_config)
|
||||
|
||||
def stream_audio():
|
||||
try:
|
||||
for txt in re.split(r"[,。/《》?;:!\n\r:;]+", text):
|
||||
for chunk in tts_mdl.tts(txt):
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
yield ("data:" + json.dumps({"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e)}}, ensure_ascii=False)).encode("utf-8")
|
||||
|
||||
resp = Response(stream_audio(), mimetype="audio/mpeg")
|
||||
resp.headers.add_header("Cache-Control", "no-cache")
|
||||
resp.headers.add_header("Connection", "keep-alive")
|
||||
resp.headers.add_header("X-Accel-Buffering", "no")
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def _build_reference_chunks(reference, include_metadata=False, metadata_fields=None):
|
||||
chunks = chunks_format(reference)
|
||||
if not include_metadata:
|
||||
return chunks
|
||||
|
||||
doc_ids_by_kb = {}
|
||||
for chunk in chunks:
|
||||
kb_id = chunk.get("dataset_id")
|
||||
doc_id = chunk.get("document_id")
|
||||
if not kb_id or not doc_id:
|
||||
continue
|
||||
doc_ids_by_kb.setdefault(kb_id, set()).add(doc_id)
|
||||
|
||||
if not doc_ids_by_kb:
|
||||
return chunks
|
||||
|
||||
meta_by_doc = {}
|
||||
for kb_id, doc_ids in doc_ids_by_kb.items():
|
||||
meta_map = DocMetadataService.get_metadata_for_documents(list(doc_ids), kb_id)
|
||||
if meta_map:
|
||||
meta_by_doc.update(meta_map)
|
||||
|
||||
if metadata_fields is not None:
|
||||
metadata_fields = {f for f in metadata_fields if isinstance(f, str)}
|
||||
if not metadata_fields:
|
||||
return chunks
|
||||
|
||||
for chunk in chunks:
|
||||
doc_id = chunk.get("document_id")
|
||||
if not doc_id:
|
||||
continue
|
||||
meta = meta_by_doc.get(doc_id)
|
||||
if not meta:
|
||||
continue
|
||||
if metadata_fields is not None:
|
||||
meta = {k: v for k, v in meta.items() if k in metadata_fields}
|
||||
if meta:
|
||||
chunk["document_metadata"] = meta
|
||||
|
||||
return chunks
|
||||
|
||||
Reference in New Issue
Block a user