Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/models_provider/api/chat_completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# coding=utf-8
from common.mixins.api_mixin import APIMixin
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from models_provider.serializers.chat_completion_serializers import ChatCompletionSerializers


class ChatCompletionAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description=_("workspace id"),
type=OpenApiTypes.STR,
location=OpenApiParameter.PATH, # type: ignore
required=True,
),
OpenApiParameter(
name="model_id",
description=_("model id"),
type=OpenApiTypes.STR,
location=OpenApiParameter.PATH, # type: ignore
required=True,
),
]

@staticmethod
def get_request():
return ChatCompletionSerializers
145 changes: 145 additions & 0 deletions apps/models_provider/serializers/chat_completion_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# coding=utf-8
"""
@project: MaxKB
@file: chat_completion_serializers.py
@desc: 通用流式补全(支持 function calling),供前端工作流 AI 生成的 agent 循环逐轮调用
"""

import json

from common.config.embedding_config import ModelManage
from common.exception.app_exception import AppApiException
from common.utils.common import to_stream_response_simple
from common.utils.logger import maxkb_logger
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from langchain_core.messages import convert_to_messages
from models_provider.base_model_provider import ModelTypeConst
from models_provider.models import Model
from models_provider.tools import (
get_model,
get_model_default_params,
get_model_instance_by_model_workspace_id,
reset_model_params,
)
from rest_framework import serializers

# messages 条数护栏,迭代上限由前端 agent 循环控制
MAX_MESSAGES = 400


class ChatCompletionSerializers(serializers.Serializer):
workspace_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Workspace ID"))
model_id = serializers.CharField(required=True, label=_("Model"))
model_params_setting = serializers.JSONField(required=False, default=dict, label=_("Model params setting"))
# OpenAI 格式消息:role 为 system/user/assistant/tool,assistant 可带 tool_calls 与 reasoning_content
messages = serializers.ListField(child=serializers.DictField(), required=True, label=_("Chat context"))
# OpenAI function 定义:[{type:'function', function:{name, description, parameters}}]
tools = serializers.ListField(child=serializers.DictField(), required=False, default=list, label=_("Tools"))

def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
messages = self.data.get("messages") or []
if not messages:
raise AppApiException(400, _("Messages cannot be empty"))
if len(messages) > MAX_MESSAGES:
raise AppApiException(400, _("Too many messages"))
for message in messages:
if message.get("role") not in ("system", "user", "assistant", "tool"):
raise AppApiException(400, _("Authentication failed. Please verify that the parameters are correct."))
model = QuerySet(Model).filter(id=self.data.get("model_id")).first()
if model is None:
raise AppApiException(500, _("Model does not exist"))
if model.model_type != ModelTypeConst.LLM.name:
raise AppApiException(400, _("Model does not exists or is not an LLM model"))
# 共享/系统级模型:workspace_id 为字符串 "None",不校验传入的工作空间;否则必须属于传入的工作空间
if (
model.workspace_id != "None"
and self.data.get("workspace_id")
and self.data.get("workspace_id") != model.workspace_id
):
raise AppApiException(403, _("Model is not visible to the workspace"))
return model

def _get_model_instance(self, model):
model_id = self.data.get("model_id")
model_params_setting = self.data.get("model_params_setting") or {}
if model.workspace_id == "None":
default_model_params = get_model_default_params(model)
model_params = reset_model_params(default_model_params, **model_params_setting)
return ModelManage.get_model(model_id, lambda _id: get_model(model, **model_params))
return get_model_instance_by_model_workspace_id(
model_id=model_id, workspace_id=model.workspace_id, **model_params_setting
)

def chat_completion(self):
model = self.is_valid(raise_exception=True)
messages = convert_to_messages([dict(m) for m in self.data.get("messages")])
tools = [dict(t) for t in self.data.get("tools") or []]

def event(payload: dict) -> str:
return "data: " + json.dumps(payload, ensure_ascii=False) + "\n\n"

