| New file |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | export const getAiConfigSummary = async (promptCode = "home.default") => { |
| | | const res = await request.get("ai/config/summary", { params: { promptCode } }); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "获取 AI 运行态摘要失败"); |
| | | }; |
| | | |
| | | export const validateAiParamDraft = async (payload) => { |
| | | const res = await request.post("aiParam/validate-draft", payload); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "AI 参数验证失败"); |
| | | }; |
| | | |
| | | export const renderAiPromptPreview = async (payload) => { |
| | | const res = await request.post("aiPrompt/render-preview", payload); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "Prompt 预览失败"); |
| | | }; |
| | |
| | | throw new Error(msg || "连通性测试失败"); |
| | | }; |
| | | |
| | | export const validateDraftMcpConnectivity = async (payload) => { |
| | | const res = await request.post("aiMcpMount/connectivity/validate-draft", payload); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "草稿连通性测试失败"); |
| | | }; |
| | | |
| | | export const testMcpTool = async (mountId, payload) => { |
| | | const res = await request.post(`aiMcpMount/${mountId}/tool/test`, payload); |
| | | const { code, msg, data } = res.data; |
| | |
| | | import React from "react"; |
| | | import React, { useState } from "react"; |
| | | import { |
| | | FormDataConsumer, |
| | | NumberInput, |
| | | SelectInput, |
| | | TextInput, |
| | | useNotify, |
| | | } from "react-admin"; |
| | | import { Grid, Typography } from "@mui/material"; |
| | | import { Alert, Button, Grid, Stack, Typography } from "@mui/material"; |
| | | import StatusSelectInput from "@/page/components/StatusSelectInput"; |
| | | import { validateDraftMcpConnectivity } from "@/api/ai/mcpMount"; |
| | | |
| | | const transportChoices = [ |
| | | { id: "SSE_HTTP", name: "SSE_HTTP" }, |
| | | { id: "STDIO", name: "STDIO" }, |
| | | { id: "BUILTIN", name: "BUILTIN" }, |
| | | ]; |
| | | |
| | | const AiMcpDraftTestSection = ({ formData, readOnly }) => { |
| | | const notify = useNotify(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [result, setResult] = useState(null); |
| | | |
| | | const handleValidate = async () => { |
| | | setLoading(true); |
| | | try { |
| | | const data = await validateDraftMcpConnectivity(formData); |
| | | setResult(data); |
| | | notify(data?.message || "草稿连通性测试完成"); |
| | | } catch (error) { |
| | | const nextResult = { |
| | | healthStatus: "UNHEALTHY", |
| | | message: error?.message || "草稿连通性测试失败", |
| | | }; |
| | | setResult(nextResult); |
| | | notify(nextResult.message, { type: "error" }); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | if (readOnly) { |
| | | return null; |
| | | } |
| | | |
| | | return ( |
| | | <> |
| | | <Grid item xs={12}> |
| | | <Stack direction="row" spacing={1} alignItems="center"> |
| | | <Button variant="outlined" onClick={handleValidate} disabled={loading}> |
| | | {loading ? "测试中..." : "保存前测试"} |
| | | </Button> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 用当前草稿配置直接校验连通性,不会落库。 |
| | | </Typography> |
| | | </Stack> |
| | | </Grid> |
| | | {result && ( |
| | | <Grid item xs={12}> |
| | | <Alert severity={result.healthStatus === "HEALTHY" ? "success" : "error"}> |
| | | {result.message} |
| | | {result.initElapsedMs ? ` · ${result.initElapsedMs} ms` : ""} |
| | | {result.testedAt ? ` · ${result.testedAt}` : ""} |
| | | </Alert> |
| | | </Grid> |
| | | )} |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | const AiMcpMountForm = ({ readOnly = false }) => ( |
| | | <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}> |
| | |
| | | <Grid item xs={12}> |
| | | <TextInput source="memo" label="备注" fullWidth multiline minRows={3} disabled={readOnly} /> |
| | | </Grid> |
| | | <FormDataConsumer> |
| | | {({ formData }) => <AiMcpDraftTestSection formData={formData} readOnly={readOnly} />} |
| | | </FormDataConsumer> |
| | | <Grid item xs={12}> |
| | | <Typography variant="h6">运行态信息</Typography> |
| | | </Grid> |
| | |
| | | <Grid item xs={12}> |
| | | <TextInput source="lastTestMessage" label="最近测试结果" fullWidth multiline minRows={3} disabled /> |
| | | </Grid> |
| | | <Grid item xs={12} md={6}> |
| | | <TextInput source="updateBy" label="最近更新人" fullWidth disabled /> |
| | | </Grid> |
| | | <Grid item xs={12} md={6}> |
| | | <TextInput source="updateTime$" label="最近更新时间" fullWidth disabled /> |
| | | </Grid> |
| | | </Grid> |
| | | ); |
| | | |
| | |
| | | import AiConfigDialog from "../aiShared/AiConfigDialog"; |
| | | import AiMcpMountToolsPanel from "./AiMcpMountToolsPanel"; |
| | | import { testMcpConnectivity } from "@/api/ai/mcpMount"; |
| | | import AiRuntimeSummary from "../aiShared/AiRuntimeSummary"; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | |
| | | </TopToolbar> |
| | | )} |
| | | > |
| | | <AiRuntimeSummary /> |
| | | <AiMcpMountCards |
| | | onView={(id) => openDialog("show", id)} |
| | | onEdit={(id) => openDialog("edit", id)} |
| | |
| | | import React from "react"; |
| | | import React, { useState } from "react"; |
| | | import { |
| | | BooleanInput, |
| | | FormDataConsumer, |
| | | NumberInput, |
| | | SelectInput, |
| | | TextInput, |
| | | useNotify, |
| | | } from "react-admin"; |
| | | import { Grid, Typography } from "@mui/material"; |
| | | import { Alert, Button, Grid, Stack, Typography } from "@mui/material"; |
| | | import StatusSelectInput from "@/page/components/StatusSelectInput"; |
| | | import { validateAiParamDraft } from "@/api/ai/configCenter"; |
| | | |
| | | const providerChoices = [ |
| | | { id: "OPENAI_COMPATIBLE", name: "OPENAI_COMPATIBLE" }, |
| | | ]; |
| | | |
| | | const AiParamValidateSection = ({ formData, readOnly }) => { |
| | | const notify = useNotify(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [result, setResult] = useState(null); |
| | | |
| | | const handleValidate = async () => { |
| | | setLoading(true); |
| | | try { |
| | | const data = await validateAiParamDraft(formData); |
| | | setResult(data); |
| | | notify(data?.message || "AI 参数验证成功"); |
| | | } catch (error) { |
| | | const nextResult = { |
| | | status: "INVALID", |
| | | message: error?.message || "AI 参数验证失败", |
| | | }; |
| | | setResult(nextResult); |
| | | notify(nextResult.message, { type: "error" }); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | {!readOnly && ( |
| | | <Grid item xs={12}> |
| | | <Stack direction="row" spacing={1} alignItems="center"> |
| | | <Button variant="outlined" onClick={handleValidate} disabled={loading}> |
| | | {loading ? "验证中..." : "保存前验证"} |
| | | </Button> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 会直接校验当前 Base URL、API Key 与模型是否可调用。 |
| | | </Typography> |
| | | </Stack> |
| | | </Grid> |
| | | )} |
| | | {result && ( |
| | | <Grid item xs={12}> |
| | | <Alert severity={result.status === "VALID" ? "success" : "error"}> |
| | | {result.message} |
| | | {result.elapsedMs ? ` · ${result.elapsedMs} ms` : ""} |
| | | {result.validatedAt ? ` · ${result.validatedAt}` : ""} |
| | | </Alert> |
| | | </Grid> |
| | | )} |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | const AiParamForm = ({ readOnly = false }) => ( |
| | | <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}> |
| | |
| | | <Grid item xs={12}> |
| | | <TextInput source="memo" label="备注" fullWidth multiline minRows={3} disabled={readOnly} /> |
| | | </Grid> |
| | | <FormDataConsumer> |
| | | {({ formData }) => <AiParamValidateSection formData={formData} readOnly={readOnly} />} |
| | | </FormDataConsumer> |
| | | <Grid item xs={12}> |
| | | <Typography variant="h6">运行与审计信息</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <TextInput source="validateStatus" label="最近校验状态" fullWidth disabled /> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <TextInput source="lastValidateElapsedMs" label="最近校验耗时(ms)" fullWidth disabled /> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <TextInput source="lastValidateTime$" label="最近校验时间" fullWidth disabled /> |
| | | </Grid> |
| | | <Grid item xs={12} md={3}> |
| | | <TextInput source="updateBy" label="最近更新人" fullWidth disabled /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextInput source="lastValidateMessage" label="最近校验结果" fullWidth multiline minRows={3} disabled /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextInput source="updateTime$" label="最近更新时间" fullWidth disabled /> |
| | | </Grid> |
| | | </Grid> |
| | | ); |
| | | |
| | |
| | | import MyExportButton from "@/page/components/MyExportButton"; |
| | | import AiParamForm from "./AiParamForm"; |
| | | import AiConfigDialog from "../aiShared/AiConfigDialog"; |
| | | import AiRuntimeSummary from "../aiShared/AiRuntimeSummary"; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | |
| | | </TopToolbar> |
| | | )} |
| | | > |
| | | <AiRuntimeSummary /> |
| | | <AiParamCards |
| | | onView={(id) => openDialog("show", id)} |
| | | onEdit={(id) => openDialog("edit", id)} |
| | |
| | | import React from "react"; |
| | | import React, { useState } from "react"; |
| | | import { |
| | | FormDataConsumer, |
| | | TextInput, |
| | | useNotify, |
| | | } from "react-admin"; |
| | | import { Grid, Typography } from "@mui/material"; |
| | | import { Alert, Button, Grid, Stack, TextField, Typography } from "@mui/material"; |
| | | import StatusSelectInput from "@/page/components/StatusSelectInput"; |
| | | import { renderAiPromptPreview } from "@/api/ai/configCenter"; |
| | | |
| | | const AiPromptPreviewSection = ({ formData }) => { |
| | | const notify = useNotify(); |
| | | const [input, setInput] = useState("请帮我总结当前页面能做什么"); |
| | | const [metadataText, setMetadataText] = useState("{\"path\":\"/system/aiPrompt\"}"); |
| | | const [preview, setPreview] = useState(null); |
| | | const [loading, setLoading] = useState(false); |
| | | |
| | | const handlePreview = async () => { |
| | | setLoading(true); |
| | | try { |
| | | const metadata = metadataText ? JSON.parse(metadataText) : {}; |
| | | const data = await renderAiPromptPreview({ |
| | | ...formData, |
| | | input, |
| | | metadata, |
| | | }); |
| | | setPreview(data); |
| | | notify("Prompt 预览完成"); |
| | | } catch (error) { |
| | | setPreview(null); |
| | | notify(error?.message || "Prompt 预览失败", { type: "error" }); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | <Grid item xs={12}> |
| | | <Typography variant="h6">Prompt 预览</Typography> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="示例输入" |
| | | value={input} |
| | | onChange={(event) => setInput(event.target.value)} |
| | | fullWidth |
| | | multiline |
| | | minRows={3} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="示例元数据 JSON" |
| | | value={metadataText} |
| | | onChange={(event) => setMetadataText(event.target.value)} |
| | | fullWidth |
| | | multiline |
| | | minRows={3} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <Stack direction="row" spacing={1} alignItems="center"> |
| | | <Button variant="outlined" onClick={handlePreview} disabled={loading}> |
| | | {loading ? "预览中..." : "预览渲染"} |
| | | </Button> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 用当前表单内容渲染 System Prompt 和 User Prompt。 |
| | | </Typography> |
| | | </Stack> |
| | | </Grid> |
| | | {preview && ( |
| | | <> |
| | | <Grid item xs={12}> |
| | | <Alert severity="success"> |
| | | 已解析变量:{(preview.resolvedVariables || []).join(", ") || "无"} |
| | | </Alert> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="渲染后的 System Prompt" |
| | | value={preview.renderedSystemPrompt || ""} |
| | | fullWidth |
| | | multiline |
| | | minRows={5} |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="渲染后的 User Prompt" |
| | | value={preview.renderedUserPrompt || ""} |
| | | fullWidth |
| | | multiline |
| | | minRows={5} |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | </> |
| | | )} |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | const AiPromptForm = ({ readOnly = false }) => ( |
| | | <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}> |
| | |
| | | <Grid item xs={12}> |
| | | <TextInput source="memo" label="备注" fullWidth multiline minRows={3} disabled={readOnly} /> |
| | | </Grid> |
| | | <FormDataConsumer> |
| | | {({ formData }) => <AiPromptPreviewSection formData={formData} />} |
| | | </FormDataConsumer> |
| | | <Grid item xs={12}> |
| | | <Typography variant="h6">运行与审计信息</Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={6}> |
| | | <TextInput source="updateBy" label="最近更新人" fullWidth disabled /> |
| | | </Grid> |
| | | <Grid item xs={12} md={6}> |
| | | <TextInput source="updateTime$" label="最近更新时间" fullWidth disabled /> |
| | | </Grid> |
| | | </Grid> |
| | | ); |
| | | |
| | |
| | | import MyExportButton from "@/page/components/MyExportButton"; |
| | | import AiPromptForm from "./AiPromptForm"; |
| | | import AiConfigDialog from "../aiShared/AiConfigDialog"; |
| | | import AiRuntimeSummary from "../aiShared/AiRuntimeSummary"; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | |
| | | </TopToolbar> |
| | | )} |
| | | > |
| | | <AiRuntimeSummary /> |
| | | <AiPromptCards |
| | | onView={(id) => openDialog("show", id)} |
| | | onEdit={(id) => openDialog("edit", id)} |
| New file |
| | |
| | | import React, { useEffect, useState } from "react"; |
| | | import { Alert, Box, Card, CardContent, Chip, CircularProgress, Grid, Stack, Typography } from "@mui/material"; |
| | | import { getAiConfigSummary } from "@/api/ai/configCenter"; |
| | | |
| | | const AiRuntimeSummary = ({ promptCode = "home.default" }) => { |
| | | const [summary, setSummary] = useState(null); |
| | | const [loading, setLoading] = useState(true); |
| | | const [error, setError] = useState(""); |
| | | |
| | | useEffect(() => { |
| | | let active = true; |
| | | setLoading(true); |
| | | setError(""); |
| | | getAiConfigSummary(promptCode) |
| | | .then((data) => { |
| | | if (!active) { |
| | | return; |
| | | } |
| | | setSummary(data); |
| | | }) |
| | | .catch((err) => { |
| | | if (!active) { |
| | | return; |
| | | } |
| | | setError(err?.message || "获取运行态摘要失败"); |
| | | }) |
| | | .finally(() => { |
| | | if (active) { |
| | | setLoading(false); |
| | | } |
| | | }); |
| | | return () => { |
| | | active = false; |
| | | }; |
| | | }, [promptCode]); |
| | | |
| | | return ( |
| | | <Box px={2} pt={2}> |
| | | <Card |
| | | variant="outlined" |
| | | sx={{ |
| | | borderRadius: 3, |
| | | boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)", |
| | | }} |
| | | > |
| | | <CardContent> |
| | | <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}> |
| | | <Box> |
| | | <Typography variant="h6">当前运行态</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 展示当前生效的模型、Prompt 与 MCP 挂载信息。 |
| | | </Typography> |
| | | </Box> |
| | | {loading && <CircularProgress size={24} />} |
| | | </Stack> |
| | | {error && <Alert severity="error">{error}</Alert>} |
| | | {!loading && !error && summary && ( |
| | | <Grid container spacing={2}> |
| | | <Grid item xs={12} md={4}> |
| | | <Typography variant="caption" color="text.secondary">当前模型</Typography> |
| | | <Typography variant="body1">{summary.activeModel || "--"}</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {summary.activeParamName || "--"} |
| | | </Typography> |
| | | <Stack direction="row" spacing={1} mt={1} flexWrap="wrap" useFlexGap> |
| | | <Chip size="small" label={`校验 ${summary.activeParamValidateStatus || "--"}`} /> |
| | | <Chip size="small" variant="outlined" label={summary.activeParamValidatedAt || "未校验"} /> |
| | | </Stack> |
| | | </Grid> |
| | | <Grid item xs={12} md={4}> |
| | | <Typography variant="caption" color="text.secondary">当前 Prompt</Typography> |
| | | <Typography variant="body1">{summary.promptName || "--"}</Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {summary.promptCode || "--"} / {summary.promptScene || "--"} |
| | | </Typography> |
| | | <Typography variant="body2" color="text.secondary" mt={1}> |
| | | 最近更新:{summary.activePromptUpdatedAt || "--"} / {summary.activePromptUpdatedBy || "--"} |
| | | </Typography> |
| | | </Grid> |
| | | <Grid item xs={12} md={4}> |
| | | <Typography variant="caption" color="text.secondary">已启用 MCP</Typography> |
| | | <Typography variant="body1">{summary.enabledMcpCount ?? 0} 个</Typography> |
| | | <Stack direction="row" spacing={1} mt={1} flexWrap="wrap" useFlexGap> |
| | | {(summary.enabledMcpNames || []).map((name) => ( |
| | | <Chip key={name} size="small" variant="outlined" label={name} /> |
| | | ))} |
| | | </Stack> |
| | | </Grid> |
| | | {summary.activeParamValidateMessage && ( |
| | | <Grid item xs={12}> |
| | | <Alert severity={summary.activeParamValidateStatus === "VALID" ? "success" : "warning"}> |
| | | {summary.activeParamValidateMessage} |
| | | </Alert> |
| | | </Grid> |
| | | )} |
| | | </Grid> |
| | | )} |
| | | </CardContent> |
| | | </Card> |
| | | </Box> |
| | | ); |
| | | }; |
| | | |
| | | export default AiRuntimeSummary; |
| | |
| | | public static final String MCP_HEALTH_NOT_TESTED = "NOT_TESTED"; |
| | | public static final String MCP_HEALTH_HEALTHY = "HEALTHY"; |
| | | public static final String MCP_HEALTH_UNHEALTHY = "UNHEALTHY"; |
| | | public static final String PARAM_VALIDATE_NOT_TESTED = "NOT_TESTED"; |
| | | public static final String PARAM_VALIDATE_VALID = "VALID"; |
| | | public static final String PARAM_VALIDATE_INVALID = "INVALID"; |
| | | public static final long SSE_TIMEOUT_MS = 0L; |
| | | public static final int DEFAULT_TIMEOUT_MS = 60000; |
| | | public static final double DEFAULT_TEMPERATURE = 0.7D; |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.controller; |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest; |
| | | import com.vincent.rsf.server.ai.service.AiConfigOpsService; |
| | | import com.vincent.rsf.server.system.controller.BaseController; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | | @RequiredArgsConstructor |
| | | public class AiConfigOpsController extends BaseController { |
| | | |
| | | private final AiConfigOpsService aiConfigOpsService; |
| | | |
| | | @PreAuthorize("hasAnyAuthority('system:aiParam:list','system:aiPrompt:list','system:aiMcpMount:list')") |
| | | @GetMapping("/ai/config/summary") |
| | | public R getSummary(@RequestParam(value = "promptCode", required = false) String promptCode) { |
| | | return R.ok().add(aiConfigOpsService.getSummary(promptCode, getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("hasAnyAuthority('system:aiPrompt:list','system:aiPrompt:save','system:aiPrompt:update')") |
| | | @PostMapping("/ai/config/prompt/render-preview") |
| | | public R renderPromptPreview(@RequestBody AiPromptPreviewRequest request) { |
| | | return R.ok().add(aiConfigOpsService.renderPromptPreview(request, getTenantId())); |
| | | } |
| | | } |
| | |
| | | return R.ok().add(aiMcpMountService.testConnectivity(id, getLoginUserId(), getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("hasAnyAuthority('system:aiMcpMount:save','system:aiMcpMount:update')") |
| | | @PostMapping("/aiMcpMount/connectivity/validate-draft") |
| | | public R testDraftConnectivity(@RequestBody AiMcpMount mount) { |
| | | mount.setTenantId(getTenantId()); |
| | | return R.ok().add(aiMcpMountService.testDraftConnectivity(mount, getLoginUserId(), getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:aiMcpMount:update')") |
| | | @PostMapping("/aiMcpMount/{id}/tool/test") |
| | | public R testTool(@PathVariable("id") Long id, @RequestBody AiMcpToolTestRequest request) { |
| | |
| | | .last("limit 1"))); |
| | | } |
| | | |
| | | @PreAuthorize("hasAnyAuthority('system:aiParam:save','system:aiParam:update')") |
| | | @PostMapping("/aiParam/validate-draft") |
| | | public R validateDraft(@RequestBody AiParam aiParam) { |
| | | aiParam.setTenantId(getTenantId()); |
| | | return R.ok().add(aiParamService.validateDraft(aiParam, getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:aiParam:save')") |
| | | @OperationLog("Create AiParam") |
| | | @PostMapping("/aiParam/save") |
| | |
| | | |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest; |
| | | import com.vincent.rsf.server.ai.entity.AiPrompt; |
| | | import com.vincent.rsf.server.ai.service.AiPromptService; |
| | | import com.vincent.rsf.server.common.annotation.OperationLog; |
| | |
| | | .last("limit 1"))); |
| | | } |
| | | |
| | | @PreAuthorize("hasAnyAuthority('system:aiPrompt:list','system:aiPrompt:save','system:aiPrompt:update')") |
| | | @PostMapping("/aiPrompt/render-preview") |
| | | public R renderPreview(@RequestBody AiPromptPreviewRequest request) { |
| | | return R.ok().add(aiPromptService.renderPreview(request, getTenantId())); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('system:aiPrompt:save')") |
| | | @OperationLog("Create AiPrompt") |
| | | @PostMapping("/aiPrompt/save") |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | @Builder |
| | | public class AiConfigSummaryDto { |
| | | |
| | | private String promptCode; |
| | | |
| | | private String promptName; |
| | | |
| | | private String promptScene; |
| | | |
| | | private String activeParamName; |
| | | |
| | | private String activeModel; |
| | | |
| | | private String activeParamUpdatedAt; |
| | | |
| | | private Long activeParamUpdatedBy; |
| | | |
| | | private String activeParamValidateStatus; |
| | | |
| | | private String activeParamValidateMessage; |
| | | |
| | | private String activeParamValidatedAt; |
| | | |
| | | private Integer enabledMcpCount; |
| | | |
| | | private List<String> enabledMcpNames; |
| | | |
| | | private String activePromptUpdatedAt; |
| | | |
| | | private Long activePromptUpdatedBy; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | |
| | | @Data |
| | | @Builder |
| | | public class AiParamValidateResultDto { |
| | | |
| | | private String status; |
| | | |
| | | private String message; |
| | | |
| | | private String model; |
| | | |
| | | private Long elapsedMs; |
| | | |
| | | private String validatedAt; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | @Builder |
| | | public class AiPromptPreviewDto { |
| | | |
| | | private String renderedSystemPrompt; |
| | | |
| | | private String renderedUserPrompt; |
| | | |
| | | private List<String> resolvedVariables; |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.dto; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.util.Map; |
| | | |
| | | @Data |
| | | public class AiPromptPreviewRequest { |
| | | |
| | | private Long id; |
| | | |
| | | private String name; |
| | | |
| | | private String code; |
| | | |
| | | private String scene; |
| | | |
| | | private String systemPrompt; |
| | | |
| | | private String userPromptTemplate; |
| | | |
| | | private String input; |
| | | |
| | | private Map<String, Object> metadata; |
| | | } |
| | |
| | | @ApiModelProperty(value = "streamingEnabled") |
| | | private Boolean streamingEnabled; |
| | | |
| | | @ApiModelProperty(value = "最近校验状态") |
| | | private String validateStatus; |
| | | |
| | | @ApiModelProperty(value = "最近校验信息") |
| | | private String lastValidateMessage; |
| | | |
| | | @ApiModelProperty(value = "最近校验耗时") |
| | | private Long lastValidateElapsedMs; |
| | | |
| | | @ApiModelProperty(value = "最近校验时间") |
| | | @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date lastValidateTime; |
| | | |
| | | @ApiModelProperty(value = "状态") |
| | | private Integer status; |
| | | |
| | |
| | | } |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime); |
| | | } |
| | | |
| | | public String getLastValidateTime$() { |
| | | if (this.lastValidateTime == null) { |
| | | return ""; |
| | | } |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.lastValidateTime); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.service; |
| | | |
| | | import com.vincent.rsf.server.ai.dto.AiConfigSummaryDto; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest; |
| | | |
| | | public interface AiConfigOpsService { |
| | | |
| | | AiConfigSummaryDto getSummary(String promptCode, Long tenantId); |
| | | |
| | | AiPromptPreviewDto renderPromptPreview(AiPromptPreviewRequest request, Long tenantId); |
| | | } |
| | |
| | | |
| | | AiMcpConnectivityTestDto testConnectivity(Long mountId, Long userId, Long tenantId); |
| | | |
| | | AiMcpConnectivityTestDto testDraftConnectivity(AiMcpMount mount, Long userId, Long tenantId); |
| | | |
| | | AiMcpToolTestDto testTool(Long mountId, Long userId, Long tenantId, AiMcpToolTestRequest request); |
| | | } |
| | |
| | | package com.vincent.rsf.server.ai.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto; |
| | | import com.vincent.rsf.server.ai.entity.AiParam; |
| | | |
| | | public interface AiParamService extends IService<AiParam> { |
| | |
| | | void validateBeforeSave(AiParam aiParam, Long tenantId); |
| | | |
| | | void validateBeforeUpdate(AiParam aiParam, Long tenantId); |
| | | |
| | | AiParamValidateResultDto validateDraft(AiParam aiParam, Long tenantId); |
| | | } |
| | |
| | | package com.vincent.rsf.server.ai.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest; |
| | | import com.vincent.rsf.server.ai.entity.AiPrompt; |
| | | |
| | | public interface AiPromptService extends IService<AiPrompt> { |
| | |
| | | void validateBeforeSave(AiPrompt aiPrompt, Long tenantId); |
| | | |
| | | void validateBeforeUpdate(AiPrompt aiPrompt, Long tenantId); |
| | | |
| | | AiPromptPreviewDto renderPreview(AiPromptPreviewRequest request, Long tenantId); |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.service.impl; |
| | | |
| | | import com.vincent.rsf.server.ai.config.AiDefaults; |
| | | import com.vincent.rsf.server.ai.dto.AiConfigSummaryDto; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest; |
| | | import com.vincent.rsf.server.ai.entity.AiMcpMount; |
| | | import com.vincent.rsf.server.ai.entity.AiParam; |
| | | import com.vincent.rsf.server.ai.entity.AiPrompt; |
| | | import com.vincent.rsf.server.ai.service.AiConfigOpsService; |
| | | import com.vincent.rsf.server.ai.service.AiMcpMountService; |
| | | import com.vincent.rsf.server.ai.service.AiParamService; |
| | | import com.vincent.rsf.server.ai.service.AiPromptService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | | public class AiConfigOpsServiceImpl implements AiConfigOpsService { |
| | | |
| | | private final AiParamService aiParamService; |
| | | private final AiPromptService aiPromptService; |
| | | private final AiMcpMountService aiMcpMountService; |
| | | private final AiPromptRenderSupport aiPromptRenderSupport; |
| | | |
| | | @Override |
| | | public AiConfigSummaryDto getSummary(String promptCode, Long tenantId) { |
| | | String finalPromptCode = StringUtils.hasText(promptCode) ? promptCode : AiDefaults.DEFAULT_PROMPT_CODE; |
| | | AiParam activeParam = aiParamService.getActiveParam(tenantId); |
| | | AiPrompt activePrompt = aiPromptService.getActivePrompt(finalPromptCode, tenantId); |
| | | List<AiMcpMount> mounts = aiMcpMountService.listActiveMounts(tenantId); |
| | | return AiConfigSummaryDto.builder() |
| | | .promptCode(activePrompt.getCode()) |
| | | .promptName(activePrompt.getName()) |
| | | .promptScene(activePrompt.getScene()) |
| | | .activeParamName(activeParam.getName()) |
| | | .activeModel(activeParam.getModel()) |
| | | .activeParamUpdatedAt(activeParam.getUpdateTime$()) |
| | | .activeParamUpdatedBy(activeParam.getUpdateBy()) |
| | | .activeParamValidateStatus(activeParam.getValidateStatus()) |
| | | .activeParamValidateMessage(activeParam.getLastValidateMessage()) |
| | | .activeParamValidatedAt(activeParam.getLastValidateTime$()) |
| | | .enabledMcpCount(mounts.size()) |
| | | .enabledMcpNames(mounts.stream().map(AiMcpMount::getName).toList()) |
| | | .activePromptUpdatedAt(activePrompt.getUpdateTime$()) |
| | | .activePromptUpdatedBy(activePrompt.getUpdateBy()) |
| | | .build(); |
| | | } |
| | | |
| | | @Override |
| | | public AiPromptPreviewDto renderPromptPreview(AiPromptPreviewRequest request, Long tenantId) { |
| | | return aiPromptService.renderPreview(request, tenantId); |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | @Override |
| | | public AiMcpConnectivityTestDto testDraftConnectivity(AiMcpMount mount, Long userId, Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | | if (userId == null) { |
| | | throw new CoolException("当前登录用户不存在"); |
| | | } |
| | | if (mount == null) { |
| | | throw new CoolException("MCP 挂载参数不能为空"); |
| | | } |
| | | mount.setTenantId(tenantId); |
| | | fillDefaults(mount); |
| | | ensureRequiredFields(mount, tenantId); |
| | | long startedAt = System.currentTimeMillis(); |
| | | try (McpMountRuntimeFactory.McpMountRuntime runtime = mcpMountRuntimeFactory.create(List.of(mount), userId)) { |
| | | long elapsedMs = System.currentTimeMillis() - startedAt; |
| | | if (!runtime.getErrors().isEmpty()) { |
| | | return AiMcpConnectivityTestDto.builder() |
| | | .mountId(mount.getId()) |
| | | .mountName(mount.getName()) |
| | | .healthStatus(AiDefaults.MCP_HEALTH_UNHEALTHY) |
| | | .message(String.join(";", runtime.getErrors())) |
| | | .initElapsedMs(elapsedMs) |
| | | .toolCount(runtime.getToolCallbacks().length) |
| | | .testedAt(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) |
| | | .build(); |
| | | } |
| | | return AiMcpConnectivityTestDto.builder() |
| | | .mountId(mount.getId()) |
| | | .mountName(mount.getName()) |
| | | .healthStatus(AiDefaults.MCP_HEALTH_HEALTHY) |
| | | .message("草稿连通性测试成功,解析出 " + runtime.getToolCallbacks().length + " 个工具") |
| | | .initElapsedMs(elapsedMs) |
| | | .toolCount(runtime.getToolCallbacks().length) |
| | | .testedAt(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) |
| | | .build(); |
| | | } catch (CoolException e) { |
| | | throw e; |
| | | } catch (Exception e) { |
| | | throw new CoolException("草稿连通性测试失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public AiMcpToolTestDto testTool(Long mountId, Long userId, Long tenantId, AiMcpToolTestRequest request) { |
| | | if (userId == null) { |
| | | throw new CoolException("当前登录用户不存在"); |
| | |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.ai.config.AiDefaults; |
| | | import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto; |
| | | import com.vincent.rsf.server.ai.entity.AiParam; |
| | | import com.vincent.rsf.server.ai.mapper.AiParamMapper; |
| | | import com.vincent.rsf.server.ai.service.AiParamService; |
| | | import com.vincent.rsf.server.system.enums.StatusType; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | |
| | | @Service("aiParamService") |
| | | @RequiredArgsConstructor |
| | | public class AiParamServiceImpl extends ServiceImpl<AiParamMapper, AiParam> implements AiParamService { |
| | | |
| | | private final AiParamValidationSupport aiParamValidationSupport; |
| | | |
| | | @Override |
| | | public AiParam getActiveParam(Long tenantId) { |
| | |
| | | fillDefaults(aiParam); |
| | | ensureBaseFields(aiParam); |
| | | ensureSingleActive(tenantId, null, aiParam.getStatus()); |
| | | applyValidation(aiParam); |
| | | } |
| | | |
| | | @Override |
| | |
| | | aiParam.setTenantId(current.getTenantId()); |
| | | ensureBaseFields(aiParam); |
| | | ensureSingleActive(tenantId, aiParam.getId(), aiParam.getStatus()); |
| | | applyValidation(aiParam); |
| | | } |
| | | |
| | | @Override |
| | | public AiParamValidateResultDto validateDraft(AiParam aiParam, Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | | fillDefaults(aiParam); |
| | | ensureBaseFields(aiParam); |
| | | return aiParamValidationSupport.validate(aiParam); |
| | | } |
| | | |
| | | private void ensureBaseFields(AiParam aiParam) { |
| | |
| | | if (aiParam.getStreamingEnabled() == null) { |
| | | aiParam.setStreamingEnabled(Boolean.TRUE); |
| | | } |
| | | if (!StringUtils.hasText(aiParam.getValidateStatus())) { |
| | | aiParam.setValidateStatus(AiDefaults.PARAM_VALIDATE_NOT_TESTED); |
| | | } |
| | | if (aiParam.getStatus() == null) { |
| | | aiParam.setStatus(StatusType.ENABLE.val); |
| | | } |
| | | } |
| | | |
| | | private void applyValidation(AiParam aiParam) { |
| | | AiParamValidateResultDto validateResult = aiParamValidationSupport.validate(aiParam); |
| | | aiParam.setValidateStatus(validateResult.getStatus()); |
| | | aiParam.setLastValidateMessage(validateResult.getMessage()); |
| | | aiParam.setLastValidateElapsedMs(validateResult.getElapsedMs()); |
| | | aiParam.setLastValidateTime(parseDate(validateResult.getValidatedAt())); |
| | | if (!AiDefaults.PARAM_VALIDATE_VALID.equals(validateResult.getStatus())) { |
| | | throw new CoolException(validateResult.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private Date parseDate(String dateTime) { |
| | | if (!StringUtils.hasText(dateTime)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateTime); |
| | | } catch (Exception e) { |
| | | throw new CoolException("解析校验时间失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.service.impl; |
| | | |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.ai.config.AiDefaults; |
| | | import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto; |
| | | import com.vincent.rsf.server.ai.entity.AiParam; |
| | | import io.micrometer.observation.ObservationRegistry; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.ai.chat.messages.UserMessage; |
| | | import org.springframework.ai.chat.model.ChatResponse; |
| | | import org.springframework.ai.chat.prompt.Prompt; |
| | | import org.springframework.ai.model.tool.DefaultToolCallingManager; |
| | | import org.springframework.ai.model.tool.ToolCallingManager; |
| | | import org.springframework.ai.openai.OpenAiChatModel; |
| | | import org.springframework.ai.openai.OpenAiChatOptions; |
| | | import org.springframework.ai.openai.api.OpenAiApi; |
| | | import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor; |
| | | import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver; |
| | | import org.springframework.ai.util.json.schema.SchemaType; |
| | | import org.springframework.context.support.GenericApplicationContext; |
| | | import org.springframework.http.client.SimpleClientHttpRequestFactory; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.client.RestClient; |
| | | import org.springframework.web.reactive.function.client.WebClient; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | |
| | | @Component |
| | | @RequiredArgsConstructor |
| | | public class AiParamValidationSupport { |
| | | |
| | | private final GenericApplicationContext applicationContext; |
| | | private final ObservationRegistry observationRegistry; |
| | | |
| | | public AiParamValidateResultDto validate(AiParam aiParam) { |
| | | long startedAt = System.currentTimeMillis(); |
| | | try { |
| | | OpenAiChatModel chatModel = createChatModel(aiParam); |
| | | ChatResponse response = chatModel.call(new Prompt(List.of(new UserMessage("请回复 OK")))); |
| | | if (response == null || response.getResult() == null || response.getResult().getOutput() == null |
| | | || !StringUtils.hasText(response.getResult().getOutput().getText())) { |
| | | throw new CoolException("模型已连接,但未返回有效响应"); |
| | | } |
| | | long elapsedMs = System.currentTimeMillis() - startedAt; |
| | | return AiParamValidateResultDto.builder() |
| | | .status(AiDefaults.PARAM_VALIDATE_VALID) |
| | | .message("模型连通成功") |
| | | .model(aiParam.getModel()) |
| | | .elapsedMs(elapsedMs) |
| | | .validatedAt(formatDate(new Date())) |
| | | .build(); |
| | | } catch (Exception e) { |
| | | long elapsedMs = System.currentTimeMillis() - startedAt; |
| | | String message = e instanceof CoolException ? e.getMessage() : "模型验证失败: " + e.getMessage(); |
| | | return AiParamValidateResultDto.builder() |
| | | .status(AiDefaults.PARAM_VALIDATE_INVALID) |
| | | .message(message) |
| | | .model(aiParam.getModel()) |
| | | .elapsedMs(elapsedMs) |
| | | .validatedAt(formatDate(new Date())) |
| | | .build(); |
| | | } |
| | | } |
| | | |
| | | private OpenAiChatModel createChatModel(AiParam aiParam) { |
| | | OpenAiApi openAiApi = buildOpenAiApi(aiParam); |
| | | ToolCallingManager toolCallingManager = DefaultToolCallingManager.builder() |
| | | .observationRegistry(observationRegistry) |
| | | .toolCallbackResolver(new SpringBeanToolCallbackResolver(applicationContext, SchemaType.OPEN_API_SCHEMA)) |
| | | .toolExecutionExceptionProcessor(new DefaultToolExecutionExceptionProcessor(false)) |
| | | .build(); |
| | | return new OpenAiChatModel( |
| | | openAiApi, |
| | | OpenAiChatOptions.builder() |
| | | .model(aiParam.getModel()) |
| | | .temperature(aiParam.getTemperature()) |
| | | .topP(aiParam.getTopP()) |
| | | .maxTokens(aiParam.getMaxTokens()) |
| | | .streamUsage(true) |
| | | .build(), |
| | | toolCallingManager, |
| | | org.springframework.retry.support.RetryTemplate.builder().maxAttempts(1).build(), |
| | | observationRegistry |
| | | ); |
| | | } |
| | | |
| | | private OpenAiApi buildOpenAiApi(AiParam aiParam) { |
| | | int timeoutMs = aiParam.getTimeoutMs() == null ? AiDefaults.DEFAULT_TIMEOUT_MS : aiParam.getTimeoutMs(); |
| | | SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); |
| | | requestFactory.setConnectTimeout(timeoutMs); |
| | | requestFactory.setReadTimeout(timeoutMs); |
| | | return OpenAiApi.builder() |
| | | .baseUrl(aiParam.getBaseUrl()) |
| | | .apiKey(aiParam.getApiKey()) |
| | | .restClientBuilder(RestClient.builder().requestFactory(requestFactory)) |
| | | .webClientBuilder(WebClient.builder()) |
| | | .build(); |
| | | } |
| | | |
| | | private String formatDate(Date date) { |
| | | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.vincent.rsf.server.ai.service.impl; |
| | | |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | @Component |
| | | public class AiPromptRenderSupport { |
| | | |
| | | private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{\\{?([a-zA-Z0-9_.-]+)}}?"); |
| | | |
| | | public AiPromptPreviewDto render(String systemPrompt, String userPromptTemplate, String input, Map<String, Object> metadata) { |
| | | String finalInput = input == null ? "" : input; |
| | | return AiPromptPreviewDto.builder() |
| | | .renderedSystemPrompt(renderTemplate(systemPrompt, finalInput, metadata)) |
| | | .renderedUserPrompt(renderUserPrompt(userPromptTemplate, finalInput, metadata)) |
| | | .resolvedVariables(resolveVariables(systemPrompt, userPromptTemplate, metadata)) |
| | | .build(); |
| | | } |
| | | |
| | | public String renderUserPrompt(String userPromptTemplate, String input, Map<String, Object> metadata) { |
| | | if (!StringUtils.hasText(userPromptTemplate)) { |
| | | return input; |
| | | } |
| | | String rendered = replaceTemplateVariables(userPromptTemplate, input, metadata); |
| | | if (Objects.equals(rendered, userPromptTemplate)) { |
| | | return userPromptTemplate + "\n\n" + input; |
| | | } |
| | | return rendered; |
| | | } |
| | | |
| | | private String renderTemplate(String template, String input, Map<String, Object> metadata) { |
| | | if (!StringUtils.hasText(template)) { |
| | | return template; |
| | | } |
| | | return replaceTemplateVariables(template, input, metadata); |
| | | } |
| | | |
| | | private String replaceTemplateVariables(String template, String input, Map<String, Object> metadata) { |
| | | String rendered = template |
| | | .replace("{{input}}", input) |
| | | .replace("{input}", input); |
| | | if (metadata == null || metadata.isEmpty()) { |
| | | return rendered; |
| | | } |
| | | for (Map.Entry<String, Object> entry : metadata.entrySet()) { |
| | | String value = entry.getValue() == null ? "" : String.valueOf(entry.getValue()); |
| | | rendered = rendered.replace("{{" + entry.getKey() + "}}", value); |
| | | rendered = rendered.replace("{" + entry.getKey() + "}", value); |
| | | } |
| | | return rendered; |
| | | } |
| | | |
| | | private List<String> resolveVariables(String systemPrompt, String userPromptTemplate, Map<String, Object> metadata) { |
| | | LinkedHashSet<String> variables = new LinkedHashSet<>(); |
| | | collectVariables(variables, systemPrompt); |
| | | collectVariables(variables, userPromptTemplate); |
| | | if (metadata != null && !metadata.isEmpty()) { |
| | | variables.addAll(metadata.keySet()); |
| | | } |
| | | return new ArrayList<>(variables); |
| | | } |
| | | |
| | | private void collectVariables(LinkedHashSet<String> variables, String template) { |
| | | if (!StringUtils.hasText(template)) { |
| | | return; |
| | | } |
| | | Matcher matcher = VARIABLE_PATTERN.matcher(template); |
| | | while (matcher.find()) { |
| | | variables.add(matcher.group(1)); |
| | | } |
| | | } |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewDto; |
| | | import com.vincent.rsf.server.ai.dto.AiPromptPreviewRequest; |
| | | import com.vincent.rsf.server.ai.entity.AiPrompt; |
| | | import com.vincent.rsf.server.ai.mapper.AiPromptMapper; |
| | | import com.vincent.rsf.server.ai.service.AiPromptService; |
| | | import com.vincent.rsf.server.system.enums.StatusType; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | @Service("aiPromptService") |
| | | @RequiredArgsConstructor |
| | | public class AiPromptServiceImpl extends ServiceImpl<AiPromptMapper, AiPrompt> implements AiPromptService { |
| | | |
| | | private final AiPromptRenderSupport aiPromptRenderSupport; |
| | | |
| | | @Override |
| | | public AiPrompt getActivePrompt(String code, Long tenantId) { |
| | |
| | | ensureUniqueCode(aiPrompt.getCode(), tenantId, aiPrompt.getId()); |
| | | } |
| | | |
| | | @Override |
| | | public AiPromptPreviewDto renderPreview(AiPromptPreviewRequest request, Long tenantId) { |
| | | ensureTenantId(tenantId); |
| | | if (request == null) { |
| | | throw new CoolException("Prompt 预览参数不能为空"); |
| | | } |
| | | if (!StringUtils.hasText(request.getSystemPrompt())) { |
| | | throw new CoolException("系统 Prompt 不能为空"); |
| | | } |
| | | return aiPromptRenderSupport.render( |
| | | request.getSystemPrompt(), |
| | | request.getUserPromptTemplate(), |
| | | request.getInput(), |
| | | request.getMetadata() |
| | | ); |
| | | } |
| | | |
| | | private void ensureRequiredFields(AiPrompt aiPrompt) { |
| | | if (!StringUtils.hasText(aiPrompt.getName())) { |
| | | throw new CoolException("Prompt 名称不能为空"); |
| | |
| | | `max_tokens` int(11) DEFAULT NULL COMMENT '最大Token', |
| | | `timeout_ms` int(11) DEFAULT NULL COMMENT '超时时间', |
| | | `streaming_enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用流式响应', |
| | | `validate_status` varchar(32) DEFAULT 'NOT_TESTED' COMMENT '最近校验状态', |
| | | `last_validate_message` varchar(500) DEFAULT NULL COMMENT '最近校验信息', |
| | | `last_validate_elapsed_ms` bigint(20) DEFAULT NULL COMMENT '最近校验耗时', |
| | | `last_validate_time` datetime DEFAULT NULL COMMENT '最近校验时间', |
| | | `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户', |
| | | `status` int(11) DEFAULT '1' COMMENT '状态', |
| | | `deleted` int(11) DEFAULT '0' COMMENT '删除标记', |
| | |
| | | KEY `idx_sys_ai_chat_message_session_seq` (`session_id`,`seq_no`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 对话消息'; |
| | | |
| | | SET @ai_param_validate_status_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_param' |
| | | AND `COLUMN_NAME` = 'validate_status' |
| | | ); |
| | | SET @ai_param_validate_status_sql := IF( |
| | | @ai_param_validate_status_exists = 0, |
| | | 'ALTER TABLE `sys_ai_param` ADD COLUMN `validate_status` varchar(32) DEFAULT ''NOT_TESTED'' COMMENT ''最近校验状态'' AFTER `streaming_enabled`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE ai_param_validate_status_stmt FROM @ai_param_validate_status_sql; |
| | | EXECUTE ai_param_validate_status_stmt; |
| | | DEALLOCATE PREPARE ai_param_validate_status_stmt; |
| | | |
| | | SET @ai_param_last_validate_message_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_param' |
| | | AND `COLUMN_NAME` = 'last_validate_message' |
| | | ); |
| | | SET @ai_param_last_validate_message_sql := IF( |
| | | @ai_param_last_validate_message_exists = 0, |
| | | 'ALTER TABLE `sys_ai_param` ADD COLUMN `last_validate_message` varchar(500) DEFAULT NULL COMMENT ''最近校验信息'' AFTER `validate_status`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE ai_param_last_validate_message_stmt FROM @ai_param_last_validate_message_sql; |
| | | EXECUTE ai_param_last_validate_message_stmt; |
| | | DEALLOCATE PREPARE ai_param_last_validate_message_stmt; |
| | | |
| | | SET @ai_param_last_validate_elapsed_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_param' |
| | | AND `COLUMN_NAME` = 'last_validate_elapsed_ms' |
| | | ); |
| | | SET @ai_param_last_validate_elapsed_sql := IF( |
| | | @ai_param_last_validate_elapsed_exists = 0, |
| | | 'ALTER TABLE `sys_ai_param` ADD COLUMN `last_validate_elapsed_ms` bigint(20) DEFAULT NULL COMMENT ''最近校验耗时'' AFTER `last_validate_message`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE ai_param_last_validate_elapsed_stmt FROM @ai_param_last_validate_elapsed_sql; |
| | | EXECUTE ai_param_last_validate_elapsed_stmt; |
| | | DEALLOCATE PREPARE ai_param_last_validate_elapsed_stmt; |
| | | |
| | | SET @ai_param_last_validate_time_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_param' |
| | | AND `COLUMN_NAME` = 'last_validate_time' |
| | | ); |
| | | SET @ai_param_last_validate_time_sql := IF( |
| | | @ai_param_last_validate_time_exists = 0, |
| | | 'ALTER TABLE `sys_ai_param` ADD COLUMN `last_validate_time` datetime DEFAULT NULL COMMENT ''最近校验时间'' AFTER `last_validate_elapsed_ms`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE ai_param_last_validate_time_stmt FROM @ai_param_last_validate_time_sql; |
| | | EXECUTE ai_param_last_validate_time_stmt; |
| | | DEALLOCATE PREPARE ai_param_last_validate_time_stmt; |
| | | |
| | | UPDATE `sys_ai_param` |
| | | SET `validate_status` = 'NOT_TESTED' |
| | | WHERE `validate_status` IS NULL OR `validate_status` = ''; |
| | | |
| | | SET @builtin_code_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |