feat: add Prompt template library, agent_call inter-agent tool, and RAG memory
- New PromptTemplatePicker component for browsing 13 preset prompt templates - AgentConfig.vue: "Load from library" button for system prompt - Agents.vue: "Create from Prompt template" entry with agent node + RAG memory - seed_prompt_templates.py: 13 preset templates (客服/研发/教育/内容/分析/创意/健康医疗) - agent_call tool: agents can delegate tasks to other agents (19th builtin tool) - Created 全能助手 (general orchestrator) and 家庭医生助手 agents - Switch template-created agents from type:llm to type:agent for full ReAct + RAG Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
323
frontend/src/components/PromptTemplatePicker.vue
Normal file
323
frontend/src/components/PromptTemplatePicker.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="prompt-template-picker">
|
||||
<!-- 搜索与筛选 -->
|
||||
<div class="picker-toolbar">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="搜索模板名称或描述..."
|
||||
clearable
|
||||
style="flex: 1"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="categoryFilter"
|
||||
placeholder="分类筛选"
|
||||
clearable
|
||||
style="width: 160px; margin-left: 10px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="客服" value="customer_service" />
|
||||
<el-option label="研发" value="dev" />
|
||||
<el-option label="教育" value="education" />
|
||||
<el-option label="内容" value="content" />
|
||||
<el-option label="分析" value="analysis" />
|
||||
<el-option label="创意" value="creative" />
|
||||
<el-option label="健康医疗" value="healthcare" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 模板列表 -->
|
||||
<div v-loading="loading" class="template-list">
|
||||
<div v-if="filteredTemplates.length === 0 && !loading" class="empty-state">
|
||||
<el-empty description="暂无匹配的模板" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="tpl in filteredTemplates"
|
||||
:key="tpl.id"
|
||||
class="template-card"
|
||||
:class="{ selected: selectedId === tpl.id }"
|
||||
@click="selectTemplate(tpl)"
|
||||
>
|
||||
<div class="template-header">
|
||||
<span class="template-name">{{ tpl.name }}</span>
|
||||
<el-tag
|
||||
v-if="tpl.is_featured"
|
||||
size="small"
|
||||
type="warning"
|
||||
effect="dark"
|
||||
>
|
||||
推荐
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="template-desc">{{ tpl.description }}</div>
|
||||
<div class="template-meta">
|
||||
<el-tag size="small" type="info">{{ categoryLabel(tpl.category) }}</el-tag>
|
||||
<el-tag
|
||||
v-for="tag in (tpl.tags || [])"
|
||||
:key="tag"
|
||||
size="small"
|
||||
class="tag-item"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<span class="use-count" v-if="tpl.use_count">
|
||||
<el-icon><CaretTop /></el-icon> {{ tpl.use_count }} 次使用
|
||||
</span>
|
||||
</div>
|
||||
<!-- 选中后展开预览 -->
|
||||
<div v-if="selectedId === tpl.id" class="template-preview">
|
||||
<div class="preview-label">提示词预览:</div>
|
||||
<pre class="preview-content">{{ tpl.prompt }}</pre>
|
||||
</div>
|
||||
<div class="template-actions" v-if="selectedId === tpl.id">
|
||||
<el-button type="primary" size="small" @click.stop="handleUse(tpl)">
|
||||
使用此模板
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="picker-pagination" v-if="total > pageSize">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next"
|
||||
small
|
||||
@current-change="loadTemplates"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Search, CaretTop } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface PromptTemplate {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
tags: string[]
|
||||
prompt: string
|
||||
is_featured: boolean
|
||||
is_public: boolean
|
||||
use_count?: number
|
||||
model?: string
|
||||
provider?: string
|
||||
temperature?: string
|
||||
variables?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
defineProps({
|
||||
pickMode: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
use: [tpl: PromptTemplate]
|
||||
select: [tpl: PromptTemplate]
|
||||
}>()
|
||||
|
||||
const templates = ref<PromptTemplate[]>([])
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
const categoryFilter = ref('')
|
||||
const selectedId = ref<string | null>(null)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
const categoryLabel = (cat: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
customer_service: '客服',
|
||||
dev: '研发',
|
||||
education: '教育',
|
||||
content: '内容',
|
||||
analysis: '分析',
|
||||
creative: '创意',
|
||||
healthcare: '健康医疗',
|
||||
}
|
||||
return map[cat] || cat
|
||||
}
|
||||
|
||||
const loadTemplates = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.set('is_public', 'true')
|
||||
params.set('limit', String(pageSize.value))
|
||||
params.set('offset', String((currentPage.value - 1) * pageSize.value))
|
||||
if (searchText.value) params.set('search', searchText.value)
|
||||
if (categoryFilter.value) params.set('category', categoryFilter.value)
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const resp = await fetch(`/api/v1/node-templates?${params.toString()}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data = await resp.json()
|
||||
templates.value = (data.items || data.data || data || []) as PromptTemplate[]
|
||||
total.value = data.total || templates.value.length
|
||||
} catch (e) {
|
||||
console.error('加载 Prompt 模板失败:', e)
|
||||
ElMessage.error('加载模板失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const filteredTemplates = computed(() => templates.value)
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleSearch = () => {
|
||||
if (searchTimer) clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
currentPage.value = 1
|
||||
loadTemplates()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const selectTemplate = (tpl: PromptTemplate) => {
|
||||
selectedId.value = tpl.id
|
||||
emit('select', tpl)
|
||||
}
|
||||
|
||||
const handleUse = (tpl: PromptTemplate) => {
|
||||
fetch(`/api/v1/node-templates/${tpl.id}/use`, { method: 'POST' }).catch(() => {})
|
||||
emit('use', tpl)
|
||||
selectedId.value = null
|
||||
}
|
||||
|
||||
// ── 暴露方法 ──
|
||||
defineExpose({ loadTemplates })
|
||||
|
||||
onMounted(() => {
|
||||
loadTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prompt-template-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.picker-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.template-card.selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.use-count {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.template-preview {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--el-border-color);
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-fill-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.template-actions {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.picker-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,8 @@ export const BUILTIN_SKILL_OPTIONS: { name: string; label: string }[] = [
|
||||
{ name: 'system_info', label: '系统信息' },
|
||||
{ name: 'json_process', label: 'JSON 处理' },
|
||||
{ name: 'database_query', label: '数据库查询' },
|
||||
{ name: 'adb_log', label: 'ADB 日志' }
|
||||
{ name: 'adb_log', label: 'ADB 日志' },
|
||||
{ name: 'agent_call', label: '调用 Agent' },
|
||||
]
|
||||
|
||||
export const BUILTIN_SKILL_LABELS: Record<string, string> = Object.fromEntries(
|
||||
|
||||
@@ -18,7 +18,16 @@
|
||||
|
||||
<el-form label-position="top" class="config-form">
|
||||
<!-- System Prompt -->
|
||||
<el-form-item label="系统提示词 (System Prompt)">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-with-action">
|
||||
<span>系统提示词 (System Prompt)</span>
|
||||
<el-button size="small" text type="primary" @click="openTemplateDialog">
|
||||
<el-icon><Collection /></el-icon>
|
||||
从 Prompt 模板库选择
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.system_prompt"
|
||||
type="textarea"
|
||||
@@ -143,6 +152,19 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- Prompt 模板选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="templateDialogVisible"
|
||||
title="Prompt 模板库"
|
||||
width="680px"
|
||||
destroy-on-close
|
||||
>
|
||||
<PromptTemplatePicker
|
||||
ref="templatePickerRef"
|
||||
@use="onTemplateUse"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
@@ -151,8 +173,9 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Collection } from '@element-plus/icons-vue'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import PromptTemplatePicker from '@/components/PromptTemplatePicker.vue'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useModelConfigStore } from '@/stores/modelConfig'
|
||||
import { BUILTIN_SKILL_OPTIONS } from '@/utils/agentSkills'
|
||||
@@ -239,6 +262,24 @@ function findAgentNode(nodes: WorkflowNode[]): WorkflowNode | undefined {
|
||||
return nodes.find((n) => n.type === 'agent' || n.type === 'llm')
|
||||
}
|
||||
|
||||
// ── Prompt 模板对话框 ──
|
||||
const templateDialogVisible = ref(false)
|
||||
const templatePickerRef = ref()
|
||||
|
||||
function openTemplateDialog() {
|
||||
templateDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onTemplateUse(tpl: any) {
|
||||
form.value.system_prompt = tpl.prompt || ''
|
||||
// 可选:同步填充模型配置
|
||||
if (tpl.model) form.value.model = tpl.model
|
||||
if (tpl.provider) form.value.provider = tpl.provider
|
||||
if (tpl.temperature) form.value.temperature = parseFloat(tpl.temperature)
|
||||
templateDialogVisible.value = false
|
||||
ElMessage.success(`已加载模板「${tpl.name}」`)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/agents')
|
||||
}
|
||||
@@ -320,6 +361,13 @@ async function handleSave() {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.label-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<el-button @click="openTemplateDialog">
|
||||
从场景模板创建
|
||||
</el-button>
|
||||
<el-button @click="openPromptCreateDialog">
|
||||
<el-icon><Collection /></el-icon>
|
||||
从 Prompt 模板创建
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleCreate" style="margin-left: 10px">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建Agent
|
||||
@@ -317,6 +321,49 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 从 Prompt 模板创建 -->
|
||||
<el-dialog
|
||||
v-model="promptCreateDialogVisible"
|
||||
title="从 Prompt 模板创建 Agent"
|
||||
width="700px"
|
||||
destroy-on-close
|
||||
@close="resetPromptCreateForm"
|
||||
>
|
||||
<PromptTemplatePicker
|
||||
ref="promptCreatePickerRef"
|
||||
@use="onPromptCreateUse"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-tip">请先在上方选择 Prompt 模板,然后填写 Agent 信息</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 填写 Agent 信息(Prompt 模板已有选中的) -->
|
||||
<el-dialog
|
||||
v-model="promptCreateInfoVisible"
|
||||
title="完善 Agent 信息"
|
||||
width="500px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="Prompt 模板">
|
||||
<el-tag>{{ promptCreateSelected?.name }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="promptCreateForm.name" placeholder="新 Agent 名称" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="promptCreateForm.description" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="promptCreateInfoVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="promptCreateSubmitting" @click="submitPromptCreate">
|
||||
创建 Agent
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 能力 / 技能配置 -->
|
||||
<el-dialog
|
||||
v-model="skillDialogVisible"
|
||||
@@ -384,6 +431,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import PromptTemplatePicker from '@/components/PromptTemplatePicker.vue'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
@@ -399,7 +447,8 @@ import {
|
||||
UploadFilled,
|
||||
ChatDotRound,
|
||||
Tools,
|
||||
Operation
|
||||
Operation,
|
||||
Collection
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import type { Agent } from '@/stores/agent'
|
||||
@@ -489,6 +538,89 @@ async function submitTemplateCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 从 Prompt 模板创建 ──
|
||||
const promptCreateDialogVisible = ref(false)
|
||||
const promptCreateInfoVisible = ref(false)
|
||||
const promptCreateSubmitting = ref(false)
|
||||
const promptCreateSelected = ref<any>(null)
|
||||
const promptCreatePickerRef = ref()
|
||||
const promptCreateForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
function openPromptCreateDialog() {
|
||||
promptCreateDialogVisible.value = true
|
||||
}
|
||||
|
||||
function resetPromptCreateForm() {
|
||||
promptCreateSelected.value = null
|
||||
promptCreateForm.value = { name: '', description: '' }
|
||||
}
|
||||
|
||||
function onPromptCreateUse(tpl: any) {
|
||||
promptCreateSelected.value = tpl
|
||||
promptCreateForm.value.name = tpl.name || ''
|
||||
promptCreateForm.value.description = tpl.description || ''
|
||||
promptCreateDialogVisible.value = false
|
||||
promptCreateInfoVisible.value = true
|
||||
}
|
||||
|
||||
async function submitPromptCreate() {
|
||||
const name = promptCreateForm.value.name.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入名称')
|
||||
return
|
||||
}
|
||||
if (!promptCreateSelected.value) {
|
||||
ElMessage.warning('请先选择 Prompt 模板')
|
||||
return
|
||||
}
|
||||
const tpl = promptCreateSelected.value
|
||||
|
||||
const workflowConfig = {
|
||||
nodes: [
|
||||
{ id: 'start-1', type: 'start', position: { x: 80, y: 120 }, data: {} },
|
||||
{
|
||||
id: 'agent-1',
|
||||
type: 'agent',
|
||||
position: { x: 320, y: 120 },
|
||||
data: {
|
||||
label: name,
|
||||
system_prompt: tpl.prompt,
|
||||
model: tpl.model || 'deepseek-v4-flash',
|
||||
provider: tpl.provider || 'deepseek',
|
||||
temperature: parseFloat(tpl.temperature || '0.7'),
|
||||
max_iterations: 10,
|
||||
tools: [],
|
||||
memory: true,
|
||||
},
|
||||
},
|
||||
{ id: 'end-1', type: 'end', position: { x: 560, y: 120 }, data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e_start_agent', source: 'start-1', target: 'agent-1', sourceHandle: 'right', targetHandle: 'left' },
|
||||
{ id: 'e_agent_end', source: 'agent-1', target: 'end-1', sourceHandle: 'right', targetHandle: 'left' },
|
||||
],
|
||||
}
|
||||
|
||||
promptCreateSubmitting.value = true
|
||||
try {
|
||||
await agentStore.createAgent({
|
||||
name,
|
||||
description: promptCreateForm.value.description?.trim() || undefined,
|
||||
workflow_config: workflowConfig,
|
||||
})
|
||||
ElMessage.success(`已从 Prompt 模板创建 Agent「${name}」`)
|
||||
promptCreateInfoVisible.value = false
|
||||
await loadAgents()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '创建失败')
|
||||
} finally {
|
||||
promptCreateSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索和筛选
|
||||
const searchText = ref('')
|
||||
const statusFilter = ref('')
|
||||
@@ -1018,4 +1150,9 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dialog-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user