def process():
try:
model_instance = self._get_model_instance(model)
llm = model_instance.bind_tools(tools) if tools else model_instance
content = ""
reasoning = ""
finish_reason = None
usage = None
# 按流式 index 聚合工具调用:id/name 只在首片出现,arguments 逐片拼接
tool_calls_by_index: dict = {}
for chunk in llm.stream(messages):
text = chunk.content if isinstance(chunk.content, str) else ""
if text:
content += text
yield event({"type": "delta", "content": text})
reasoning_delta = (chunk.additional_kwargs or {}).get("reasoning_content")
if reasoning_delta:
reasoning += reasoning_delta
yield event({"type": "reasoning", "content": reasoning_delta})
for tool_chunk in getattr(chunk, "tool_call_chunks", None) or []:
index = tool_chunk.get("index") or 0
call = tool_calls_by_index.setdefault(index, {"id": "", "name": "", "arguments": ""})
if tool_chunk.get("id"):
call["id"] = tool_chunk["id"]
if tool_chunk.get("name"):
call["name"] = call["name"] or tool_chunk["name"]
args_delta = tool_chunk.get("args") or ""
call["arguments"] += args_delta
if call["id"]:
yield event(
{"type": "tool_call", "id": call["id"], "name": call["name"], "arguments": args_delta}
)
metadata = chunk.response_metadata or {}
if metadata.get("finish_reason"):
finish_reason = metadata["finish_reason"]
if getattr(chunk, "usage_metadata", None):
usage = dict(chunk.usage_metadata)
tool_calls = [
{
"id": call["id"],
"type": "function",
"function": {"name": call["name"], "arguments": call["arguments"] or "{}"},
}
for _index, call in sorted(tool_calls_by_index.items())
if call["id"] and call["name"]
]
yield event(
{
"type": "completion",
"content": content,
"reasoningKey": "reasoning_content" if reasoning else None,
"reasoningContent": reasoning or None,
"toolCalls": tool_calls,
"finishReason": finish_reason or ("tool_calls" if tool_calls else "stop"),
"usage": usage,
}
)
except Exception as e:
maxkb_logger.error(f"chat completion error: {e}", exc_info=True)
yield event({"type": "error", "message": str(e)})
yield "data: [DONE]\n\n"

return to_stream_response_simple(process())
1 change: 1 addition & 0 deletions apps/models_provider/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
path('workspace/<str:workspace_id>/model_list', views.ModelList.as_view()),
path('workspace/<str:workspace_id>/model/<str:model_id>/model_params_form', views.ModelSetting.ModelParamsForm.as_view()),
path('workspace/<str:workspace_id>/model/<str:model_id>/prompt_generate', views.ModelPromptGenerateView.as_view()),
path('workspace/<str:workspace_id>/model/<str:model_id>/chat_completion', views.ModelChatCompletionView.as_view()),
path('workspace/<str:workspace_id>/model/<str:model_id>', views.ModelSetting.Operate.as_view()),
path('workspace/<str:workspace_id>/model/<str:model_id>/pause_download', views.ModelSetting.PauseDownload.as_view()),
path('workspace/<str:workspace_id>/model/<str:model_id>/meta', views.ModelSetting.ModelMeta.as_view()),
Expand Down
28 changes: 28 additions & 0 deletions apps/models_provider/views/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from models_provider.api.model import DefaultModelResponse, GetModelApi, ModelCreateAPI, ModelEditApi, ModelListResponse
from models_provider.api.chat_completion import ChatCompletionAPI
from models_provider.api.prompt_generate import PromptGenerateAPI
from models_provider.api.provide import ProvideApi
from models_provider.models import Model
from models_provider.serializers.chat_completion_serializers import ChatCompletionSerializers
from models_provider.serializers.model_serializer import ModelSerializer, WorkspaceSharedModelSerializer
from models_provider.serializers.prompt_generate_serializers import PromptGenerateSerializers
from rest_framework.request import Request
Expand Down Expand Up @@ -413,3 +415,29 @@ def post(self, request: Request, workspace_id: str, model_id: str):
return PromptGenerateSerializers(
data={**request.data, "workspace_id": workspace_id, "model_id": model_id}
).generate_prompt()


class ModelChatCompletionView(APIView):
authentication_classes = [TokenAuth]

@extend_schema(
methods=["POST"],
summary=_("Chat completion"),
description=_("Stream chat completion with function calling"),
operation_id=_("Chat completion"), # type: ignore
parameters=ChatCompletionAPI.get_parameters(),
request=ChatCompletionAPI.get_request(),
responses=None,
tags=[_("Model")], # type: ignore
)
@has_permissions(
PermissionConstants.MODEL_READ.get_workspace_model_permission(),
PermissionConstants.MODEL_READ.get_workspace_permission_workspace_manage_role(),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(),
RoleConstants.USER.get_workspace_role(),
)
def post(self, request: Request, workspace_id: str, model_id: str):
# agent 循环每轮都会调用,不记操作日志
return ChatCompletionSerializers(
data={**request.data, "workspace_id": workspace_id, "model_id": model_id}
).chat_completion()
13 changes: 12 additions & 1 deletion ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ Action、Drawer 或 Dialog。复用方直接使用 `typeof XxxApi` 约束完整
智能体、工具、知识库 Workflow API 分别维护 application_version、tool_version、knowledge_version,
使用同名 getWorkflowVersions/putWorkflowVersion,类型为 WorkflowVersion/WorkflowVersionPayload。
前端版本标题上限 64、更新说明上限 1000,提交字段为 name、publish_desc,
不将资源 desc 当作版本说明。智能体后端已支持 publish_desc;工具和知识库版本当前只处理 name。
不将资源 desc 当作版本说明。智能体和工具后端已支持 publish_desc;知识库版本当前只处理 name。
智能体与工具的 putXxxPublish 提交 publish_name、publish_desc,由发布弹框填写。
三个版本列表均未返回历史默认模型配置,预览与恢复保留当前配置。

