776 lines
26 KiB
Python
776 lines
26 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
BaoDan Workflow 自动化配置脚本
|
|||
|
|
|
|||
|
|
功能:
|
|||
|
|
1. 登录 BaoDan 后台获取 token
|
|||
|
|
2. 创建 Workflow 应用(产品推荐方案生成)
|
|||
|
|
3. 配置 6 个节点的 Workflow 图
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
python scripts/setup_workflow.py
|
|||
|
|
|
|||
|
|
前置条件:
|
|||
|
|
1. BaoDan 服务已启动(docker compose up -d)
|
|||
|
|
2. 已创建管理员账号(默认:taiyi@baodan.com / taiyi1224)
|
|||
|
|
3. 已配置 LLM 模型(在 BaoDan 后台 设置 -> 模型供应商)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import base64
|
|||
|
|
import json
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import requests
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
# 修复 Windows 控制台编码
|
|||
|
|
import io
|
|||
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|||
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
|||
|
|
|
|||
|
|
# ===== 配置 =====
|
|||
|
|
BAODAN_URL = "http://localhost:5001"
|
|||
|
|
ADMIN_EMAIL = "taiyi@baodan.com"
|
|||
|
|
ADMIN_PASSWORD = "taiyi1224"
|
|||
|
|
|
|||
|
|
# Workflow 节点 ID(固定值,用于连线)
|
|||
|
|
NODE_IDS = {
|
|||
|
|
"start": "start",
|
|||
|
|
"validate": "validate_params",
|
|||
|
|
"generate_queries": "generate_queries",
|
|||
|
|
"knowledge_retrieval": "knowledge_retrieval",
|
|||
|
|
"llm_generate": "llm_generate",
|
|||
|
|
"error_handler": "error_handler",
|
|||
|
|
"format_output": "format_output",
|
|||
|
|
"end": "end",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BaoDanWorkflowSetup:
|
|||
|
|
"""BaoDan Workflow 自动化配置器。"""
|
|||
|
|
|
|||
|
|
def __init__(self, base_url: str):
|
|||
|
|
self.base_url = base_url
|
|||
|
|
self.session = requests.Session()
|
|||
|
|
self.token = None
|
|||
|
|
self.csrf_token = None
|
|||
|
|
self.tenant_id = None
|
|||
|
|
self.app_id = None
|
|||
|
|
|
|||
|
|
def login(self, email: str, password: str) -> bool:
|
|||
|
|
"""登录获取 token。"""
|
|||
|
|
print(f"[1/6] 登录 BaoDan ({email})...")
|
|||
|
|
# Dify 要求密码 Base64 编码
|
|||
|
|
encoded_password = base64.b64encode(password.encode('utf-8')).decode('utf-8')
|
|||
|
|
resp = self.session.post(
|
|||
|
|
f"{self.base_url}/console/api/login",
|
|||
|
|
json={"email": email, "password": encoded_password},
|
|||
|
|
)
|
|||
|
|
if resp.status_code != 200:
|
|||
|
|
print(f" ❌ 登录失败: {resp.text}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
data = resp.json()
|
|||
|
|
if data.get("result") != "success":
|
|||
|
|
print(f" ❌ 登录失败: {data}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# Dify 将 token 设置到 cookie 中
|
|||
|
|
# 从 cookie 中提取 access_token
|
|||
|
|
cookies = resp.cookies
|
|||
|
|
self.token = cookies.get("access_token")
|
|||
|
|
|
|||
|
|
if not self.token:
|
|||
|
|
# 尝试从 set-cookie 头中提取
|
|||
|
|
for cookie in resp.headers.get("set-cookie", "").split(";"):
|
|||
|
|
if "access_token" in cookie:
|
|||
|
|
parts = cookie.split("=")
|
|||
|
|
if len(parts) == 2:
|
|||
|
|
self.token = parts[1].strip()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not self.token:
|
|||
|
|
print(f" ❌ 未获取到 token,cookies: {dict(cookies)}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 提取 CSRF token
|
|||
|
|
self.csrf_token = cookies.get("csrf_token")
|
|||
|
|
if not self.csrf_token:
|
|||
|
|
for cookie in resp.headers.get("set-cookie", "").split(";"):
|
|||
|
|
if "csrf_token" in cookie:
|
|||
|
|
parts = cookie.split("=")
|
|||
|
|
if len(parts) == 2:
|
|||
|
|
self.csrf_token = parts[1].strip()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
self.session.headers.update({
|
|||
|
|
"Authorization": f"Bearer {self.token}",
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 设置 CSRF token 到请求头
|
|||
|
|
if self.csrf_token:
|
|||
|
|
self.session.headers.update({
|
|||
|
|
"X-CSRF-Token": self.csrf_token,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
print(f" ✅ 登录成功")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def find_existing_app(self) -> str | None:
|
|||
|
|
"""查找已存在的同名 Workflow 应用。"""
|
|||
|
|
page = 1
|
|||
|
|
while True:
|
|||
|
|
resp = self.session.get(
|
|||
|
|
f"{self.base_url}/console/api/apps",
|
|||
|
|
params={"page": page, "limit": 100}
|
|||
|
|
)
|
|||
|
|
if resp.status_code != 200:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
data = resp.json()
|
|||
|
|
apps = data.get("data", [])
|
|||
|
|
if not apps:
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
for app in apps:
|
|||
|
|
if app.get("name") == "产品推荐方案生成" and app.get("mode") == "workflow":
|
|||
|
|
return app.get("id")
|
|||
|
|
|
|||
|
|
# 检查是否还有更多页
|
|||
|
|
if len(apps) < 100:
|
|||
|
|
break
|
|||
|
|
page += 1
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def create_workflow_app(self) -> bool:
|
|||
|
|
"""创建 Workflow 类型应用(如果不存在则复用)。"""
|
|||
|
|
print("[2/6] 检查/创建 Workflow 应用...")
|
|||
|
|
|
|||
|
|
# 先检查是否已存在同名应用
|
|||
|
|
existing_app_id = self.find_existing_app()
|
|||
|
|
if existing_app_id:
|
|||
|
|
self.app_id = existing_app_id
|
|||
|
|
print(f" ♻️ 复用已存在的应用,App ID: {self.app_id}")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 不存在则创建新应用
|
|||
|
|
resp = self.session.post(
|
|||
|
|
f"{self.base_url}/console/api/apps",
|
|||
|
|
json={
|
|||
|
|
"name": "产品推荐方案生成",
|
|||
|
|
"description": "根据客户信息和险种需求,调用知识库检索并生成保险产品推荐方案",
|
|||
|
|
"mode": "workflow",
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
if resp.status_code not in (200, 201):
|
|||
|
|
print(f" ❌ 创建应用失败: {resp.text}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
data = resp.json()
|
|||
|
|
self.app_id = data.get("id")
|
|||
|
|
if not self.app_id:
|
|||
|
|
print(f" ❌ 未获取到 App ID: {data}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
print(f" ✅ 应用创建成功,App ID: {self.app_id}")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def get_workflow_id(self) -> str | None:
|
|||
|
|
"""获取当前 draft workflow ID。"""
|
|||
|
|
resp = self.session.get(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/workflows/publish"
|
|||
|
|
)
|
|||
|
|
if resp.status_code == 200:
|
|||
|
|
data = resp.json()
|
|||
|
|
if data:
|
|||
|
|
return data.get("workflow", {}).get("id")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def build_workflow_graph(self) -> dict:
|
|||
|
|
"""构建 Workflow 节点图。"""
|
|||
|
|
return {
|
|||
|
|
"nodes": self._build_nodes(),
|
|||
|
|
"edges": self._build_edges(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _build_nodes(self) -> list[dict]:
|
|||
|
|
"""构建所有节点配置。"""
|
|||
|
|
return [
|
|||
|
|
# Start 节点
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["start"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "start",
|
|||
|
|
"title": "开始",
|
|||
|
|
"desc": "",
|
|||
|
|
"variables": self._get_start_variables(),
|
|||
|
|
},
|
|||
|
|
"position": {"x": 80, "y": 282},
|
|||
|
|
},
|
|||
|
|
# 节点1:参数校验
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["validate"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "code",
|
|||
|
|
"title": "参数校验",
|
|||
|
|
"desc": "校验客户年龄、性别、职业、预算等参数",
|
|||
|
|
"variables": [],
|
|||
|
|
"code": self._get_validate_code(),
|
|||
|
|
"code_language": "python3",
|
|||
|
|
"outputs": {
|
|||
|
|
"validated_params": {"type": "object", "children": None},
|
|||
|
|
"error_msg": {"type": "string", "children": None},
|
|||
|
|
"is_valid": {"type": "boolean", "children": None},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"position": {"x": 320, "y": 282},
|
|||
|
|
},
|
|||
|
|
# 节点2:检索策略生成
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["generate_queries"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "code",
|
|||
|
|
"title": "检索策略生成",
|
|||
|
|
"desc": "为每个险种生成检索词",
|
|||
|
|
"variables": [],
|
|||
|
|
"code": self._get_generate_queries_code(),
|
|||
|
|
"code_language": "python3",
|
|||
|
|
"outputs": {
|
|||
|
|
"search_queries": {"type": "array[object]", "children": None},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"position": {"x": 560, "y": 282},
|
|||
|
|
},
|
|||
|
|
# 节点3:知识库检索
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["knowledge_retrieval"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "knowledge-retrieval",
|
|||
|
|
"title": "知识库检索",
|
|||
|
|
"desc": "从保险知识库中检索相关产品条款",
|
|||
|
|
"query_variable_selector": [NODE_IDS["generate_queries"], "search_queries"],
|
|||
|
|
"dataset_ids": [], # 需要手动关联知识库
|
|||
|
|
"retrieval_mode": "multiple",
|
|||
|
|
"multiple_retrieval_config": {
|
|||
|
|
"top_k": 5,
|
|||
|
|
"score_threshold": 0.6,
|
|||
|
|
"reranking_model": None,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"position": {"x": 800, "y": 282},
|
|||
|
|
},
|
|||
|
|
# 节点4:LLM 方案生成
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["llm_generate"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "llm",
|
|||
|
|
"title": "LLM 方案生成",
|
|||
|
|
"desc": "调用 LLM 生成三套保险推荐方案",
|
|||
|
|
"model": {
|
|||
|
|
"provider": "deepseek",
|
|||
|
|
"name": "deepseek-chat",
|
|||
|
|
"mode": "chat",
|
|||
|
|
"completion_params": {
|
|||
|
|
"temperature": 0.7,
|
|||
|
|
"top_p": 0.9,
|
|||
|
|
"max_tokens": 8192,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"prompt_template": self._get_llm_prompt(),
|
|||
|
|
"memory": None,
|
|||
|
|
},
|
|||
|
|
"position": {"x": 1040, "y": 282},
|
|||
|
|
},
|
|||
|
|
# 节点5:异常处理
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["error_handler"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "code",
|
|||
|
|
"title": "异常处理",
|
|||
|
|
"desc": "处理校验失败或生成失败的情况",
|
|||
|
|
"variables": [],
|
|||
|
|
"code": self._get_error_handler_code(),
|
|||
|
|
"code_language": "python3",
|
|||
|
|
"outputs": {
|
|||
|
|
"final_output": {"type": "string", "children": None},
|
|||
|
|
"is_success": {"type": "boolean", "children": None},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"position": {"x": 1040, "y": 520},
|
|||
|
|
},
|
|||
|
|
# 节点6:格式化输出
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["format_output"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "code",
|
|||
|
|
"title": "格式化输出",
|
|||
|
|
"desc": "去除多余空行和空白",
|
|||
|
|
"variables": [],
|
|||
|
|
"code": self._get_format_output_code(),
|
|||
|
|
"code_language": "python3",
|
|||
|
|
"outputs": {
|
|||
|
|
"recommendation": {"type": "string", "children": None},
|
|||
|
|
"is_success": {"type": "boolean", "children": None},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"position": {"x": 1280, "y": 282},
|
|||
|
|
},
|
|||
|
|
# End 节点
|
|||
|
|
{
|
|||
|
|
"id": NODE_IDS["end"],
|
|||
|
|
"type": "custom",
|
|||
|
|
"data": {
|
|||
|
|
"type": "end",
|
|||
|
|
"title": "结束",
|
|||
|
|
"desc": "",
|
|||
|
|
"outputs": [
|
|||
|
|
{
|
|||
|
|
"variable_selector": [NODE_IDS["format_output"], "recommendation"],
|
|||
|
|
"value_selector": [NODE_IDS["format_output"], "recommendation"],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"variable_selector": [NODE_IDS["format_output"], "is_success"],
|
|||
|
|
"value_selector": [NODE_IDS["format_output"], "is_success"],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
"position": {"x": 1520, "y": 282},
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def _build_edges(self) -> list[dict]:
|
|||
|
|
"""构建节点连线。"""
|
|||
|
|
return [
|
|||
|
|
# Start → 参数校验
|
|||
|
|
{"source": NODE_IDS["start"], "target": NODE_IDS["validate"]},
|
|||
|
|
# 参数校验 → is_valid=True → 检索策略生成
|
|||
|
|
{
|
|||
|
|
"source": NODE_IDS["validate"],
|
|||
|
|
"target": NODE_IDS["generate_queries"],
|
|||
|
|
"sourceHandle": "is_valid",
|
|||
|
|
},
|
|||
|
|
# 参数校验 → is_valid=False → 异常处理
|
|||
|
|
{
|
|||
|
|
"source": NODE_IDS["validate"],
|
|||
|
|
"target": NODE_IDS["error_handler"],
|
|||
|
|
"sourceHandle": "is_invalid",
|
|||
|
|
},
|
|||
|
|
# 检索策略生成 → 知识库检索
|
|||
|
|
{"source": NODE_IDS["generate_queries"], "target": NODE_IDS["knowledge_retrieval"]},
|
|||
|
|
# 知识库检索 → LLM 方案生成
|
|||
|
|
{"source": NODE_IDS["knowledge_retrieval"], "target": NODE_IDS["llm_generate"]},
|
|||
|
|
# LLM 方案生成 → 格式化输出
|
|||
|
|
{"source": NODE_IDS["llm_generate"], "target": NODE_IDS["format_output"]},
|
|||
|
|
# 异常处理 → 格式化输出
|
|||
|
|
{"source": NODE_IDS["error_handler"], "target": NODE_IDS["format_output"]},
|
|||
|
|
# 格式化输出 → 结束
|
|||
|
|
{"source": NODE_IDS["format_output"], "target": NODE_IDS["end"]},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def _get_start_variables(self) -> list[dict]:
|
|||
|
|
"""获取开始节点的输入变量定义。"""
|
|||
|
|
return [
|
|||
|
|
{"variable": "age", "label": "年龄", "type": "string", "required": True},
|
|||
|
|
{"variable": "gender", "label": "性别", "type": "string", "required": True},
|
|||
|
|
{"variable": "occupation", "label": "职业", "type": "string", "required": True},
|
|||
|
|
{"variable": "annual_income", "label": "年收入(万)", "type": "string", "required": True},
|
|||
|
|
{"variable": "monthly_budget", "label": "月预算(元)", "type": "string", "required": True},
|
|||
|
|
{"variable": "insurance_types", "label": "险种", "type": "string", "required": True},
|
|||
|
|
{"variable": "coverage_amount", "label": "保额(元)", "type": "string", "required": True},
|
|||
|
|
{"variable": "coverage_period", "label": "保障期限", "type": "string", "required": True},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def _get_validate_code(self) -> str:
|
|||
|
|
"""节点1:参数校验代码。"""
|
|||
|
|
return '''def main(age: str, gender: str, occupation: str, annual_income: str,
|
|||
|
|
monthly_budget: str, insurance_types: str, coverage_amount: str,
|
|||
|
|
coverage_period: str) -> dict:
|
|||
|
|
"""校验客户参数。"""
|
|||
|
|
errors = []
|
|||
|
|
|
|||
|
|
# 校验年龄
|
|||
|
|
try:
|
|||
|
|
age_int = int(age)
|
|||
|
|
if age_int < 1 or age_int > 150:
|
|||
|
|
errors.append("年龄必须在 1-150 之间")
|
|||
|
|
except ValueError:
|
|||
|
|
errors.append("年龄必须为整数")
|
|||
|
|
|
|||
|
|
# 校验性别
|
|||
|
|
if gender not in ("male", "female"):
|
|||
|
|
errors.append("性别必须为 male 或 female")
|
|||
|
|
|
|||
|
|
# 校验职业
|
|||
|
|
if not occupation or len(occupation) < 1:
|
|||
|
|
errors.append("职业不能为空")
|
|||
|
|
|
|||
|
|
# 校验月预算
|
|||
|
|
try:
|
|||
|
|
budget = float(monthly_budget)
|
|||
|
|
if budget <= 0:
|
|||
|
|
errors.append("月预算必须大于 0")
|
|||
|
|
except ValueError:
|
|||
|
|
errors.append("月预算必须为数字")
|
|||
|
|
|
|||
|
|
# 校验保额
|
|||
|
|
try:
|
|||
|
|
amount = float(coverage_amount)
|
|||
|
|
if amount < 10000:
|
|||
|
|
errors.append("保额不能低于 10000 元")
|
|||
|
|
except ValueError:
|
|||
|
|
errors.append("保额必须为数字")
|
|||
|
|
|
|||
|
|
# 校验险种
|
|||
|
|
valid_types = ["重疾险", "医疗险", "意外险", "寿险", "年金险", "财产险"]
|
|||
|
|
types_list = [t.strip() for t in insurance_types.split(",") if t.strip()]
|
|||
|
|
if not types_list:
|
|||
|
|
errors.append("至少选择一种险种")
|
|||
|
|
else:
|
|||
|
|
for t in types_list:
|
|||
|
|
if t not in valid_types:
|
|||
|
|
errors.append(f"不支持的险种: {t}")
|
|||
|
|
|
|||
|
|
# 校验保障期限
|
|||
|
|
valid_periods = ["10年", "20年", "30年", "至60岁", "至70岁", "终身"]
|
|||
|
|
if coverage_period not in valid_periods:
|
|||
|
|
errors.append(f"不支持的保障期限: {coverage_period}")
|
|||
|
|
|
|||
|
|
if errors:
|
|||
|
|
return {
|
|||
|
|
"validated_params": {},
|
|||
|
|
"error_msg": "; ".join(errors),
|
|||
|
|
"is_valid": False,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"validated_params": {
|
|||
|
|
"age": age,
|
|||
|
|
"gender": gender,
|
|||
|
|
"occupation": occupation,
|
|||
|
|
"annual_income": annual_income,
|
|||
|
|
"monthly_budget": monthly_budget,
|
|||
|
|
"insurance_types": insurance_types,
|
|||
|
|
"coverage_amount": coverage_amount,
|
|||
|
|
"coverage_period": coverage_period,
|
|||
|
|
},
|
|||
|
|
"error_msg": "",
|
|||
|
|
"is_valid": True,
|
|||
|
|
}
|
|||
|
|
'''
|
|||
|
|
|
|||
|
|
def _get_generate_queries_code(self) -> str:
|
|||
|
|
"""节点2:检索策略生成代码。"""
|
|||
|
|
return '''def main(validated_params: dict) -> dict:
|
|||
|
|
"""为每个险种生成检索词。"""
|
|||
|
|
insurance_types = validated_params.get("insurance_types", "")
|
|||
|
|
age = validated_params.get("age", "")
|
|||
|
|
occupation = validated_params.get("occupation", "")
|
|||
|
|
|
|||
|
|
types_list = [t.strip() for t in insurance_types.split(",") if t.strip()]
|
|||
|
|
|
|||
|
|
search_queries = []
|
|||
|
|
for ins_type in types_list:
|
|||
|
|
query = f"{ins_type} 产品条款 保额 费率 {age}岁 {occupation}"
|
|||
|
|
search_queries.append({
|
|||
|
|
"insurance_type": ins_type,
|
|||
|
|
"query": query,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {"search_queries": search_queries}
|
|||
|
|
'''
|
|||
|
|
|
|||
|
|
def _get_llm_prompt(self) -> list[dict]:
|
|||
|
|
"""节点4:LLM Prompt 模板。"""
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
"role": "system",
|
|||
|
|
"text": """你是一位专业的保险顾问,擅长根据客户需求生成保险产品推荐方案。
|
|||
|
|
|
|||
|
|
重要规则:
|
|||
|
|
1. 保费数据必须来自检索结果,绝对不能编造
|
|||
|
|
2. 推荐方案的总保费不能超过客户月预算 × 12
|
|||
|
|
3. 每套方案必须包含:产品名称、保险公司、险种、保额、年保费、推荐理由
|
|||
|
|
4. 最终输出必须包含免责声明"""
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"role": "user",
|
|||
|
|
"text": """请根据以下客户信息和检索到的保险产品资料,生成三套保险推荐方案。
|
|||
|
|
|
|||
|
|
## 客户信息
|
|||
|
|
- 年龄:{{#start.age#}} 岁
|
|||
|
|
- 性别:{{#start.gender#}}
|
|||
|
|
- 职业:{{#start.occupation#}}
|
|||
|
|
- 年收入:{{#start.annual_income#}} 万元
|
|||
|
|
- 月预算:{{#start.monthly_budget#}} 元
|
|||
|
|
- 需要险种:{{#start.insurance_types#}}
|
|||
|
|
- 期望保额:{{#start.coverage_amount#}} 元
|
|||
|
|
- 保障期限:{{#start.coverage_period#}}
|
|||
|
|
|
|||
|
|
## 检索到的保险产品资料
|
|||
|
|
{{#knowledge_retrieval.result#}}
|
|||
|
|
|
|||
|
|
## 输出要求
|
|||
|
|
请生成以下三套方案,使用 Markdown 表格格式:
|
|||
|
|
|
|||
|
|
### 基础方案(年保费约 XXXX 元)
|
|||
|
|
| 产品名称 | 所属保险公司 | 险种 | 保额 | 年保费 | 推荐理由 |
|
|||
|
|
|---------|------------|------|------|-------|---------|
|
|||
|
|
| ... | ... | ... | ... | ... | ... |
|
|||
|
|
|
|||
|
|
方案总结:...
|
|||
|
|
|
|||
|
|
### 均衡方案(年保费约 XXXX 元)
|
|||
|
|
(格式同上)
|
|||
|
|
|
|||
|
|
### 全面方案(年保费约 XXXX 元)
|
|||
|
|
(格式同上)
|
|||
|
|
|
|||
|
|
## 免责声明
|
|||
|
|
本推荐方案仅供参考,具体保障内容以保险合同为准。建议在投保前仔细阅读保险条款。"""
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def _get_error_handler_code(self) -> str:
|
|||
|
|
"""节点5:异常处理代码。"""
|
|||
|
|
return '''def main(is_valid: bool, error_msg: str, recommendation: str = "") -> dict:
|
|||
|
|
"""处理异常情况。"""
|
|||
|
|
if not is_valid:
|
|||
|
|
return {
|
|||
|
|
"final_output": f"参数校验失败:{error_msg}",
|
|||
|
|
"is_success": False,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if not recommendation:
|
|||
|
|
return {
|
|||
|
|
"final_output": "未能生成推荐方案,请稍后重试",
|
|||
|
|
"is_success": False,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"final_output": recommendation,
|
|||
|
|
"is_success": True,
|
|||
|
|
}
|
|||
|
|
'''
|
|||
|
|
|
|||
|
|
def _get_format_output_code(self) -> str:
|
|||
|
|
"""节点6:格式化输出代码。"""
|
|||
|
|
return '''def main(recommendation: str, is_success: bool) -> dict:
|
|||
|
|
"""格式化输出,去除多余空白。"""
|
|||
|
|
if not is_success:
|
|||
|
|
return {
|
|||
|
|
"recommendation": recommendation,
|
|||
|
|
"is_success": False,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 去除多余空行(连续空行合并为一个)
|
|||
|
|
import re
|
|||
|
|
formatted = re.sub(r"\\n{3,}", "\\n\\n", recommendation)
|
|||
|
|
# 去除首尾空白
|
|||
|
|
formatted = formatted.strip()
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"recommendation": formatted,
|
|||
|
|
"is_success": True,
|
|||
|
|
}
|
|||
|
|
'''
|
|||
|
|
|
|||
|
|
def sync_workflow(self) -> bool:
|
|||
|
|
"""同步 Workflow 配置到 BaoDan。"""
|
|||
|
|
print("[3/6] 同步 Workflow 配置...")
|
|||
|
|
|
|||
|
|
graph = self.build_workflow_graph()
|
|||
|
|
|
|||
|
|
# 获取 workflow ID
|
|||
|
|
workflow_id = self.get_workflow_id()
|
|||
|
|
if not workflow_id:
|
|||
|
|
print(" ⚠️ 未找到 workflow,尝试直接同步...")
|
|||
|
|
|
|||
|
|
resp = self.session.post(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/workflows/draft",
|
|||
|
|
json={
|
|||
|
|
"graph": graph,
|
|||
|
|
"features": {},
|
|||
|
|
"hash": "",
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if resp.status_code != 200:
|
|||
|
|
print(f" ❌ 同步失败: {resp.text}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
print(" ✅ Workflow 配置同步成功")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
def get_api_key(self) -> str | None:
|
|||
|
|
"""获取应用的 API Key。"""
|
|||
|
|
print("[4/6] 获取 API Key...")
|
|||
|
|
|
|||
|
|
# 先尝试获取已有的 API Key
|
|||
|
|
resp = self.session.get(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/api-keys"
|
|||
|
|
)
|
|||
|
|
if resp.status_code == 200:
|
|||
|
|
keys = resp.json().get("data", [])
|
|||
|
|
if keys:
|
|||
|
|
api_key = keys[0].get("token")
|
|||
|
|
print(f" ✅ API Key: {api_key}")
|
|||
|
|
return api_key
|
|||
|
|
|
|||
|
|
# 创建新的 API Key
|
|||
|
|
resp = self.session.post(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/api-keys",
|
|||
|
|
json={"name": "推荐服务"},
|
|||
|
|
)
|
|||
|
|
if resp.status_code in (200, 201):
|
|||
|
|
data = resp.json()
|
|||
|
|
api_key = data.get("token")
|
|||
|
|
if api_key:
|
|||
|
|
print(f" ✅ 新建 API Key: {api_key}")
|
|||
|
|
return api_key
|
|||
|
|
# 可能返回的是完整对象
|
|||
|
|
api_key = data.get("id") # 有些版本返回 id 作为 token
|
|||
|
|
if api_key:
|
|||
|
|
print(f" ✅ 新建 API Key: {api_key}")
|
|||
|
|
return api_key
|
|||
|
|
|
|||
|
|
print(f" ❌ 创建 API Key 失败: {resp.text}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def publish_workflow(self) -> bool:
|
|||
|
|
"""发布 Workflow。"""
|
|||
|
|
print("[5/6] 发布 Workflow...")
|
|||
|
|
|
|||
|
|
# 先获取 draft workflow
|
|||
|
|
draft_resp = self.session.get(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/workflows/draft"
|
|||
|
|
)
|
|||
|
|
if draft_resp.status_code == 200:
|
|||
|
|
draft_data = draft_resp.json()
|
|||
|
|
workflow_id = draft_data.get("id")
|
|||
|
|
if workflow_id:
|
|||
|
|
print(f" 📝 Draft Workflow ID: {workflow_id}")
|
|||
|
|
|
|||
|
|
# 发布
|
|||
|
|
resp = self.session.post(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/workflows/publish",
|
|||
|
|
json={},
|
|||
|
|
)
|
|||
|
|
if resp.status_code in (200, 201):
|
|||
|
|
print(" ✅ Workflow 已发布")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 尝试不带 body 发布
|
|||
|
|
resp = self.session.post(
|
|||
|
|
f"{self.base_url}/console/api/apps/{self.app_id}/workflows/publish"
|
|||
|
|
)
|
|||
|
|
if resp.status_code in (200, 201):
|
|||
|
|
print(" ✅ Workflow 已发布")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
print(f" ⚠️ 发布失败(可手动在后台发布): {resp.text}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def update_env_config(self, api_key: str) -> bool:
|
|||
|
|
"""更新 .env 配置文件。"""
|
|||
|
|
print("[6/6] 更新 .env 配置...")
|
|||
|
|
|
|||
|
|
env_content = f"""# BaoDan Workflow API Key(自动生成于 {time.strftime('%Y-%m-%d %H:%M:%S')})
|
|||
|
|
BAODAN_WORKFLOW_API_KEY={api_key}
|
|||
|
|
BAODAN_API_URL={self.base_url}
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 写入到 docker-compose 环境变量提示
|
|||
|
|
print(f"""
|
|||
|
|
✅ 请将以下配置添加到 docker-compose.dify.yml 的 baodan-api 环境变量:
|
|||
|
|
|
|||
|
|
BAODAN_WORKFLOW_API_KEY: "{api_key}"
|
|||
|
|
|
|||
|
|
或者创建 .env 文件:
|
|||
|
|
{env_content}
|
|||
|
|
""")
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主函数。"""
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("BaoDan Workflow 自动化配置脚本")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
setup = BaoDanWorkflowSetup(BAODAN_URL)
|
|||
|
|
|
|||
|
|
# 1. 登录
|
|||
|
|
if not setup.login(ADMIN_EMAIL, ADMIN_PASSWORD):
|
|||
|
|
print("\n❌ 登录失败,请检查:")
|
|||
|
|
print(" 1. BaoDan 服务是否已启动")
|
|||
|
|
print(" 2. 管理员账号密码是否正确")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 2. 创建 Workflow 应用
|
|||
|
|
if not setup.create_workflow_app():
|
|||
|
|
print("\n❌ 创建应用失败")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 3. 同步 Workflow 配置
|
|||
|
|
if not setup.sync_workflow():
|
|||
|
|
print("\n❌ 同步 Workflow 失败")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 4. 获取 API Key
|
|||
|
|
api_key = setup.get_api_key()
|
|||
|
|
if not api_key:
|
|||
|
|
print("\n⚠️ 获取 API Key 失败,请手动在后台创建")
|
|||
|
|
|
|||
|
|
# 5. 发布 Workflow
|
|||
|
|
setup.publish_workflow()
|
|||
|
|
|
|||
|
|
# 6. 更新配置
|
|||
|
|
if api_key:
|
|||
|
|
setup.update_env_config(api_key)
|
|||
|
|
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("✅ Workflow 配置完成!")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(f"""
|
|||
|
|
后续步骤:
|
|||
|
|
1. 访问 http://localhost:3000/workflow/{setup.app_id} 查看 Workflow
|
|||
|
|
2. 在「知识库检索」节点关联已创建的知识库
|
|||
|
|
3. 在「模型配置」中确认 DeepSeek 模型已配置
|
|||
|
|
4. 测试 Workflow:点击「运行」按钮测试
|
|||
|
|
5. 更新 docker-compose.dify.yml 中的 BAODAN_WORKFLOW_API_KEY
|
|||
|
|
|
|||
|
|
API 调用示例:
|
|||
|
|
curl -X POST http://localhost:5001/v1/workflows/run \\
|
|||
|
|
-H "Authorization: Bearer {api_key or 'YOUR_API_KEY'}" \\
|
|||
|
|
-H "Content-Type: application/json" \\
|
|||
|
|
-d '{{
|
|||
|
|
"inputs": {{
|
|||
|
|
"age": "35",
|
|||
|
|
"gender": "male",
|
|||
|
|
"occupation": "工程师",
|
|||
|
|
"annual_income": "30",
|
|||
|
|
"monthly_budget": "2000",
|
|||
|
|
"insurance_types": "重疾险,医疗险",
|
|||
|
|
"coverage_amount": "500000",
|
|||
|
|
"coverage_period": "终身"
|
|||
|
|
}},
|
|||
|
|
"response_mode": "blocking",
|
|||
|
|
"user": "test_user"
|
|||
|
|
}}'
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|