`admin/store.ts` 的 getStoreApplicationList、getStoreToolWorkflowList、getStoreKnowledgeList 查询三类模板,
Expand Down Expand Up @@ -269,6 +270,16 @@ System 接口不依赖工作空间 ID。
`useWorkflowStore(resourceScope).postPromptGenerate` 调用;三个范围的 Store 适配器分别引用对应的
Model API,直接转发生成请求,不使用查询缓存或请求去重。

### 流式补全

Workspace、System 资源管理和 System 共享资源均在对应范围的 `model.ts` 提供
`postChatCompletion(modelId, payload, signal)`,请求结构为 `ChatCompletionPayload`(OpenAI 格式的 messages 与 tools)。
路径为 `/workspace/<workspaceId>/model/<modelId>/chat_completion`、`/system/resource/model/<modelId>/chat_completion` 与
`/system/shared/model/<modelId>/chat_completion`;当前后端仅实现 Workspace 接口,System 接口需部署提供对应扩展。
均返回原始 SSE Response,事件为 `delta`、`reasoning`、`tool_call`、`completion` 与 `error`;`signal` 用于中止在途请求,
`postStream` 的第四个可选参数透传给 `fetch`。工作流 AI 生成通过 `useWorkflowStore(resourceScope).postChatCompletion` 调用,
Store 直接转发,不缓存或去重。

### 知识库维护

Workspace、System 资源管理、System 共享各自的 knowledge.ts 保留真实能力,Workspace 共享只查询。
Expand Down
3 changes: 2 additions & 1 deletion ui/src/api/admin/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export async function downloadRequest(url: string, method: string, data?: unknow
}

/** 发送 POST 请求并返回可逐块读取的原始响应。 */
export function postStream(base: string, path: string, data?: unknown): Promise<Response> {
export function postStream(base: string, path: string, data?: unknown, signal?: AbortSignal): Promise<Response> {
const { auth, user } = useStore()
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (auth.token) {
Expand All @@ -205,6 +205,7 @@ export function postStream(base: string, path: string, data?: unknown): Promise<
method: 'POST',
headers,
body: data === undefined ? undefined : JSON.stringify(data),
signal,
})
}

Expand Down
21 changes: 18 additions & 3 deletions ui/src/api/admin/system/resource-management/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { del, get, post, postStream, put } from '../../core/request'
import type { ParamsPage, ResponsePage } from '../../core/types'
import type { Dict, DynamicFormField, ModelItem, ModelPayload, PromptGeneratePayload } from '@/api/types'
import type { Dict, DynamicFormField, ModelItem, ModelPayload, ChatCompletionPayload, PromptGeneratePayload } from '@/api/types'
import { ADMIN_API_BASE_PATH } from '@/api/constants'

const prefix = '/system/resource/model'
Expand Down Expand Up @@ -35,10 +35,25 @@ const getModelParamsForm = (modelId: string) => get<DynamicFormField[]>(`${prefi
const putModelParamsForm = (modelId: string, payload: DynamicFormField[]) =>
put<DynamicFormField[], boolean>(`${prefix}/${modelId}/model_params_form`, payload)


/** 使用指定模型流式生成或优化系统提示词。 */
const postPromptGenerate = (modelId: string, payload: PromptGeneratePayload) => {
return postStream(ADMIN_API_BASE_PATH, `/system/resource/model/${modelId}/prompt_generate`, { ...payload })
}

export default { getModelPage, getModelListWithShared, getModelDetail, postModel, putModel, deleteModel, getModelParamsForm, putModelParamsForm, postPromptGenerate }
/** 流式补全(支持 function calling),返回原始 SSE Response;signal 用于中止在途请求。 */
const postChatCompletion = (modelId: string, payload: ChatCompletionPayload, signal?: AbortSignal) => {
return postStream(ADMIN_API_BASE_PATH, `/system/resource/model/${modelId}/chat_completion`, { ...payload }, signal)
}

export default {
getModelPage,
getModelListWithShared,
getModelDetail,
postModel,
putModel,
deleteModel,
getModelParamsForm,
putModelParamsForm,
postPromptGenerate,
postChatCompletion,
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ const putToolWorkflow = (toolId: string, payload: ToolWorkflowPayload) => {
return put<ToolWorkflowPayload, ToolWorkflowDetail>(`${prefix}/${toolId}/workflow`, payload)
}

/** 发布工具工作流。 */
const putToolWorkflowPublish = (toolId: string) => {
return put<undefined, boolean>(`${prefix}/${toolId}/publish`)
/** 发布工具工作流,并记录版本标题和更新说明。 */
const putToolWorkflowPublish = (toolId: string, publishName: string, publishDesc?: string) => {
return put<Record<string, unknown>, boolean>(`${prefix}/${toolId}/publish`, {
publish_name: publishName,
publish_desc: publishDesc,
})
}

/** 调试已保存的工具工作流,返回 SSE 响应。 */
Expand Down
8 changes: 7 additions & 1 deletion ui/src/api/admin/system/shared-resources/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { del, get, post, postStream, put } from '../../core/request'
import type { Dict, DynamicFormField, ModelItem, ModelPayload, PromptGeneratePayload } from '@/api/types'
import type { Dict, DynamicFormField, ModelItem, ModelPayload, ChatCompletionPayload, PromptGeneratePayload } from '@/api/types'
import { ADMIN_API_BASE_PATH } from '@/api/constants'

const prefix = '/system/shared/model'
Expand Down Expand Up @@ -61,6 +61,11 @@ const postPromptGenerate = (modelId: string, payload: PromptGeneratePayload) =>
return postStream(ADMIN_API_BASE_PATH, `/system/shared/model/${modelId}/prompt_generate`, { ...payload })
}

/** 流式补全(支持 function calling),返回原始 SSE Response;signal 用于中止在途请求。 */
const postChatCompletion = (modelId: string, payload: ChatCompletionPayload, signal?: AbortSignal) => {
return postStream(ADMIN_API_BASE_PATH, `/system/shared/model/${modelId}/chat_completion`, { ...payload }, signal)
}

export default {
deleteModel,
getModelDetail,
Expand All @@ -70,6 +75,7 @@ export default {
getModelParamsForm,
postModel,
postPromptGenerate,
postChatCompletion,
putModel,
putModelParamsForm,
putPauseModelDownload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ const putToolWorkflow = (toolId: string, payload: ToolWorkflowPayload) => {
return put<ToolWorkflowPayload, ToolWorkflowDetail>(`${prefix}/${toolId}/workflow`, payload)
}

/** 发布工具工作流。 */
const putToolWorkflowPublish = (toolId: string) => {
return put<undefined, boolean>(`${prefix}/${toolId}/publish`)
/** 发布工具工作流,并记录版本标题和更新说明。 */
const putToolWorkflowPublish = (toolId: string, publishName: string, publishDesc?: string) => {
return put<Record<string, unknown>, boolean>(`${prefix}/${toolId}/publish`, {
publish_name: publishName,
publish_desc: publishDesc,
})
}

/** 调试已保存的工具工作流,返回 SSE 响应。 */
Expand Down
8 changes: 7 additions & 1 deletion ui/src/api/admin/workspace/model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { del, get, post, postStream, put } from '../core/request'
import type { Dict, DynamicFormField, ModelPayload, ModelItem } from '@/api/types'
import { getWorkspaceId } from '@/utils/resource-context'
import type { PromptGeneratePayload } from '@/api/types'
import type { ChatCompletionPayload, PromptGeneratePayload } from '@/api/types'
import { ADMIN_API_BASE_PATH } from '@/api/constants'

const getPrefix = () => {
Expand Down Expand Up @@ -72,13 +72,19 @@ const postPromptGenerate = (modelId: string, payload: PromptGeneratePayload) =>
})
}

/** 流式补全(支持 function calling),返回原始 SSE Response;signal 用于中止在途请求。 */
const postChatCompletion = (modelId: string, payload: ChatCompletionPayload, signal?: AbortSignal) => {
return postStream(ADMIN_API_BASE_PATH, `/workspace/${getWorkspaceId()}/model/${modelId}/chat_completion`, { ...payload }, signal)
}

export default {
deleteModel,
getModelDetail,
getModelList,
getModelListWithShared,
getModelMeta,
getModelParamsForm,
postChatCompletion,
postModel,
postPromptGenerate,
putModel,
Expand Down
Loading
Loading