Browse Source

mod: first

wey 4 months ago
commit
3923abbf4e

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
1
+/logs/*
2
+/rasa_files/*

+ 4 - 0
api/__init__.py

@@ -0,0 +1,4 @@
1
+from api.main import app
2
+from api.schemas import GenerateFilesRequest
3
+
4
+__all__ = ['app', 'GenerateFilesRequest']

BIN
api/__pycache__/__init__.cpython-310.pyc


BIN
api/__pycache__/main.cpython-310.pyc


BIN
api/__pycache__/schemas.cpython-310.pyc


+ 149 - 0
api/main.py

@@ -0,0 +1,149 @@
1
+import logging
2
+from fastapi import FastAPI, Depends, HTTPException, Query
3
+from fastapi.middleware.cors import CORSMiddleware
4
+from sqlalchemy.orm import Session
5
+from typing import Optional, List, Dict, Any
6
+
7
+# 导入服务和模型
8
+from db import get_db, db, SystemConfig
9
+from services import RasaFileGenerator, RasaManager
10
+from api.schemas import GenerateFilesRequest
11
+from config import config
12
+
13
+# 初始化日志
14
+logger = logging.getLogger(__name__)
15
+
16
+# 初始化FastAPI
17
+app = FastAPI(
18
+    title="Rasa流程管理系统API",
19
+    description="支持版本管理、文件生成和服务重启的Rasa系统API",
20
+    version="1.0.0"
21
+)
22
+
23
+# 配置CORS
24
+app.add_middleware(
25
+    CORSMiddleware,
26
+    allow_origins=["*"],  # 生产环境中应指定具体的源
27
+    allow_credentials=True,
28
+    allow_methods=["*"],
29
+    allow_headers=["*"],
30
+)
31
+
32
+# 初始化服务
33
+try:
34
+    # 创建数据库表(如果不存在)
35
+    db.create_tables()
36
+    
37
+    # 初始化文件生成器和Rasa管理器
38
+    file_generator = RasaFileGenerator()
39
+    rasa_manager = RasaManager()
40
+    
41
+    logger.info("服务初始化成功")
42
+except Exception as e:
43
+    logger.error(f"服务初始化失败: {str(e)}", exc_info=True)
44
+    raise
45
+
46
+# API接口
47
+@app.post("/generate-files", response_model=Dict[str, Any], tags=["文件生成"])
48
+def generate_rasa_files(request: GenerateFilesRequest, db: Session = Depends(get_db)):
49
+    """生成Rasa配置文件"""
50
+    try:
51
+        logger.info(f"生成Rasa文件请求: {request.dict()}")
52
+        result = file_generator.generate_all_files(
53
+            session=db,
54
+        )
55
+        if result["success"]:
56
+            return result
57
+        else:
58
+            raise HTTPException(status_code=500, detail=result["message"])
59
+    except Exception as e:
60
+        logger.error(f"生成Rasa文件失败: {str(e)}", exc_info=True)
61
+        raise HTTPException(status_code=500, detail=f"生成Rasa文件失败: {str(e)}")
62
+
63
+@app.post("/rasa/restart", response_model=Dict[str, Any], tags=["服务管理"])
64
+def restart_rasa_services(
65
+    model_path: Optional[str] = None,
66
+    actions_dir: Optional[str] = None,
67
+    db: Session = Depends(get_db)
68
+):
69
+    """重启Rasa服务"""
70
+    try:
71
+        logger.info(f"重启Rasa服务请求: model_path={model_path}, actions_dir={actions_dir}")
72
+        # 如果未指定路径,使用生成的默认路径
73
+        if not actions_dir:
74
+            actions_dir = f"{file_generator.output_dir}/actions"
75
+        
76
+        result = rasa_manager.restart_rasa_services(
77
+            session=db,
78
+            model_path=model_path,
79
+            actions_dir=actions_dir
80
+        )
81
+        
82
+        if result["success"]:
83
+            return result
84
+        else:
85
+            raise HTTPException(status_code=500, detail=result["message"])
86
+    except Exception as e:
87
+        logger.error(f"重启Rasa服务失败: {str(e)}", exc_info=True)
88
+        raise HTTPException(status_code=500, detail=f"重启Rasa服务失败: {str(e)}")
89
+
90
+@app.post("/rasa/stop", response_model=Dict[str, Any], tags=["服务管理"])
91
+def stop_rasa_services():
92
+    """停止Rasa服务"""
93
+    try:
94
+        logger.info("停止Rasa服务请求")
95
+        success = rasa_manager.stop_all_services()
96
+        if success:
97
+            return {
98
+                "success": True,
99
+                "message": "所有Rasa服务已停止"
100
+            }
101
+        else:
102
+            raise HTTPException(status_code=500, detail="停止Rasa服务失败")
103
+    except Exception as e:
104
+        logger.error(f"停止Rasa服务失败: {str(e)}", exc_info=True)
105
+        raise HTTPException(status_code=500, detail=f"停止Rasa服务失败: {str(e)}")
106
+
107
+@app.get("/rasa/status", response_model=Dict[str, Any], tags=["服务管理"])
108
+def get_rasa_status(db: Session = Depends(get_db)):
109
+    """获取Rasa服务状态"""
110
+    try:
111
+        logger.info("获取Rasa服务状态请求")
112
+        rasa_url = SystemConfig.get_value(db, "rasa_server_url", "http://localhost:5005")
113
+        action_url = SystemConfig.get_value(db, "rasa_actions_url", "http://localhost:5055")
114
+        
115
+        rasa_healthy = rasa_manager._check_rasa_health(rasa_url)
116
+        action_healthy = rasa_manager._check_action_health(action_url)
117
+        
118
+        return {
119
+            "success": True,
120
+            "rasa_server": {
121
+                "url": rasa_url,
122
+                "healthy": rasa_healthy,
123
+                "pid": rasa_manager.rasa_process.pid if (rasa_manager.rasa_process and not rasa_manager.rasa_process.poll()) else None
124
+            },
125
+            "action_server": {
126
+                "url": action_url,
127
+                "healthy": action_healthy,
128
+                "pid": rasa_manager.action_process.pid if (rasa_manager.action_process and not rasa_manager.action_process.poll()) else None
129
+            }
130
+        }
131
+    except Exception as e:
132
+        logger.error(f"获取Rasa服务状态失败: {str(e)}", exc_info=True)
133
+        raise HTTPException(status_code=500, detail=f"获取Rasa服务状态失败: {str(e)}")
134
+
135
+# 启动服务
136
+if __name__ == "__main__":
137
+    import uvicorn
138
+    host = config.get("api.host", "0.0.0.0")
139
+    port = config.get("api.port", 8000)
140
+    debug = config.get("api.debug", False)
141
+    
142
+    logger.info(f"启动API服务: http://{host}:{port}")
143
+    uvicorn.run(
144
+        "api.main:app", 
145
+        host=host, 
146
+        port=port, 
147
+        debug=debug,
148
+        reload=debug
149
+    )

+ 5 - 0
api/schemas.py

@@ -0,0 +1,5 @@
1
+from pydantic import BaseModel
2
+from typing import Optional, List, Dict, Any
3
+
4
+class GenerateFilesRequest(BaseModel):
5
+    version_id: Optional[int] = None

+ 155 - 0
config/__init__.py

@@ -0,0 +1,155 @@
1
+import yaml
2
+import os
3
+import logging
4
+from typing import Dict, Any, Optional
5
+
6
+class Config:
7
+    """配置管理类,用于加载和访问配置文件"""
8
+    
9
+    def __init__(self, config_path: str = "config/config.yaml"):
10
+        """
11
+        初始化配置管理类
12
+        
13
+        Args:
14
+            config_path: 配置文件路径
15
+        """
16
+        self.config_path = config_path
17
+        self.config_data: Dict[str, Any] = {}
18
+        self._load_config()
19
+        self._setup_logging()
20
+    
21
+    def _load_config(self) -> None:
22
+        """加载配置文件"""
23
+        try:
24
+            if not os.path.exists(self.config_path):
25
+                raise FileNotFoundError(f"配置文件 {self.config_path} 不存在")
26
+            
27
+            with open(self.config_path, 'r', encoding='utf-8') as f:
28
+                self.config_data = yaml.safe_load(f)
29
+                
30
+            if not self.config_data:
31
+                raise ValueError(f"配置文件 {self.config_path} 内容为空")
32
+                
33
+            # 创建必要的目录
34
+            self._create_directories()
35
+            
36
+        except FileNotFoundError as e:
37
+            raise Exception(f"配置文件加载失败: {str(e)}")
38
+        except yaml.YAMLError as e:
39
+            raise Exception(f"配置文件解析错误: {str(e)}")
40
+        except Exception as e:
41
+            raise Exception(f"加载配置时发生错误: {str(e)}")
42
+    
43
+    def _create_directories(self) -> None:
44
+        """创建配置中指定的目录"""
45
+        try:
46
+            # 创建Rasa文件目录
47
+            rasa_files_dir = self.get("file_storage.rasa_files_dir")
48
+            if rasa_files_dir:
49
+                os.makedirs(rasa_files_dir, exist_ok=True)
50
+                os.makedirs(os.path.join(rasa_files_dir, "nlu"), exist_ok=True)
51
+                os.makedirs(os.path.join(rasa_files_dir, "stories"), exist_ok=True)
52
+                os.makedirs(os.path.join(rasa_files_dir, "actions"), exist_ok=True)
53
+            
54
+            # 创建日志目录
55
+            log_dir = self.get("file_storage.log_dir")
56
+            if log_dir:
57
+                os.makedirs(log_dir, exist_ok=True)
58
+                
59
+        except Exception as e:
60
+            raise Exception(f"创建目录时发生错误: {str(e)}")
61
+    
62
+    def _setup_logging(self) -> None:
63
+        """配置日志系统"""
64
+        try:
65
+            log_dir = self.get("file_storage.log_dir", "./logs")
66
+            max_log_size = self.get("file_storage.max_log_size", 10)
67
+            log_retention_days = self.get("file_storage.log_retention_days", 30)
68
+            
69
+            # 确保日志目录存在
70
+            os.makedirs(log_dir, exist_ok=True)
71
+            
72
+            # 配置日志格式
73
+            log_format = logging.Formatter(
74
+                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
75
+            )
76
+            
77
+            # 配置根日志器
78
+            root_logger = logging.getLogger()
79
+            root_logger.setLevel(logging.INFO)
80
+            
81
+            # 控制台日志处理器
82
+            console_handler = logging.StreamHandler()
83
+            console_handler.setFormatter(log_format)
84
+            root_logger.addHandler(console_handler)
85
+            
86
+            # 文件日志处理器
87
+            from logging.handlers import RotatingFileHandler
88
+            
89
+            # 按大小切割日志
90
+            file_handler = RotatingFileHandler(
91
+                os.path.join(log_dir, "rasa_manager.log"),
92
+                maxBytes=max_log_size * 1024 * 1024,  # 转换为字节
93
+                backupCount=log_retention_days,
94
+                encoding='utf-8'
95
+            )
96
+            file_handler.setFormatter(log_format)
97
+            root_logger.addHandler(file_handler)
98
+            
99
+        except Exception as e:
100
+            raise Exception(f"配置日志系统时发生错误: {str(e)}")
101
+    
102
+    def get(self, key: str, default: Any = None) -> Any:
103
+        """
104
+        获取配置值
105
+        
106
+        Args:
107
+            key: 配置键,支持点符号嵌套,如 "database.connection_string"
108
+            default: 默认值,如果配置不存在则返回此值
109
+            
110
+        Returns:
111
+            配置值或默认值
112
+        """
113
+        try:
114
+            parts = key.split('.')
115
+            value = self.config_data
116
+            
117
+            for part in parts:
118
+                if isinstance(value, dict) and part in value:
119
+                    value = value[part]
120
+                else:
121
+                    return default
122
+                    
123
+            return value
124
+        except Exception as e:
125
+            logging.warning(f"获取配置项 {key} 时发生错误: {str(e)},使用默认值 {default}")
126
+            return default
127
+    
128
+    def set(self, key: str, value: Any) -> None:
129
+        """
130
+        设置配置值
131
+        
132
+        Args:
133
+            key: 配置键,支持点符号嵌套
134
+            value: 要设置的值
135
+        """
136
+        try:
137
+            parts = key.split('.')
138
+            config = self.config_data
139
+            
140
+            for i, part in enumerate(parts[:-1]):
141
+                if part not in config or not isinstance(config[part], dict):
142
+                    config[part] = {}
143
+                config = config[part]
144
+                
145
+            config[parts[-1]] = value
146
+            
147
+            # 保存配置到文件
148
+            with open(self.config_path, 'w', encoding='utf-8') as f:
149
+                yaml.safe_dump(self.config_data, f, allow_unicode=True, sort_keys=False)
150
+                
151
+        except Exception as e:
152
+            raise Exception(f"设置配置项 {key} 时发生错误: {str(e)}")
153
+
154
+# 全局配置实例
155
+config = Config()

BIN
config/__pycache__/__init__.cpython-310.pyc


+ 41 - 0
config/config.yaml

@@ -0,0 +1,41 @@
1
+# 数据库配置
2
+database:
3
+  # 数据库连接字符串
4
+  connection_string: "mysql+pymysql://root:800100@192.168.1.8/rasa_db?charset=utf8mb4"
5
+  # 连接池大小
6
+  pool_size: 10
7
+  # 连接超时时间(秒)
8
+  timeout: 30
9
+
10
+# 文件存储配置
11
+file_storage:
12
+  # Rasa配置文件生成目录
13
+  rasa_files_dir: "./rasa_files"
14
+  # 日志文件目录
15
+  log_dir: "./logs"
16
+  # 最大日志文件大小(MB)
17
+  max_log_size: 10
18
+  # 日志保留天数
19
+  log_retention_days: 30
20
+
21
+# Rasa服务配置
22
+rasa:
23
+  # Rasa服务器默认地址
24
+  server_url: "http://127.0.0.1:5005"
25
+  # 动作服务器默认地址
26
+  action_server_url: "http://127.0.0.1:5055"
27
+  # Rasa模型默认路径
28
+  default_model_path: "./models"
29
+  # 启动超时时间(秒)
30
+  startup_timeout: 60
31
+
32
+# API服务配置
33
+api:
34
+  # 服务绑定的主机地址
35
+  host: "0.0.0.0"
36
+  # 服务监听的端口
37
+  port: 8000
38
+  # 是否开启调试模式
39
+  debug: true
40
+  # API请求超时时间(秒)
41
+  request_timeout: 30

+ 8 - 0
db/__init__.py

@@ -0,0 +1,8 @@
1
+from db.connection import db, get_db
2
+from db.models import Base, Intent, IntentExample, Story, StoryStep, CustomAction, SystemConfig
3
+
4
+__all__ = [
5
+    'db', 'get_db', 'Base', 'Intent', 
6
+    'IntentExample', 'Story', 'StoryStep', 
7
+    'CustomAction', 'SystemConfig'
8
+]

BIN
db/__pycache__/__init__.cpython-310.pyc


BIN
db/__pycache__/connection.cpython-310.pyc


BIN
db/__pycache__/models.cpython-310.pyc


+ 86 - 0
db/connection.py

@@ -0,0 +1,86 @@
1
+import logging
2
+from sqlalchemy import create_engine
3
+from sqlalchemy.orm import sessionmaker, Session
4
+from sqlalchemy.exc import SQLAlchemyError
5
+from db.models import Base
6
+from config import config
7
+
8
+# 初始化日志
9
+logger = logging.getLogger(__name__)
10
+
11
+class Database:
12
+    """数据库连接管理类"""
13
+    
14
+    def __init__(self):
15
+        """初始化数据库连接"""
16
+        self.connection_string = config.get("database.connection_string")
17
+        self.pool_size = config.get("database.pool_size", 10)
18
+        self.timeout = config.get("database.timeout", 30)
19
+        
20
+        if not self.connection_string:
21
+            raise ValueError("数据库连接字符串未配置")
22
+            
23
+        try:
24
+            # 创建数据库引擎
25
+            self.engine = create_engine(
26
+                self.connection_string,
27
+                pool_size=self.pool_size,
28
+                pool_recycle=3600,  # 1小时后回收连接
29
+                connect_args={"connect_timeout": self.timeout}
30
+            )
31
+            
32
+            # 创建会话工厂
33
+            self.SessionLocal = sessionmaker(
34
+                autocommit=False,
35
+                autoflush=False,
36
+                bind=self.engine
37
+            )
38
+            
39
+            # 验证数据库连接
40
+            self._test_connection()
41
+            
42
+            logger.info("数据库连接初始化成功")
43
+            
44
+        except SQLAlchemyError as e:
45
+            logger.error(f"数据库连接初始化失败: {str(e)}")
46
+            raise Exception(f"数据库连接初始化失败: {str(e)}")
47
+        except Exception as e:
48
+            logger.error(f"初始化数据库时发生错误: {str(e)}")
49
+            raise Exception(f"初始化数据库时发生错误: {str(e)}")
50
+    
51
+    def _test_connection(self) -> None:
52
+        """测试数据库连接是否有效"""
53
+        try:
54
+            with self.engine.connect():
55
+                pass  # 连接成功
56
+        except SQLAlchemyError as e:
57
+            raise Exception(f"数据库连接测试失败: {str(e)}")
58
+    
59
+    def create_tables(self) -> None:
60
+        """创建数据库表"""
61
+        try:
62
+            Base.metadata.create_all(bind=self.engine)
63
+            logger.info("数据库表创建/验证成功")
64
+        except SQLAlchemyError as e:
65
+            logger.error(f"创建数据库表失败: {str(e)}")
66
+            raise Exception(f"创建数据库表失败: {str(e)}")
67
+    
68
+    def get_session(self) -> Session:
69
+        """获取数据库会话"""
70
+        session = self.SessionLocal()
71
+        try:
72
+            yield session
73
+        except SQLAlchemyError as e:
74
+            logger.error(f"数据库会话操作失败: {str(e)}")
75
+            session.rollback()
76
+            raise
77
+        finally:
78
+            session.close()
79
+
80
+# 全局数据库实例
81
+db = Database()
82
+
83
+# 用于FastAPI依赖的会话获取函数
84
+def get_db():
85
+    """FastAPI依赖项:获取数据库会话"""
86
+    yield from db.get_session()

+ 197 - 0
db/models.py

@@ -0,0 +1,197 @@
1
+import logging
2
+import json
3
+from datetime import datetime
4
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, UniqueConstraint, Index
5
+from sqlalchemy.ext.declarative import declarative_base
6
+from sqlalchemy.orm import relationship
7
+from sqlalchemy.exc import SQLAlchemyError
8
+
9
+# 初始化日志
10
+logger = logging.getLogger(__name__)
11
+
12
+Base = declarative_base()
13
+
14
+class Intent(Base):
15
+    """意图表模型"""
16
+    __tablename__ = 'intent'
17
+    
18
+    id = Column(Integer, primary_key=True, autoincrement=True, comment='意图ID')
19
+    name = Column(String(100), nullable=False, unique=True, comment='意图名称')
20
+    description = Column(String(500), comment='意图描述')
21
+    created_time = Column(DateTime, default=datetime.now, nullable=False, comment='创建时间')
22
+    updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
23
+    
24
+    examples = relationship('IntentExample', backref='intent', cascade='all, delete-orphan')
25
+    
26
+    __table_args__ = {'comment': '存储Rasa意图定义'}
27
+
28
+class IntentExample(Base):
29
+    """意图样本表模型"""
30
+    __tablename__ = 'intent_example'
31
+    
32
+    id = Column(Integer, primary_key=True, autoincrement=True, comment='样本ID')
33
+    intent_id = Column(Integer, ForeignKey('intent.id'), nullable=False, comment='意图ID')
34
+    text = Column(String(1000), nullable=False, comment='样本文本')
35
+    created_time = Column(DateTime, default=datetime.now, nullable=False, comment='创建时间')
36
+    updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
37
+
38
+    
39
+    __table_args__ = {'comment': '存储意图对应的用户输入示例'}
40
+
41
+class Story(Base):
42
+    """故事表模型"""
43
+    __tablename__ = 'story'
44
+    
45
+    id = Column(Integer, primary_key=True, autoincrement=True, comment='故事ID')
46
+    name = Column(String(200), nullable=False, unique=True, comment='故事名称')
47
+    description = Column(String(500), comment='故事描述')
48
+    created_time = Column(DateTime, default=datetime.now, nullable=False, comment='创建时间')
49
+    updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
50
+    
51
+    # 关联关系
52
+    steps = relationship('StoryStep', backref='story', cascade='all, delete-orphan')
53
+    
54
+    __table_args__ = {'comment': '存储Rasa对话流程定义'}
55
+
56
+class StoryStep(Base):
57
+    """故事步骤表模型"""
58
+    __tablename__ = 'story_step'
59
+    
60
+    id = Column(Integer, primary_key=True, autoincrement=True, comment='步骤ID')
61
+    story_id = Column(Integer, ForeignKey('story.id'), nullable=False, comment='故事ID')
62
+    step_order = Column(Integer, nullable=False, comment='步骤顺序')
63
+    step_type = Column(String(20), nullable=False, comment='步骤类型')
64
+    content = Column(Text, nullable=False, comment='步骤内容(JSON)')
65
+    created_time = Column(DateTime, default=datetime.now, nullable=False, comment='创建时间')
66
+    updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
67
+
68
+    
69
+    @property
70
+    def content_dict(self):
71
+        """将JSON字符串转换为字典"""
72
+        try:
73
+            if not self.content:
74
+                return {}
75
+            return json.loads(self.content)
76
+        except json.JSONDecodeError as e:
77
+            logger.error(f"解析StoryStep内容失败: {str(e)}, 内容: {self.content}")
78
+            return {}
79
+    
80
+    __table_args__ = {'comment': '存储故事的具体步骤'}
81
+
82
+class CustomAction(Base):
83
+    """自定义动作表模型"""
84
+    __tablename__ = 'custom_action'
85
+    
86
+    id = Column(Integer, primary_key=True, autoincrement=True, comment='动作ID')
87
+    name = Column(String(100), nullable=False, unique=True, comment='动作名称')
88
+    description = Column(String(500), comment='动作描述')
89
+    http_method = Column(String(10), nullable=False, comment='HTTP方法')
90
+    api_url = Column(String(500), nullable=False, comment='API地址')
91
+    token = Column(String(500), comment='鉴权Token')
92
+    headers = Column(Text, comment='请求头(JSON)')
93
+    request_body = Column(Text, comment='请求体(JSON)')
94
+    response_mapping = Column(Text, comment='响应映射规则(JSON)')
95
+    created_time = Column(DateTime, default=datetime.now, nullable=False, comment='创建时间')
96
+    updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
97
+    
98
+    @property
99
+    def headers_dict(self):
100
+        """将请求头JSON转换为字典"""
101
+        try:
102
+            if not self.headers:
103
+                return {}
104
+            return json.loads(self.headers)
105
+        except json.JSONDecodeError as e:
106
+            logger.error(f"解析CustomAction请求头失败: {str(e)}, 内容: {self.headers}")
107
+            return {}
108
+    
109
+    @property
110
+    def request_body_dict(self):
111
+        """将请求体JSON转换为字典"""
112
+        try:
113
+            if not self.request_body:
114
+                return {}
115
+            return json.loads(self.request_body)
116
+        except json.JSONDecodeError as e:
117
+            logger.error(f"解析CustomAction请求体失败: {str(e)}, 内容: {self.request_body}")
118
+            return {}
119
+    
120
+    @property
121
+    def response_mapping_dict(self):
122
+        """将响应映射JSON转换为字典"""
123
+        try:
124
+            if not self.response_mapping:
125
+                return {}
126
+            return json.loads(self.response_mapping)
127
+        except json.JSONDecodeError as e:
128
+            logger.error(f"解析CustomAction响应映射失败: {str(e)}, 内容: {self.response_mapping}")
129
+            return {}
130
+    
131
+    __table_args__ = {'comment': '存储对接第三方API的自定义动作配置'}
132
+
133
+class SystemConfig(Base):
134
+    """系统配置表模型"""
135
+    __tablename__ = 'system_config'
136
+    
137
+    id = Column(Integer, primary_key=True, autoincrement=True, comment='配置ID')
138
+    config_key = Column(String(100), nullable=False, unique=True, comment='配置键')
139
+    config_value = Column(String(500), nullable=False, comment='配置值')
140
+    description = Column(String(500), comment='配置描述')
141
+    updated_by = Column(String(100), comment='更新人')
142
+    updated_time = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment='更新时间')
143
+    
144
+    __table_args__ = {'comment': '存储系统配置信息'}
145
+    
146
+    @classmethod
147
+    def get_value(cls, session, key, default=None):
148
+        """
149
+        获取配置值
150
+        
151
+        Args:
152
+            session: 数据库会话
153
+            key: 配置键
154
+            default: 默认值
155
+            
156
+        Returns:
157
+            配置值或默认值
158
+        """
159
+        try:
160
+            config = session.query(cls).filter_by(config_key=key).first()
161
+            return config.config_value if config else default
162
+        except SQLAlchemyError as e:
163
+            logger.error(f"获取系统配置 {key} 失败: {str(e)}")
164
+            return default
165
+    
166
+    @classmethod
167
+    def set_value(cls, session, key, value, user=None):
168
+        """
169
+        设置配置值
170
+        
171
+        Args:
172
+            session: 数据库会话
173
+            key: 配置键
174
+            value: 配置值
175
+            user: 操作人
176
+            
177
+        Returns:
178
+            配置对象
179
+        """
180
+        try:
181
+            config = session.query(cls).filter_by(config_key=key).first()
182
+            if config:
183
+                config.config_value = value
184
+                config.updated_by = user
185
+            else:
186
+                config = cls(
187
+                    config_key=key,
188
+                    config_value=value,
189
+                    updated_by=user
190
+                )
191
+                session.add(config)
192
+            session.commit()
193
+            return config
194
+        except SQLAlchemyError as e:
195
+            session.rollback()
196
+            logger.error(f"设置系统配置 {key} 失败: {str(e)}")
197
+            raise Exception(f"设置系统配置失败: {str(e)}")

+ 33 - 0
redme.md

@@ -0,0 +1,33 @@
1
+# 安装依赖
2
+```
3
+pip install -r requirements.txt
4
+```
5
+
6
+# 初始化数据库
7
+```
8
+# 从项目根目录执行
9
+python -m scripts.init_db
10
+```
11
+
12
+# 启动服务命令
13
+```
14
+uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
15
+```
16
+
17
+# 启动 Rasa 相关服务
18
+```
19
+# 启动Rasa核心服务(使用生成的配置文件)
20
+rasa run --enable-api --cors "*" --model ./rasa_files/models --config ./rasa_files/config.yml
21
+
22
+# 启动Rasa动作服务器(使用生成的自定义动作)
23
+rasa run actions --actions actions --actions-dir ./rasa_files/actions
24
+```
25
+
26
+# 生产环境启动建议
27
+```
28
+# 安装进程管理工具
29
+pip install gunicorn
30
+
31
+# 启动命令(4个工作进程,绑定8000端口)
32
+gunicorn api.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
33
+```

+ 7 - 0
requirements.txt

@@ -0,0 +1,7 @@
1
+fastapi==0.95.0
2
+uvicorn==0.21.1
3
+sqlalchemy==2.0.9
4
+pymysql==1.0.2
5
+pyyaml==6.0
6
+requests==2.28.2
7
+python-dotenv==1.0.0

+ 1 - 0
scripts/__init__.py

@@ -0,0 +1 @@
1
+"""辅助脚本模块"""

BIN
scripts/__pycache__/__init__.cpython-310.pyc


BIN
scripts/__pycache__/init_db.cpython-310.pyc


+ 63 - 0
scripts/init_db.py

@@ -0,0 +1,63 @@
1
+import logging
2
+import os
3
+import sys
4
+from sqlalchemy import create_engine
5
+from sqlalchemy.exc import SQLAlchemyError
6
+
7
+# 添加项目根目录到Python路径
8
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
+
10
+from config import config
11
+from db import Base
12
+
13
+# 初始化日志
14
+logging.basicConfig(level=logging.INFO)
15
+logger = logging.getLogger(__name__)
16
+
17
+def init_database():
18
+    """初始化数据库:创建表结构并执行初始SQL脚本"""
19
+    try:
20
+        # 获取数据库连接字符串
21
+        connection_string = config.get("database.connection_string")
22
+        if not connection_string:
23
+            raise ValueError("数据库连接字符串未配置")
24
+        
25
+        # 创建数据库引擎
26
+        engine = create_engine(connection_string)
27
+        
28
+        # 创建所有表
29
+        logger.info("开始创建数据库表结构...")
30
+        Base.metadata.create_all(engine)
31
+        logger.info("数据库表结构创建成功")
32
+        
33
+        # 执行SQL初始化脚本
34
+        sql_script_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "sql", "schema.sql")
35
+        if os.path.exists(sql_script_path):
36
+            logger.info(f"开始执行初始化SQL脚本: {sql_script_path}")
37
+            
38
+            with engine.connect() as connection:
39
+                with open(sql_script_path, 'r', encoding='utf-8') as f:
40
+                    # 读取并执行SQL脚本
41
+                    sql = f.read()
42
+                    # 分割SQL语句(简单处理,适用于大多数情况)
43
+                    statements = sql.split(';')
44
+                    for stmt in statements:
45
+                        stmt = stmt.strip()
46
+                        if stmt and not stmt.startswith('--'):
47
+                            connection.execute(stmt)
48
+                connection.commit()
49
+            logger.info("SQL初始化脚本执行成功")
50
+        else:
51
+            logger.warning(f"未找到SQL初始化脚本: {sql_script_path}")
52
+        
53
+        logger.info("数据库初始化完成")
54
+        
55
+    except SQLAlchemyError as e:
56
+        logger.error(f"数据库操作错误: {str(e)}")
57
+        sys.exit(1)
58
+    except Exception as e:
59
+        logger.error(f"初始化数据库失败: {str(e)}")
60
+        sys.exit(1)
61
+
62
+if __name__ == "__main__":
63
+    init_database()

+ 4 - 0
services/__init__.py

@@ -0,0 +1,4 @@
1
+from services.rasa_generator import RasaFileGenerator
2
+from services.rasa_manager import RasaManager
3
+
4
+__all__ = ['RasaFileGenerator', 'RasaManager']

BIN
services/__pycache__/__init__.cpython-310.pyc


BIN
services/__pycache__/rasa_generator.cpython-310.pyc


BIN
services/__pycache__/rasa_generator_by_json.cpython-310.pyc


BIN
services/__pycache__/rasa_manager.cpython-310.pyc


BIN
services/__pycache__/version_service.cpython-310.pyc


+ 576 - 0
services/flow.json

@@ -0,0 +1,576 @@
1
+{
2
+    "flowName": "测试",
3
+    "flowId": "1",
4
+    "flowJson": {
5
+        "nodes": [
6
+            {
7
+                "id": "1755743973598277318",
8
+                "type": "start",
9
+                "x": 340,
10
+                "y": 600,
11
+                "properties": {
12
+                    "name": "开始",
13
+                    "desc": "酒店预订流程开始",
14
+                    "frontend_status": "1"
15
+                }
16
+            },
17
+            {
18
+                "id": "1755744073948164528",
19
+                "type": "intention",
20
+                "x": 340,
21
+                "y": 780,
22
+                "properties": {
23
+                    "name": "用户预定酒店",
24
+                    "desc": "用户发起预定酒店",
25
+                    "frontend_status": "1",
26
+                    "code": "book_hotel",
27
+                    "samples": [
28
+                        {
29
+                            "text": "我想订酒店",
30
+                            "entities": []
31
+                        },
32
+                        {
33
+                            "text": "我想定明天的酒店",
34
+                            "entities": [
35
+                                {
36
+                                    "text": "明天",
37
+                                    "label": "check_in_out",
38
+                                    "start": 3,
39
+                                    "end": 4
40
+                                }
41
+                            ]
42
+                        },
43
+                        {
44
+                            "text": "帮我订一间房",
45
+                            "entities": []
46
+                        }
47
+                    ]
48
+                }
49
+            },
50
+            {
51
+                "id": "1755744170948999578",
52
+                "type": "action",
53
+                "x": 340,
54
+                "y": 960,
55
+                "properties": {
56
+                    "name": "确定开始预定回复",
57
+                    "desc": "回复预定动作",
58
+                    "frontend_status": "1",
59
+                    "code": "confirm_booking",
60
+                    "configText": "好的,我将为您办理酒店预定,请提供一下信息:"
61
+                }
62
+            },
63
+            {
64
+                "id": "1755744259460386308",
65
+                "type": "collection",
66
+                "x": 340,
67
+                "y": 1140,
68
+                "properties": {
69
+                    "name": "酒店预定表单",
70
+                    "desc": "酒店预定表单信息采集",
71
+                    "frontend_status": "1",
72
+                    "code": "hotel",
73
+                    "formFields": [
74
+                        {
75
+                            "slotName": "",
76
+                            "entityType": "check_in_out",
77
+                            "question": "请问您的入住日期是?",
78
+                            "validation": "date",
79
+                            "customRegex": "",
80
+                            "required": true,
81
+                            "retryMessage": "请输入正确的时间格式"
82
+                        }
83
+                    ],
84
+                    "submitIntent": "",
85
+                    "cancelIntent": "",
86
+                    "completionMessage": ""
87
+                }
88
+            },
89
+            {
90
+                "id": "1755744367061997285",
91
+                "type": "form",
92
+                "x": 340,
93
+                "y": 1320,
94
+                "properties": {
95
+                    "name": "获取会员信息",
96
+                    "desc": "调用接口获取会员信息",
97
+                    "frontend_status": "0",
98
+                    "anchors": [
99
+                        {
100
+                            "id": "1755744368596992170",
101
+                            "text": "成功",
102
+                            "checked": true,
103
+                            "isDefault": true
104
+                        },
105
+                        {
106
+                            "id": "1755744368596493642",
107
+                            "text": "失败",
108
+                            "checked": true,
109
+                            "isDefault": true
110
+                        }
111
+                    ],
112
+                    "code": "get_vipinfo",
113
+                    "requestMethod": "GET",
114
+                    "requestUrl": "http://localhost:3400",
115
+                    "headers": [
116
+                        {
117
+                            "key": "token",
118
+                            "value": "123"
119
+                        }
120
+                    ],
121
+                    "params": [
122
+                        {
123
+                            "name": "phone",
124
+                            "remark": "手机号",
125
+                            "type": "string",
126
+                            "entity": "event",
127
+                            "required": true,
128
+                            "placeholder": ""
129
+                        }
130
+                    ],
131
+                    "responseMappings": [
132
+                        {
133
+                            "responseField": "data[0].vip",
134
+                            "targetVar": "is_vip",
135
+                            "defaultValue": "false"
136
+                        }
137
+                    ]
138
+                }
139
+            },
140
+            {
141
+                "id": "1755744802261201276",
142
+                "type": "condition",
143
+                "x": 240,
144
+                "y": 1540,
145
+                "properties": {
146
+                    "name": "判断是否是会员",
147
+                    "desc": "",
148
+                    "frontend_status": "1",
149
+                    "code": "condition_ismember",
150
+                    "configText": "",
151
+                    "entities": [],
152
+                    "conditionGroups": [
153
+                        {
154
+                            "name": "是会员",
155
+                            "operator": "and",
156
+                            "conditions": [
157
+                                {
158
+                                    "type": "slot",
159
+                                    "slotName": "is_vip",
160
+                                    "intentName": "",
161
+                                    "operator": "==",
162
+                                    "value": "true"
163
+                                }
164
+                            ]
165
+                        }
166
+                    ],
167
+                    "anchors": [
168
+                        {
169
+                            "id": "1755744870669973945",
170
+                            "text": "A",
171
+                            "tooltip": "是会员",
172
+                            "checked": true,
173
+                            "isDefault": true
174
+                        },
175
+                        {
176
+                            "id": "1755744870669360142",
177
+                            "text": "其他",
178
+                            "tooltip": "未匹配到规则",
179
+                            "checked": true,
180
+                            "isDefault": true
181
+                        }
182
+                    ]
183
+                }
184
+            },
185
+            {
186
+                "id": "1755745052943625022",
187
+                "type": "action",
188
+                "x": 190,
189
+                "y": 1740,
190
+                "properties": {
191
+                    "name": "会员确定预定提醒",
192
+                    "desc": "",
193
+                    "frontend_status": "1",
194
+                    "code": "submit_comfirm_vip",
195
+                    "configText": "尊敬的会员您好,已成功为您预定<date|日期>的房间"
196
+                }
197
+            },
198
+            {
199
+                "id": "1755745287568590291",
200
+                "type": "action",
201
+                "x": 560,
202
+                "y": 1710,
203
+                "properties": {
204
+                    "name": "普通用户确定预定回复",
205
+                    "desc": "",
206
+                    "frontend_status": "1",
207
+                    "code": "submit_confirm_novip",
208
+                    "configText": "好的,已经为您预定成功,感谢您的支持,祝您生活愉快!"
209
+                }
210
+            },
211
+            {
212
+                "id": "1755745352034663098",
213
+                "type": "end",
214
+                "x": 400,
215
+                "y": 1980,
216
+                "properties": {
217
+                    "name": "结束",
218
+                    "desc": "",
219
+                    "frontend_status": "0"
220
+                }
221
+            }
222
+        ],
223
+        "edges": [
224
+            {
225
+                "id": "1755744156551690020",
226
+                "type": "myBezier",
227
+                "sourceNodeId": "1755743973598277318",
228
+                "targetNodeId": "1755744073948164528",
229
+                "startPoint": {
230
+                    "x": 340,
231
+                    "y": 650
232
+                },
233
+                "endPoint": {
234
+                    "x": 340,
235
+                    "y": 730
236
+                },
237
+                "properties": {
238
+                    "edgeType": "start"
239
+                },
240
+                "pointsList": [
241
+                    {
242
+                        "x": 340,
243
+                        "y": 650
244
+                    },
245
+                    {
246
+                        "x": 340,
247
+                        "y": 750
248
+                    },
249
+                    {
250
+                        "x": 340,
251
+                        "y": 630
252
+                    },
253
+                    {
254
+                        "x": 340,
255
+                        "y": 730
256
+                    }
257
+                ]
258
+            },
259
+            {
260
+                "id": "1755744175271745566",
261
+                "type": "myBezier",
262
+                "sourceNodeId": "1755744073948164528",
263
+                "targetNodeId": "1755744170948999578",
264
+                "startPoint": {
265
+                    "x": 340,
266
+                    "y": 830
267
+                },
268
+                "endPoint": {
269
+                    "x": 340,
270
+                    "y": 910
271
+                },
272
+                "properties": {
273
+                    "edgeType": "nextStep"
274
+                },
275
+                "pointsList": [
276
+                    {
277
+                        "x": 340,
278
+                        "y": 830
279
+                    },
280
+                    {
281
+                        "x": 340,
282
+                        "y": 930
283
+                    },
284
+                    {
285
+                        "x": 340,
286
+                        "y": 810
287
+                    },
288
+                    {
289
+                        "x": 340,
290
+                        "y": 910
291
+                    }
292
+                ]
293
+            },
294
+            {
295
+                "id": "1755744264519257586",
296
+                "type": "myBezier",
297
+                "sourceNodeId": "1755744170948999578",
298
+                "targetNodeId": "1755744259460386308",
299
+                "startPoint": {
300
+                    "x": 340,
301
+                    "y": 1010
302
+                },
303
+                "endPoint": {
304
+                    "x": 340,
305
+                    "y": 1090
306
+                },
307
+                "properties": {
308
+                    "edgeType": "nextStep"
309
+                },
310
+                "pointsList": [
311
+                    {
312
+                        "x": 340,
313
+                        "y": 1010
314
+                    },
315
+                    {
316
+                        "x": 340,
317
+                        "y": 1110
318
+                    },
319
+                    {
320
+                        "x": 340,
321
+                        "y": 990
322
+                    },
323
+                    {
324
+                        "x": 340,
325
+                        "y": 1090
326
+                    }
327
+                ]
328
+            },
329
+            {
330
+                "id": "1755744373342277013",
331
+                "type": "myBezier",
332
+                "sourceNodeId": "1755744259460386308",
333
+                "targetNodeId": "1755744367061997285",
334
+                "startPoint": {
335
+                    "x": 340,
336
+                    "y": 1190
337
+                },
338
+                "endPoint": {
339
+                    "x": 340,
340
+                    "y": 1270
341
+                },
342
+                "properties": {
343
+                    "edgeType": "nextStep"
344
+                },
345
+                "pointsList": [
346
+                    {
347
+                        "x": 340,
348
+                        "y": 1190
349
+                    },
350
+                    {
351
+                        "x": 340,
352
+                        "y": 1290
353
+                    },
354
+                    {
355
+                        "x": 340,
356
+                        "y": 1170
357
+                    },
358
+                    {
359
+                        "x": 340,
360
+                        "y": 1270
361
+                    }
362
+                ]
363
+            },
364
+            {
365
+                "id": "1755744805233383108",
366
+                "type": "myBezier",
367
+                "sourceNodeId": "1755744367061997285",
368
+                "targetNodeId": "1755744802261201276",
369
+                "startPoint": {
370
+                    "x": 234,
371
+                    "y": 1370
372
+                },
373
+                "endPoint": {
374
+                    "x": 240,
375
+                    "y": 1490
376
+                },
377
+                "properties": {
378
+                    "edgeType": "nextStep"
379
+                },
380
+                "pointsList": [
381
+                    {
382
+                        "x": 234,
383
+                        "y": 1370
384
+                    },
385
+                    {
386
+                        "x": 234,
387
+                        "y": 1470
388
+                    },
389
+                    {
390
+                        "x": 240,
391
+                        "y": 1390
392
+                    },
393
+                    {
394
+                        "x": 240,
395
+                        "y": 1490
396
+                    }
397
+                ]
398
+            },
399
+            {
400
+                "id": "1755745059414490384",
401
+                "type": "myBezier",
402
+                "sourceNodeId": "1755744802261201276",
403
+                "targetNodeId": "1755745052943625022",
404
+                "startPoint": {
405
+                    "x": 126.5,
406
+                    "y": 1590
407
+                },
408
+                "endPoint": {
409
+                    "x": 190,
410
+                    "y": 1690
411
+                },
412
+                "properties": {
413
+                    "edgeType": "nextStep"
414
+                },
415
+                "pointsList": [
416
+                    {
417
+                        "x": 126.5,
418
+                        "y": 1590
419
+                    },
420
+                    {
421
+                        "x": 126.5,
422
+                        "y": 1690
423
+                    },
424
+                    {
425
+                        "x": 190,
426
+                        "y": 1590
427
+                    },
428
+                    {
429
+                        "x": 190,
430
+                        "y": 1690
431
+                    }
432
+                ]
433
+            },
434
+            {
435
+                "id": "1755745293386979251",
436
+                "type": "myBezier",
437
+                "sourceNodeId": "1755744802261201276",
438
+                "targetNodeId": "1755745287568590291",
439
+                "startPoint": {
440
+                    "x": 157,
441
+                    "y": 1590
442
+                },
443
+                "endPoint": {
444
+                    "x": 560,
445
+                    "y": 1660
446
+                },
447
+                "properties": {
448
+                    "edgeType": "nextStep"
449
+                },
450
+                "pointsList": [
451
+                    {
452
+                        "x": 157,
453
+                        "y": 1590
454
+                    },
455
+                    {
456
+                        "x": 157,
457
+                        "y": 1690
458
+                    },
459
+                    {
460
+                        "x": 560,
461
+                        "y": 1560
462
+                    },
463
+                    {
464
+                        "x": 560,
465
+                        "y": 1660
466
+                    }
467
+                ]
468
+            },
469
+            {
470
+                "id": "1755745297779860170",
471
+                "type": "myBezier",
472
+                "sourceNodeId": "1755744367061997285",
473
+                "targetNodeId": "1755745287568590291",
474
+                "startPoint": {
475
+                    "x": 272,
476
+                    "y": 1370
477
+                },
478
+                "endPoint": {
479
+                    "x": 560,
480
+                    "y": 1660
481
+                },
482
+                "properties": {
483
+                    "edgeType": "nextStep"
484
+                },
485
+                "pointsList": [
486
+                    {
487
+                        "x": 272,
488
+                        "y": 1370
489
+                    },
490
+                    {
491
+                        "x": 272,
492
+                        "y": 1470
493
+                    },
494
+                    {
495
+                        "x": 560,
496
+                        "y": 1560
497
+                    },
498
+                    {
499
+                        "x": 560,
500
+                        "y": 1660
501
+                    }
502
+                ]
503
+            },
504
+            {
505
+                "id": "1755745356697455012",
506
+                "type": "myBezier",
507
+                "sourceNodeId": "1755745052943625022",
508
+                "targetNodeId": "1755745352034663098",
509
+                "startPoint": {
510
+                    "x": 190,
511
+                    "y": 1790
512
+                },
513
+                "endPoint": {
514
+                    "x": 400,
515
+                    "y": 1930
516
+                },
517
+                "properties": {
518
+                    "edgeType": "nextStep"
519
+                },
520
+                "pointsList": [
521
+                    {
522
+                        "x": 190,
523
+                        "y": 1790
524
+                    },
525
+                    {
526
+                        "x": 190,
527
+                        "y": 1890
528
+                    },
529
+                    {
530
+                        "x": 400,
531
+                        "y": 1830
532
+                    },
533
+                    {
534
+                        "x": 400,
535
+                        "y": 1930
536
+                    }
537
+                ]
538
+            },
539
+            {
540
+                "id": "1755745359943951336",
541
+                "type": "myBezier",
542
+                "sourceNodeId": "1755745287568590291",
543
+                "targetNodeId": "1755745352034663098",
544
+                "startPoint": {
545
+                    "x": 560,
546
+                    "y": 1760
547
+                },
548
+                "endPoint": {
549
+                    "x": 400,
550
+                    "y": 1930
551
+                },
552
+                "properties": {
553
+                    "edgeType": "nextStep"
554
+                },
555
+                "pointsList": [
556
+                    {
557
+                        "x": 560,
558
+                        "y": 1760
559
+                    },
560
+                    {
561
+                        "x": 560,
562
+                        "y": 1860
563
+                    },
564
+                    {
565
+                        "x": 400,
566
+                        "y": 1830
567
+                    },
568
+                    {
569
+                        "x": 400,
570
+                        "y": 1930
571
+                    }
572
+                ]
573
+            }
574
+        ]
575
+    }
576
+}

+ 414 - 0
services/rasa_generator.py

@@ -0,0 +1,414 @@
1
+import os
2
+import json
3
+import logging
4
+from pathlib import Path
5
+from sqlalchemy.orm import Session
6
+from sqlalchemy.exc import SQLAlchemyError
7
+from db import (
8
+    Intent, IntentExample, Story, StoryStep, CustomAction,
9
+    SystemConfig
10
+)
11
+
12
+from config import config
13
+
14
+# 初始化日志
15
+logger = logging.getLogger(__name__)
16
+
17
+class RasaFileGenerator:
18
+    """Rasa配置文件生成服务,带完整异常处理"""
19
+    
20
+    def __init__(self):
21
+        """初始化生成器,从配置获取输出目录"""
22
+        try:
23
+            self.output_dir = config.get("file_storage.rasa_files_dir", "./rasa_files")
24
+            
25
+            # 创建输出目录
26
+            self._create_directories()
27
+            
28
+            logger.info(f"Rasa文件生成器初始化成功,输出目录: {self.output_dir}")
29
+            
30
+        except Exception as e:
31
+            logger.error(f"初始化Rasa文件生成器失败: {str(e)}")
32
+            raise Exception(f"初始化Rasa文件生成器失败: {str(e)}")
33
+    
34
+    def _create_directories(self) -> None:
35
+        """创建必要的目录结构"""
36
+        try:
37
+            Path(self.output_dir).mkdir(parents=True, exist_ok=True)
38
+            Path(f"{self.output_dir}/nlu").mkdir(parents=True, exist_ok=True)
39
+            Path(f"{self.output_dir}/stories").mkdir(parents=True, exist_ok=True)
40
+            Path(f"{self.output_dir}/actions").mkdir(parents=True, exist_ok=True)
41
+        except OSError as e:
42
+            raise Exception(f"创建目录失败: {str(e)}")
43
+    
44
+    def generate_all_files(self, session: Session) -> dict:
45
+        """
46
+        生成所有Rasa配置文件
47
+        
48
+        Args:
49
+            session: 数据库会话
50
+            
51
+        Returns:
52
+            生成结果,包含文件路径和状态
53
+        """
54
+        try:
55
+            
56
+            # 生成各文件
57
+            domain_path = self.generate_domain_file(session)
58
+            nlu_path = self.generate_nlu_file(session)
59
+            story_paths = self.generate_stories_files(session)
60
+            action_path = self.generate_actions_file(session)
61
+            
62
+            
63
+            return {
64
+                "success": True,
65
+                "message": "所有文件生成成功",
66
+                "output_dir": self.output_dir,
67
+                "files": {
68
+                    "domain": domain_path,
69
+                    "nlu": nlu_path,
70
+                    "stories": story_paths,
71
+                    "actions": action_path
72
+                }
73
+            }
74
+        except SQLAlchemyError as e:
75
+            logger.error(f"数据库错误导致文件生成失败: {str(e)}")
76
+            return {
77
+                "success": False,
78
+                "message": f"数据库错误: {str(e)}",
79
+            }
80
+        except OSError as e:
81
+            logger.error(f"文件系统错误导致文件生成失败: {str(e)}")
82
+            return {
83
+                "success": False,
84
+                "message": f"文件系统错误: {str(e)}",
85
+            }
86
+        except Exception as e:
87
+            logger.error(f"文件生成失败: {str(e)}")
88
+            return {
89
+                "success": False,
90
+                "message": f"生成文件时发生错误: {str(e)}",
91
+            }
92
+    
93
+    def generate_domain_file(self, session: Session) -> str:
94
+        """生成domain.yml文件"""
95
+        try:
96
+            domain_data = {
97
+                "version": "3.1",
98
+                "intents": [],
99
+                "slots": {},
100
+                "forms": {},
101
+                "responses": {},
102
+                "actions": []
103
+            }
104
+            
105
+            # 获取指定版本的意图
106
+            intents = session.query(Intent).all()
107
+            
108
+            if not intents:
109
+                logger.warning(f"未找到任何意图")
110
+            
111
+            # 添加意图
112
+            domain_data["intents"] = [intent.name for intent in intents]
113
+            
114
+            # 获取指定版本的自定义动作
115
+            actions = session.query(CustomAction).all()
116
+            
117
+            # 添加动作
118
+            domain_data["actions"] = [action.name for action in actions]
119
+            
120
+            # 实际应用中还需要添加槽位、表单和响应
121
+            
122
+            
123
+            # 写入文件
124
+            file_path = f"{self.output_dir}/domain.yml"
125
+            with open(file_path, "w", encoding="utf-8") as f:
126
+                self._write_yaml(f, domain_data)
127
+            
128
+            logger.info(f"生成domain文件: {file_path}")
129
+            return file_path
130
+            
131
+        except Exception as e:
132
+            logger.error(f"生成domain文件失败: {str(e)}")
133
+            raise Exception(f"生成domain文件失败: {str(e)}")
134
+    
135
+    def generate_nlu_file(self, session: Session) -> str:
136
+        """生成nlu.yml文件"""
137
+        try:
138
+            nlu_data = {
139
+                "version": "3.1",
140
+                "nlu": []
141
+            }
142
+            
143
+            # 获取指定版本的意图及样本
144
+            intents = session.query(Intent).all()
145
+            
146
+            for intent in intents:
147
+                # 获取该意图指定版本的样本
148
+                examples = session.query(IntentExample).filter(
149
+                    IntentExample.intent_id == intent.id,
150
+                ).all()
151
+                
152
+                # 构建examples字符串,使用正确的YAML多行文本格式
153
+                examples_str = "\n".join([f"      - {ex.text}" for ex in examples])
154
+                
155
+
156
+                intent_entry = {
157
+                    "intent": intent.name,
158
+                    "examples": examples_str
159
+                }
160
+                
161
+                if intent.description:
162
+                    intent_entry["intent"] = f"{intent.name} # {intent.description}"
163
+
164
+                nlu_data["nlu"].append(intent_entry)
165
+            
166
+            # 写入文件
167
+            file_path = f"{self.output_dir}/nlu/nlu.yml"
168
+            with open(file_path, "w", encoding="utf-8") as f:
169
+                self._write_yaml(f, nlu_data)
170
+            
171
+            logger.info(f"生成nlu文件: {file_path}")
172
+            return file_path
173
+            
174
+        except Exception as e:
175
+            logger.error(f"生成nlu文件失败: {str(e)}")
176
+            raise Exception(f"生成nlu文件失败: {str(e)}")
177
+    
178
+    def generate_stories_files(self, session: Session) -> list[str]:
179
+        """生成stories文件"""
180
+        try:
181
+            files = []
182
+            
183
+            # 获取指定版本的故事
184
+            stories = session.query(Story).all()
185
+            
186
+            if not stories:
187
+                logger.warning(f"未找到任何故事")
188
+            
189
+            # 生成合并的stories.yml
190
+            all_stories = {
191
+                "version": "3.1",
192
+                "stories": []
193
+            }
194
+            
195
+            for story in stories:
196
+                # 获取故事步骤
197
+                steps = session.query(StoryStep).filter(
198
+                    StoryStep.story_id == story.id
199
+                ).order_by(StoryStep.step_order).all()
200
+                
201
+                # 构建故事内容
202
+                story_entry = {
203
+                    "story": story.name,
204
+                    "steps": []
205
+                }
206
+                
207
+                if story.description:
208
+                    story_entry["story"] = f"{story.name} # {story.description}"
209
+                    
210
+                # 处理步骤
211
+                for step in steps:
212
+                    content = step.content_dict
213
+                    step_type = step.step_type
214
+                    
215
+                    if step_type == "intent":
216
+                        story_entry["steps"].append({"intent": content.get("name")})
217
+                    elif step_type == "action":
218
+                        story_entry["steps"].append({"action": content.get("name")})
219
+                    elif step_type == "form":
220
+                        if content.get("activate", True):
221
+                            story_entry["steps"].append({"action": content.get("name")})
222
+                        else:
223
+                            story_entry["steps"].append({"action": f"form_deactivate_{content.get('name')}"})
224
+                    # 处理其他类型的步骤...
225
+                    
226
+                all_stories["stories"].append(story_entry)
227
+                
228
+                # 生成单个故事文件
229
+                story_file_name = story.name.lower().replace(" ", "_") + ".yml"
230
+                story_file_path = f"{self.output_dir}/stories/{story_file_name}"
231
+                
232
+                with open(story_file_path, "w", encoding="utf-8") as f:
233
+                    self._write_yaml(f, {
234
+                        "version": "3.1",
235
+                        "stories": [story_entry]
236
+                    })
237
+                
238
+                files.append(story_file_path)
239
+            
240
+            # 写入合并的stories.yml
241
+            merged_file_path = f"{self.output_dir}/stories/stories.yml"
242
+            with open(merged_file_path, "w", encoding="utf-8") as f:
243
+                self._write_yaml(f, all_stories)
244
+            
245
+            files.append(merged_file_path)
246
+            logger.info(f"生成stories文件: {len(files)} 个文件")
247
+            return files
248
+            
249
+        except Exception as e:
250
+            logger.error(f"生成stories文件失败: {str(e)}")
251
+            raise Exception(f"生成stories文件失败: {str(e)}")
252
+    
253
+    def generate_actions_file(self, session: Session) -> str:
254
+        """生成自定义动作Python文件"""
255
+        try:
256
+            actions = session.query(CustomAction).all()
257
+            
258
+            if not actions:
259
+                logger.warning(f"未找到任何自定义动作")
260
+            
261
+            # 生成actions.py内容
262
+            code = [
263
+                "from rasa_sdk import Action, Tracker",
264
+                "from rasa_sdk.executor import CollectingDispatcher",
265
+                "from rasa_sdk.events import SlotSet",
266
+                "import requests",
267
+                "import json\n"
268
+            ]
269
+            
270
+            for action in actions:
271
+                class_name = f"Action{''.join(word.capitalize() for word in action.name.split('_'))}"
272
+                
273
+                # 生成类定义
274
+                code.append(f"class {class_name}(Action):")
275
+                code.append(f"    def name(self) -> str:")
276
+                code.append(f"        return \"{action.name}\"\n")
277
+                
278
+                # 生成run方法
279
+                code.append(f"    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: dict) -> list:")
280
+                code.append("        # 构建请求头")
281
+                code.append("        headers = {}")
282
+                
283
+                # 添加Token
284
+                if action.token:
285
+                    code.append(f"        headers[\"Authorization\"] = \"Bearer {action.token}\"")
286
+                
287
+                # 添加自定义请求头
288
+                headers = action.headers_dict
289
+                for key, value in headers.items():
290
+                    code.append(f"        headers[\"{key}\"] = \"{value}\"")
291
+                
292
+                # 添加内容类型
293
+                code.append("        headers[\"Content-Type\"] = \"application/json\"\n")
294
+                
295
+                # 构建请求体
296
+                if action.http_method in ["POST", "PUT"] and action.request_body:
297
+                    code.append("        # 构建请求体")
298
+                    code.append(f"        payload = {action.request_body}")
299
+                    
300
+                    # 替换模板变量
301
+                    code.append("        # 替换模板变量")
302
+                    code.append("        from jinja2 import Template")
303
+                    code.append("        payload_str = json.dumps(payload)")
304
+                    code.append("        template = Template(payload_str)")
305
+                    code.append("        payload = json.loads(template.render(tracker.slots))\n")
306
+                
307
+                # 发送请求
308
+                code.append("        # 发送请求")
309
+                code.append("        try:")
310
+                
311
+                if action.http_method == "GET":
312
+                    code.append(f"            response = requests.get(\"{action.api_url}\", headers=headers)")
313
+                elif action.http_method == "POST":
314
+                    code.append(f"            response = requests.post(\"{action.api_url}\", json=payload, headers=headers)")
315
+                elif action.http_method == "PUT":
316
+                    code.append(f"            response = requests.put(\"{action.api_url}\", json=payload, headers=headers)")
317
+                elif action.http_method == "DELETE":
318
+                    code.append(f"            response = requests.delete(\"{action.api_url}\", headers=headers)")
319
+                else:
320
+                    code.append(f"            dispatcher.utter_message(text=f\"不支持的HTTP方法: {action.http_method}\")")
321
+                    code.append("            return []")
322
+                
323
+                # 处理响应
324
+                code.append("            if response.status_code == 200:")
325
+                code.append("                result = response.json()")
326
+                
327
+                # 处理响应映射
328
+                if action.response_mapping:
329
+                    code.append("                # 处理响应映射")
330
+                    code.append(f"                mappings = {action.response_mapping}")
331
+                    code.append("                slot_events = []")
332
+                    code.append("                for slot, path in mappings.items():")
333
+                    code.append("                    # 简化的路径解析")
334
+                    code.append("                    value = result")
335
+                    code.append("                    for part in path.split('.'):")
336
+                    code.append("                        if isinstance(value, dict) and part in value:")
337
+                    code.append("                            value = value[part]")
338
+                    code.append("                        else:")
339
+                    code.append("                            value = None")
340
+                    code.append("                            break")
341
+                    code.append("                    if value is not None:")
342
+                    code.append("                        slot_events.append(SlotSet(slot, value))")
343
+                    code.append("                dispatcher.utter_message(text=str(result))")
344
+                    code.append("                return slot_events")
345
+                else:
346
+                    code.append("                dispatcher.utter_message(text=str(result))")
347
+                
348
+                code.append("            else:")
349
+                code.append("                dispatcher.utter_message(text=f\"API调用失败,状态码: {response.status_code}\")")
350
+                code.append("        except Exception as e:")
351
+                code.append("            dispatcher.utter_message(text=f\"调用API时发生错误: {str(e)}\")\n")
352
+                code.append("        return []\n")
353
+            
354
+            # 写入文件
355
+            file_path = f"{self.output_dir}/actions/actions.py"
356
+            with open(file_path, "w", encoding="utf-8") as f:
357
+                f.write("\n".join(code))
358
+            
359
+            logger.info(f"生成actions文件: {file_path}")
360
+            return file_path
361
+            
362
+        except Exception as e:
363
+            logger.error(f"生成actions文件失败: {str(e)}")
364
+            raise Exception(f"生成actions文件失败: {str(e)}")
365
+    
366
+    def _write_yaml(self, file, data, indent: int = 0, reset: bool = False) -> None:
367
+        """
368
+        简单的YAML写入函数
369
+        
370
+        Args:
371
+            file: 文件对象
372
+            data: 要写入的数据
373
+            indent: 当前缩进
374
+        """
375
+        try:
376
+            indent_str = ""
377
+            if (indent != 0):
378
+                indent_str = "  " * indent
379
+            
380
+            if isinstance(data, dict):
381
+                for key, value in data.items():
382
+                    if isinstance(value, (dict, list)):
383
+                        file.write(f"{indent_str}{key}:\n")
384
+                        self._write_yaml(file, value, indent + 1)
385
+                    else:
386
+                        if (key != 'examples'): 
387
+                            file.write(f"{key}: {self._format_yaml_value(value)}\n")
388
+                        else:
389
+                            file.write(f"{indent_str}{key}: {self._format_yaml_value(value)}\n")
390
+                            
391
+            elif isinstance(data, list):
392
+                for item in data:
393
+                    if isinstance(item, (dict, list)):
394
+                        file.write(f"{indent_str}- ")
395
+                        self._write_yaml(file, item, indent + 1, True)
396
+                    else:
397
+                        file.write(f"{indent_str}- {self._format_yaml_value(item)}\n")
398
+            else:
399
+                file.write(f"{self._format_yaml_value(data)}\n")
400
+        except Exception as e:
401
+            raise Exception(f"写入YAML数据失败: {str(e)}")
402
+    
403
+    def _format_yaml_value(self, value) -> str:
404
+        """格式化YAML值"""
405
+        if isinstance(value, str) and (":" in value or "\n" in value):
406
+            return f"|{chr(10)}{value}"
407
+        elif isinstance(value, str):
408
+            return f'{value}'
409
+        elif isinstance(value, bool):
410
+            return "true" if value else "false"
411
+        elif value is None:
412
+            return "null"
413
+        else:
414
+            return str(value)

+ 544 - 0
services/rasa_generator_by_json.py

@@ -0,0 +1,544 @@
1
+import os
2
+import sys
3
+import json
4
+import logging
5
+from pathlib import Path
6
+
7
+# 添加项目根目录到Python搜索路径
8
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
9
+from config import Config
10
+
11
+# 初始化日志
12
+logger = logging.getLogger(__name__)
13
+
14
+class RasaFileGenerator:
15
+    """Rasa配置文件生成服务,带完整异常处理"""
16
+    
17
+    def __init__(self):
18
+        """初始化生成器,从配置获取输出目录"""
19
+        try:
20
+            # 创建配置实例
21
+            self.config = Config()
22
+            self.output_dir = self.config.get("file_storage.rasa_files_dir", "./rasa_files")
23
+            
24
+            # 创建输出目录
25
+            self._create_directories()
26
+            
27
+            logger.info(f"Rasa文件生成器初始化成功,输出目录: {self.output_dir}")
28
+            
29
+        except Exception as e:
30
+            logger.error(f"初始化Rasa文件生成器失败: {str(e)}")
31
+            raise Exception(f"初始化Rasa文件生成器失败: {str(e)}")
32
+    
33
+    def _load_flow_json(self, flow_json_path: str) -> dict:
34
+        """加载并解析flow.json文件"""
35
+        try:
36
+            with open(flow_json_path, 'r', encoding='utf-8') as f:
37
+                return json.load(f)
38
+        except FileNotFoundError:
39
+            raise Exception(f"flow.json文件未找到: {flow_json_path}")
40
+        except json.JSONDecodeError:
41
+            raise Exception(f"flow.json文件格式无效")
42
+        except Exception as e:
43
+            raise Exception(f"加载flow.json文件失败: {str(e)}")
44
+    
45
+    def _create_directories(self) -> None:
46
+        """创建必要的目录结构"""
47
+        try:
48
+            Path(self.output_dir).mkdir(parents=True, exist_ok=True)
49
+            Path(f"{self.output_dir}/nlu").mkdir(parents=True, exist_ok=True)
50
+            Path(f"{self.output_dir}/stories").mkdir(parents=True, exist_ok=True)
51
+            Path(f"{self.output_dir}/actions").mkdir(parents=True, exist_ok=True)
52
+        except OSError as e:
53
+            raise Exception(f"创建目录失败: {str(e)}")
54
+    
55
+    def generate_all_files(self, flow_json_str: str, flow_name: str) -> dict:
56
+        """
57
+        生成所有Rasa配置文件
58
+        
59
+        Args:
60
+            flow_json_str: flow数据的JSON字符串
61
+            
62
+        Returns:
63
+            生成结果,包含文件路径和状态
64
+        """
65
+        try:
66
+            # 解析flow JSON字符串
67
+            flow_data = json.loads(flow_json_str)
68
+            
69
+            # 生成各文件
70
+            domain_path = self.generate_domain_file(flow_data)
71
+            nlu_path = self.generate_nlu_file(flow_data)
72
+            story_paths = self.generate_stories_files(flow_data, flow_name)
73
+            action_path = self.generate_actions_file(flow_data)
74
+            
75
+            return {
76
+                "success": True,
77
+                "message": "所有文件生成成功",
78
+                "output_dir": self.output_dir,
79
+                "files": {
80
+                    "domain": domain_path,
81
+                    "nlu": nlu_path,
82
+                    "stories": story_paths,
83
+                    "actions": action_path
84
+                }
85
+            }
86
+        except OSError as e:
87
+            logger.error(f"文件系统错误导致文件生成失败: {str(e)}")
88
+            return {
89
+                "success": False,
90
+                "message": f"文件系统错误: {str(e)}",
91
+            }
92
+        except Exception as e:
93
+            logger.error(f"文件生成失败: {str(e)}")
94
+            return {
95
+                "success": False,
96
+                "message": f"生成文件时发生错误: {str(e)}",
97
+            }
98
+    
99
+    def generate_domain_file(self, flow_data: dict) -> str:
100
+        """生成domain.yml文件"""
101
+        try:
102
+            domain_data = {
103
+                "version": "3.1",
104
+                "intents": [],
105
+                "slots": {},
106
+                "forms": {},
107
+                "responses": {},
108
+                "actions": []
109
+            }
110
+            
111
+            # 从flow_data中提取意图和动作
112
+            nodes = flow_data.get("flowJson", {}).get("nodes", [])
113
+            intents = []
114
+            actions = []
115
+            forms = []
116
+            
117
+            for node in nodes:
118
+                node_type = node.get("type")
119
+                properties = node.get("properties", {})
120
+                code = properties.get("code")
121
+                
122
+                if node_type == "intention" and code:
123
+                    intents.append(code)
124
+                elif node_type == "action" and code:
125
+                    actions.append(code)
126
+                elif node_type == "form" and code:
127
+                    actions.append(code)
128
+                elif node_type == "collection" and code:
129
+                    forms.append(code)
130
+                    actions.append(f"form_{code}")
131
+            
132
+            # 添加意图
133
+            domain_data["intents"] = intents
134
+            
135
+            # 添加动作
136
+            domain_data["actions"] = actions
137
+            
138
+            # 添加表单
139
+            for form_code in forms:
140
+                domain_data["forms"][form_code] = {}
141
+            
142
+            # 添加槽位 - 从表单节点提取
143
+            for node in nodes:
144
+                if node.get("type") == "collection":
145
+                    form_code = node.get("properties", {}).get("code")
146
+                    form_fields = node.get("properties", {}).get("formFields", [])
147
+                    for field in form_fields:
148
+                        slot_name = field.get("slotName") or f"{form_code}_{field.get('entityType')}"
149
+                        domain_data["slots"][slot_name] = {
150
+                            "type": "text"  # 默认为text类型,可根据需要调整
151
+                        }
152
+            
153
+            # 写入文件
154
+            file_path = f"{self.output_dir}/domain.yml"
155
+            with open(file_path, "w", encoding="utf-8") as f:
156
+                self._write_yaml(f, domain_data)
157
+            
158
+            logger.info(f"生成domain文件: {file_path}")
159
+            return file_path
160
+            
161
+        except Exception as e:
162
+            logger.error(f"生成domain文件失败: {str(e)}")
163
+            raise Exception(f"生成domain文件失败: {str(e)}")
164
+    
165
+    def generate_nlu_file(self, flow_data: dict) -> str:
166
+        """生成nlu.yml文件"""
167
+        try:
168
+            nlu_data = {
169
+                "version": "3.1",
170
+                "nlu": []
171
+            }
172
+            
173
+            # 从flow_data中提取意图及样本
174
+            nodes = flow_data.get("flowJson", {}).get("nodes", [])
175
+            
176
+            for node in nodes:
177
+                if node.get("type") == "intention":
178
+                    properties = node.get("properties", {})
179
+                    intent_name = properties.get("code")
180
+                    intent_desc = properties.get("desc")
181
+                    samples = properties.get("samples", [])
182
+                    
183
+                    if not intent_name:
184
+                        continue
185
+                    # {
186
+                    #         "text": "我想定明天的酒店",
187
+                    #         "entities": [
188
+                    #             {
189
+                    #                 "text": "明天",
190
+                    #                 "label": "DATE",
191
+                    #                 "start": 3,
192
+                    #                 "end": 4
193
+                    #             }
194
+                    #         ]
195
+                    #     },
196
+
197
+                    # entities = samples.get("entities", [])
198
+                    # 如果entities存在说明需要在样本中标记实体
199
+
200
+
201
+                    # 处理实体并构建examples字符串
202
+                    def format_sample_with_entities(sample):
203
+                        text = sample.get('text', '')
204
+                        entities = sample.get('entities', [])
205
+                        
206
+                        # 没有实体,直接返回文本
207
+                        if not entities:
208
+                            return text
209
+                        
210
+                        # 按start位置降序排列实体,确保从后向前处理,避免替换位置偏移
211
+                        sorted_entities = sorted(entities, key=lambda e: e.get('start', 0), reverse=True)
212
+                        
213
+                        for entity in sorted_entities:
214
+                            start = entity.get('start', 0)
215
+                            end = entity.get('end', 0) + 1  # 因为end是 exclusive 的
216
+                            entity_text = text[start:end]
217
+                            label = entity.get('label', 'UNKNOWN')
218
+                            
219
+                            # 替换文本中的实体部分
220
+                            text = text[:start] + f"[{entity_text}]({label})" + text[end:]
221
+                        
222
+                        return text
223
+                    
224
+                    examples_str = "\n".join([f"      - {format_sample_with_entities(sample)}" for sample in samples])
225
+                    
226
+                    intent_entry = {
227
+                        "intent": intent_name,
228
+                        "examples": examples_str
229
+                    }
230
+                    
231
+                    if intent_desc:
232
+                        intent_entry["intent"] = f"{intent_name} # {intent_desc}"
233
+                    
234
+                    nlu_data["nlu"].append(intent_entry)
235
+            
236
+            # 写入文件
237
+            file_path = f"{self.output_dir}/nlu/nlu.yml"
238
+            with open(file_path, "w", encoding="utf-8") as f:
239
+                self._write_yaml(f, nlu_data)
240
+            
241
+            logger.info(f"生成nlu文件: {file_path}")
242
+            return file_path
243
+            
244
+        except Exception as e:
245
+            logger.error(f"生成nlu文件失败: {str(e)}")
246
+            raise Exception(f"生成nlu文件失败: {str(e)}")
247
+    
248
+    def generate_stories_files(self, flow_data: dict, flow_name: str) -> list[str]:
249
+        """生成stories文件"""
250
+        try:
251
+            files = []
252
+            
253
+            # 从flow_data中提取故事信息
254
+            # flow_name = flow_data.get("flowName", "default_flow")
255
+            nodes = flow_data.get("flowJson", {}).get("nodes", [])
256
+            edges = flow_data.get("flowJson", {}).get("edges", [])
257
+            
258
+            # 构建节点ID到节点的映射
259
+            node_map = {node.get("id"): node for node in nodes}
260
+            
261
+            # 构建边的映射 (source -> target)
262
+            edge_map = {}
263
+            for edge in edges:
264
+                source = edge.get("sourceNodeId")
265
+                target = edge.get("targetNodeId")
266
+                if source not in edge_map:
267
+                    edge_map[source] = []
268
+                edge_map[source].append(target)
269
+            
270
+            # 找到开始节点
271
+            start_node = None
272
+            for node in nodes:
273
+                if node.get("type") == "start":
274
+                    start_node = node
275
+                    break
276
+            
277
+            if not start_node:
278
+                logger.warning("未找到开始节点")
279
+                return files
280
+            
281
+            # 构建故事步骤
282
+            story_steps = []
283
+            current_node = start_node
284
+            
285
+            while current_node:
286
+                node_type = current_node.get("type")
287
+                properties = current_node.get("properties", {})
288
+                code = properties.get("code")
289
+                
290
+                if node_type == "intention" and code:
291
+                    story_steps.append({"intent": code})
292
+                elif node_type == "action" and code:
293
+                    story_steps.append({"action": code})
294
+                elif node_type == "collection" and code:
295
+                    story_steps.append({"action": f"form_{code}"})
296
+                    # 添加表单激活后的槽位填充
297
+                    story_steps.append({"active_loop": {"name": code}})
298
+                    story_steps.append({"active_loop": None})
299
+                elif node_type == "form" and code:
300
+                    story_steps.append({"action": code})
301
+                elif node_type == "condition" and code:
302
+                    # 简化处理条件节点
303
+                    story_steps.append({"action": code})
304
+                    # 这里应该根据条件分支生成不同的故事路径,但为简化起见,我们只取第一条路径
305
+                
306
+                # 找到下一个节点
307
+                current_node_id = current_node.get("id")
308
+                next_nodes = edge_map.get(current_node_id, [])
309
+                
310
+                if not next_nodes or node_type == "end":
311
+                    break
312
+                
313
+                # 简单处理,只取第一个下一个节点
314
+                current_node = node_map.get(next_nodes[0])
315
+            
316
+            # 构建故事内容
317
+            story_entry = {
318
+                "story": flow_name,
319
+                "steps": story_steps
320
+            }
321
+            
322
+            # 生成合并的stories.yml
323
+            all_stories = {
324
+                "version": "3.1",
325
+                "stories": [story_entry]
326
+            }
327
+            
328
+            # 写入合并的stories.yml
329
+            merged_file_path = f"{self.output_dir}/stories/stories.yml"
330
+            with open(merged_file_path, "w", encoding="utf-8") as f:
331
+                self._write_yaml(f, all_stories)
332
+            
333
+            files.append(merged_file_path)
334
+            logger.info(f"生成stories文件: {len(files)} 个文件")
335
+            return files
336
+            
337
+        except Exception as e:
338
+            logger.error(f"生成stories文件失败: {str(e)}")
339
+            raise Exception(f"生成stories文件失败: {str(e)}")
340
+    
341
+    def generate_actions_file(self, flow_data: dict) -> str:
342
+        """生成自定义动作Python文件"""
343
+        try:
344
+            # 从flow_data中提取动作信息
345
+            nodes = flow_data.get("flowJson", {}).get("nodes", [])
346
+            actions = []
347
+            
348
+            for node in nodes:
349
+                node_type = node.get("type")
350
+                if node_type == "action" or node_type == "form" or node_type == "condition":
351
+                    properties = node.get("properties", {})
352
+                    if properties.get("code"):
353
+                        actions.append({
354
+                            "type": node_type,
355
+                            "properties": properties
356
+                        })
357
+            
358
+            if not actions:
359
+                logger.warning(f"未找到任何自定义动作")
360
+            
361
+            # 生成actions.py内容
362
+            code = [
363
+                "from rasa_sdk import Action, Tracker",
364
+                "from rasa_sdk.executor import CollectingDispatcher",
365
+                "from rasa_sdk.events import SlotSet, ActiveLoop",
366
+                "import requests",
367
+                "import json\n"
368
+            ]
369
+            
370
+            for action in actions:
371
+                properties = action.get("properties", {})
372
+                action_name = properties.get("code")
373
+                action_type = action.get("type")
374
+                
375
+                if not action_name:
376
+                    continue
377
+                
378
+                class_name = f"Action{''.join(word.capitalize() for word in action_name.split('_'))}"
379
+                
380
+                # 生成类定义
381
+                code.append(f"class {class_name}(Action):")
382
+                code.append(f"    def name(self) -> str:")
383
+                code.append(f"        return \"{action_name}\"\n")
384
+                
385
+                # 生成run方法
386
+                code.append(f"    def run(self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: dict) -> list:")
387
+                
388
+                if action_type == "action":
389
+                    # 普通动作节点
390
+                    config_text = properties.get("configText", "")
391
+                    code.append(f"        dispatcher.utter_message(text=\"{config_text}\")")
392
+                    code.append("        return []\n")
393
+                elif action_type == "form":
394
+                    # 表单节点(API调用)
395
+                    code.append("        # 构建请求头")
396
+                    code.append("        headers = {}")
397
+                    
398
+                    # 添加自定义请求头
399
+                    headers = properties.get("headers", [])
400
+                    for header in headers:
401
+                        code.append(f"        headers[\"{header.get('key')}\"] = \"{header.get('value')}\"")
402
+                    
403
+                    # 添加内容类型
404
+                    code.append("        headers[\"Content-Type\"] = \"application/json\"\n")
405
+                    
406
+                    # 构建请求参数
407
+                    params = properties.get("params", [])
408
+                    if params:
409
+                        code.append("        # 构建请求参数")
410
+                        code.append("        params = {}")
411
+                        for param in params:
412
+                            entity = param.get("entity")
413
+                            param_name = param.get("name")
414
+                            code.append(f"        params[\"{param_name}\"] = tracker.get_slot(\"{entity}\")")
415
+                        code.append("")
416
+                    
417
+                    # 发送请求
418
+                    code.append("        # 发送请求")
419
+                    code.append("        try:")
420
+                    
421
+                    method = properties.get("requestMethod")
422
+                    url = properties.get("requestUrl")
423
+                    
424
+                    if method == "GET":
425
+                        code.append(f"            response = requests.get(\"{url}\", headers=headers, params=params)")
426
+                    elif method == "POST":
427
+                        code.append(f"            response = requests.post(\"{url}\", headers=headers, params=params)")
428
+                    elif method == "PUT":
429
+                        code.append(f"            response = requests.put(\"{url}\", headers=headers, params=params)")
430
+                    elif method == "DELETE":
431
+                        code.append(f"            response = requests.delete(\"{url}\", headers=headers, params=params)")
432
+                    else:
433
+                        code.append(f"            dispatcher.utter_message(text=f\"不支持的HTTP方法: {method}\")")
434
+                        code.append("            return []")
435
+                    
436
+                    # 处理响应
437
+                    code.append("            if response.status_code == 200:")
438
+                    code.append("                result = response.json()")
439
+                    
440
+                    # 处理响应映射
441
+                    response_mappings = properties.get("responseMappings", [])
442
+                    if response_mappings:
443
+                        code.append("                # 处理响应映射")
444
+                        code.append("                slot_events = []")
445
+                        for mapping in response_mappings:
446
+                            field = mapping.get("responseField")
447
+                            target = mapping.get("targetVar")
448
+                            default = mapping.get("defaultValue")
449
+                            
450
+                            code.append(f"                # 提取 {field}")
451
+                            code.append(f"                value = {default}")
452
+                            code.append(f"                try:")
453
+                            code.append(f"                    # 简化的路径解析")
454
+                            code.append(f"                    parts = \"{field}\".split('.')")
455
+                            code.append(f"                    temp = result")
456
+                            code.append(f"                    for part in parts:")
457
+                            code.append(f"                        if isinstance(temp, dict) and part in temp:")
458
+                            code.append(f"                            temp = temp[part]")
459
+                            code.append(f"                        elif isinstance(temp, list) and part.isdigit() and int(part) < len(temp):")
460
+                            code.append(f"                            temp = temp[int(part)]")
461
+                            code.append(f"                        else:")
462
+                            code.append(f"                            raise Exception(\"路径无效\")")
463
+                            code.append(f"                    value = temp")
464
+                            code.append(f"                except:")
465
+                            code.append(f"                    pass")
466
+                            code.append(f"                slot_events.append(SlotSet(\"{target}\", value))")
467
+                        
468
+                        code.append("                dispatcher.utter_message(text=str(result))")
469
+                        code.append("                return slot_events")
470
+                    else:
471
+                        code.append("                dispatcher.utter_message(text=str(result))")
472
+                    
473
+                    code.append("            else:")
474
+                    code.append("                dispatcher.utter_message(text=f\"API调用失败,状态码: {response.status_code}\")")
475
+                    code.append("        except Exception as e:")
476
+                    code.append("            dispatcher.utter_message(text=f\"调用API时发生错误: {str(e)}\")\n")
477
+                    code.append("        return []\n")
478
+                elif action_type == "condition":
479
+                    # 条件节点
480
+                    code.append("        # 处理条件逻辑")
481
+                    code.append("        # 这里简化处理,实际应用中应根据条件执行不同的逻辑")
482
+                    code.append("        return []\n")
483
+            
484
+            # 写入文件
485
+            file_path = f"{self.output_dir}/actions/actions.py"
486
+            with open(file_path, "w", encoding="utf-8") as f:
487
+                f.write("\n".join(code))
488
+            
489
+            logger.info(f"生成actions文件: {file_path}")
490
+            return file_path
491
+            
492
+        except Exception as e:
493
+            logger.error(f"生成actions文件失败: {str(e)}")
494
+            raise Exception(f"生成actions文件失败: {str(e)}")
495
+    
496
+    def _write_yaml(self, file, data, indent: int = 0, reset: bool = False) -> None:
497
+        """
498
+        简单的YAML写入函数
499
+        
500
+        Args:
501
+            file: 文件对象
502
+            data: 要写入的数据
503
+            indent: 当前缩进
504
+        """
505
+        try:
506
+            indent_str = ""
507
+            if (indent != 0):
508
+                indent_str = "  " * indent
509
+            
510
+            if isinstance(data, dict):
511
+                for key, value in data.items():
512
+                    if isinstance(value, (dict, list)):
513
+                        file.write(f"{indent_str}{key}:\n")
514
+                        self._write_yaml(file, value, indent + 1)
515
+                    else:
516
+                        if (key != 'examples'): 
517
+                            file.write(f"{key}: {self._format_yaml_value(value)}\n")
518
+                        else:
519
+                            file.write(f"{indent_str}{key}: {self._format_yaml_value(value)}\n")
520
+                            
521
+            elif isinstance(data, list):
522
+                for item in data:
523
+                    if isinstance(item, (dict, list)):
524
+                        file.write(f"{indent_str}- ")
525
+                        self._write_yaml(file, item, indent + 1, True)
526
+                    else:
527
+                        file.write(f"{indent_str}- {self._format_yaml_value(item)}\n")
528
+            else:
529
+                file.write(f"{self._format_yaml_value(data)}\n")
530
+        except Exception as e:
531
+            raise Exception(f"写入YAML数据失败: {str(e)}")
532
+    
533
+    def _format_yaml_value(self, value) -> str:
534
+        """格式化YAML值"""
535
+        if isinstance(value, str) and (":" in value or "\n" in value):
536
+            return f"|{chr(10)}{value}"
537
+        elif isinstance(value, str):
538
+            return f'{value}'
539
+        elif isinstance(value, bool):
540
+            return "true" if value else "false"
541
+        elif value is None:
542
+            return "null"
543
+        else:
544
+            return str(value)

+ 354 - 0
services/rasa_manager.py

@@ -0,0 +1,354 @@
1
+import os
2
+import time
3
+import signal
4
+import logging
5
+import subprocess
6
+import requests
7
+from sqlalchemy.orm import Session
8
+from sqlalchemy.exc import SQLAlchemyError
9
+from db import SystemConfig
10
+from config import config
11
+
12
+# 初始化日志
13
+logger = logging.getLogger(__name__)
14
+
15
+class RasaManager:
16
+    """Rasa服务管理器,带完整异常处理"""
17
+    
18
+    def __init__(self):
19
+        """初始化管理器"""
20
+        self.rasa_process = None
21
+        self.action_process = None
22
+        self.startup_timeout = config.get("rasa.startup_timeout", 60)
23
+        
24
+        logger.info("Rasa服务管理器初始化成功")
25
+    
26
+    def start_rasa_server(self, session: Session, model_path: str = None) -> dict:
27
+        """
28
+        启动Rasa服务器
29
+        
30
+        Args:
31
+            session: 数据库会话
32
+            model_path: 模型路径,None表示使用默认模型
33
+            
34
+        Returns:
35
+            启动结果
36
+        """
37
+        try:
38
+            # 停止已运行的服务
39
+            if self.rasa_process:
40
+                logger.info("检测到已运行的Rasa服务器,正在停止...")
41
+                self.stop_rasa_server()
42
+            
43
+            # 获取配置的Rasa服务器地址
44
+            rasa_url = SystemConfig.get_value(session, "rasa_server_url", "http://localhost:5005")
45
+            host, port = self._parse_url(rasa_url)
46
+            
47
+            # 构建启动命令
48
+            cmd = ["rasa", "run", "--enable-api", "--cors", "*", "--host", host, "--port", str(port)]
49
+            
50
+            # 如果指定了模型路径
51
+            if model_path and os.path.exists(model_path):
52
+                if not os.path.isfile(model_path) and not os.path.isdir(model_path):
53
+                    raise ValueError(f"模型路径不存在: {model_path}")
54
+                cmd.extend(["--model", model_path])
55
+                logger.info(f"使用指定模型路径: {model_path}")
56
+            else:
57
+                default_model = config.get("rasa.default_model_path")
58
+                if default_model and os.path.exists(default_model):
59
+                    cmd.extend(["--model", default_model])
60
+                    logger.info(f"使用默认模型路径: {default_model}")
61
+            
62
+            logger.info(f"启动Rasa服务器命令: {' '.join(cmd)}")
63
+            
64
+            # 启动Rasa服务器
65
+            self.rasa_process = subprocess.Popen(
66
+                cmd,
67
+                stdout=subprocess.PIPE,
68
+                stderr=subprocess.PIPE,
69
+                text=True
70
+            )
71
+            
72
+            logger.info(f"Rasa服务器已启动,PID: {self.rasa_process.pid}")
73
+            
74
+            # 等待服务启动
75
+            start_time = time.time()
76
+            health_check_interval = 2  # 每2秒检查一次
77
+            max_attempts = self.startup_timeout // health_check_interval
78
+            
79
+            for attempt in range(max_attempts):
80
+                if self._check_rasa_health(rasa_url):
81
+                    logger.info(f"Rasa服务器启动成功,耗时 {time.time() - start_time:.2f} 秒")
82
+                    return {
83
+                        "success": True,
84
+                        "message": "Rasa服务器启动成功",
85
+                        "url": rasa_url,
86
+                        "pid": self.rasa_process.pid
87
+                    }
88
+                
89
+                # 检查进程是否已退出
90
+                if self.rasa_process.poll() is not None:
91
+                    stderr = self.rasa_process.stderr.read()
92
+                    raise Exception(f"Rasa服务器启动后意外退出,错误输出: {stderr}")
93
+                
94
+                time.sleep(health_check_interval)
95
+            
96
+            # 超时未成功启动
97
+            stderr = self.rasa_process.stderr.read()
98
+            self.stop_rasa_server()  # 清理进程
99
+            raise Exception(f"Rasa服务器启动超时({self.startup_timeout}秒),错误输出: {stderr}")
100
+                
101
+        except ValueError as e:
102
+            logger.warning(f"启动Rasa服务器参数错误: {str(e)}")
103
+            return {
104
+                "success": False,
105
+                "message": str(e)
106
+            }
107
+        except subprocess.SubprocessError as e:
108
+            logger.error(f"启动Rasa服务器进程错误: {str(e)}")
109
+            return {
110
+                "success": False,
111
+                "message": f"进程错误: {str(e)}"
112
+            }
113
+        except Exception as e:
114
+            logger.error(f"启动Rasa服务器失败: {str(e)}")
115
+            return {
116
+                "success": False,
117
+                "message": str(e)
118
+            }
119
+    
120
+    def start_action_server(self, session: Session, actions_dir: str = None) -> dict:
121
+        """启动Rasa动作服务器"""
122
+        try:
123
+            # 停止已运行的服务
124
+            if self.action_process:
125
+                logger.info("检测到已运行的动作服务器,正在停止...")
126
+                self.stop_action_server()
127
+            
128
+            # 获取配置的动作服务器地址
129
+            action_url = SystemConfig.get_value(session, "rasa_actions_url", "http://localhost:5055")
130
+            host, port = self._parse_url(action_url)
131
+            
132
+            # 构建启动命令
133
+            cmd = ["rasa", "run", "actions", "--host", host, "--port", str(port)]
134
+            
135
+            # 如果指定了动作目录
136
+            if actions_dir and os.path.exists(actions_dir):
137
+                if not os.path.isdir(actions_dir):
138
+                    raise ValueError(f"动作目录不是有效的文件夹: {actions_dir}")
139
+                cmd.extend(["--actions", "actions", "--actions-dir", actions_dir])
140
+                logger.info(f"使用指定动作目录: {actions_dir}")
141
+            else:
142
+                default_actions = os.path.join(config.get("file_storage.rasa_files_dir", "./rasa_files"), "actions")
143
+                if os.path.exists(default_actions):
144
+                    cmd.extend(["--actions", "actions", "--actions-dir", default_actions])
145
+                    logger.info(f"使用默认动作目录: {default_actions}")
146
+            
147
+            logger.info(f"启动动作服务器命令: {' '.join(cmd)}")
148
+            
149
+            # 启动动作服务器
150
+            self.action_process = subprocess.Popen(
151
+                cmd,
152
+                stdout=subprocess.PIPE,
153
+                stderr=subprocess.PIPE,
154
+                text=True
155
+            )
156
+            
157
+            logger.info(f"动作服务器已启动,PID: {self.action_process.pid}")
158
+            
159
+            # 等待服务启动
160
+            start_time = time.time()
161
+            health_check_interval = 2  # 每2秒检查一次
162
+            max_attempts = self.startup_timeout // health_check_interval
163
+            
164
+            for attempt in range(max_attempts):
165
+                if self._check_action_health(action_url):
166
+                    logger.info(f"动作服务器启动成功,耗时 {time.time() - start_time:.2f} 秒")
167
+                    return {
168
+                        "success": True,
169
+                        "message": "Rasa动作服务器启动成功",
170
+                        "url": action_url,
171
+                        "pid": self.action_process.pid
172
+                    }
173
+                
174
+                # 检查进程是否已退出
175
+                if self.action_process.poll() is not None:
176
+                    stderr = self.action_process.stderr.read()
177
+                    raise Exception(f"动作服务器启动后意外退出,错误输出: {stderr}")
178
+                
179
+                time.sleep(health_check_interval)
180
+            
181
+            # 超时未成功启动
182
+            stderr = self.action_process.stderr.read()
183
+            self.stop_action_server()  # 清理进程
184
+            raise Exception(f"动作服务器启动超时({self.startup_timeout}秒),错误输出: {stderr}")
185
+                
186
+        except ValueError as e:
187
+            logger.warning(f"启动动作服务器参数错误: {str(e)}")
188
+            return {
189
+                "success": False,
190
+                "message": str(e)
191
+            }
192
+        except subprocess.SubprocessError as e:
193
+            logger.error(f"启动动作服务器进程错误: {str(e)}")
194
+            return {
195
+                "success": False,
196
+                "message": f"进程错误: {str(e)}"
197
+            }
198
+        except Exception as e:
199
+            logger.error(f"启动动作服务器失败: {str(e)}")
200
+            return {
201
+                "success": False,
202
+                "message": str(e)
203
+            }
204
+    
205
+    def restart_rasa_services(self, session: Session, model_path: str = None, actions_dir: str = None) -> dict:
206
+        """重启所有Rasa服务"""
207
+        try:
208
+            logger.info("开始重启所有Rasa服务")
209
+            
210
+            # 先停止所有服务
211
+            self.stop_all_services()
212
+            
213
+            # 启动Rasa服务器
214
+            rasa_result = self.start_rasa_server(session, model_path)
215
+            if not rasa_result["success"]:
216
+                return {
217
+                    "success": False,
218
+                    "message": f"Rasa服务器启动失败: {rasa_result['message']}",
219
+                    "rasa": rasa_result
220
+                }
221
+            
222
+            # 启动动作服务器
223
+            action_result = self.start_action_server(session, actions_dir)
224
+            if not action_result["success"]:
225
+                # 如果动作服务器启动失败,停止已启动的Rasa服务器
226
+                self.stop_rasa_server()
227
+                return {
228
+                    "success": False,
229
+                    "message": f"动作服务器启动失败: {action_result['message']}",
230
+                    "rasa": rasa_result,
231
+                    "action": action_result
232
+                }
233
+            
234
+            logger.info("所有Rasa服务重启成功")
235
+            return {
236
+                "success": True,
237
+                "message": "所有Rasa服务重启成功",
238
+                "rasa": rasa_result,
239
+                "action": action_result
240
+            }
241
+        except Exception as e:
242
+            logger.error(f"重启Rasa服务时发生错误: {str(e)}")
243
+            return {
244
+                "success": False,
245
+                "message": f"重启Rasa服务失败: {str(e)}"
246
+            }
247
+    
248
+    def stop_rasa_server(self) -> bool:
249
+        """停止Rasa服务器"""
250
+        result = self._stop_process(self.rasa_process, "Rasa服务器")
251
+        self.rasa_process = None
252
+        return result
253
+    
254
+    def stop_action_server(self) -> bool:
255
+        """停止Rasa动作服务器"""
256
+        result = self._stop_process(self.action_process, "动作服务器")
257
+        self.action_process = None
258
+        return result
259
+    
260
+    def stop_all_services(self) -> bool:
261
+        """停止所有Rasa服务"""
262
+        logger.info("停止所有Rasa服务")
263
+        rasa_stopped = self.stop_rasa_server()
264
+        action_stopped = self.stop_action_server()
265
+        return rasa_stopped and action_stopped
266
+    
267
+    def _stop_process(self, process, process_name: str) -> bool:
268
+        """
269
+        停止进程
270
+        
271
+        Args:
272
+            process: 要停止的进程对象
273
+            process_name: 进程名称,用于日志
274
+            
275
+        Returns:
276
+            是否成功停止
277
+        """
278
+        if not process or process.poll() is not None:
279
+            logger.info(f"{process_name}未在运行")
280
+            return True
281
+            
282
+        try:
283
+            pid = process.pid
284
+            logger.info(f"正在停止{process_name},PID: {pid}")
285
+            
286
+            # 尝试优雅关闭
287
+            process.terminate()
288
+            
289
+            # 等待进程退出
290
+            for _ in range(10):  # 最多等待10秒
291
+                if process.poll() is not None:
292
+                    logger.info(f"{process_name}已成功停止,PID: {pid}")
293
+                    return True
294
+                time.sleep(1)
295
+            
296
+            # 强制关闭
297
+            logger.warning(f"{process_name}未能优雅关闭,尝试强制终止,PID: {pid}")
298
+            os.kill(pid, signal.SIGKILL)
299
+            
300
+            # 再次检查
301
+            time.sleep(2)
302
+            if process.poll() is not None:
303
+                logger.info(f"{process_name}已强制终止,PID: {pid}")
304
+                return True
305
+            else:
306
+                logger.error(f"{process_name}强制终止失败,PID: {pid}")
307
+                return False
308
+                
309
+        except Exception as e:
310
+            logger.error(f"停止{process_name}时发生错误: {str(e)}")
311
+            return False
312
+    
313
+    def _check_rasa_health(self, url: str) -> bool:
314
+        """检查Rasa服务器健康状态"""
315
+        try:
316
+            health_url = f"{url}/health"
317
+            response = requests.get(health_url, timeout=5)
318
+            return response.status_code == 200
319
+        except requests.RequestException:
320
+            return False
321
+    
322
+    def _check_action_health(self, url: str) -> bool:
323
+        """检查动作服务器健康状态"""
324
+        try:
325
+            health_url = f"{url}/health"
326
+            response = requests.get(health_url, timeout=5)
327
+            return response.status_code == 200
328
+        except requests.RequestException:
329
+            return False
330
+    
331
+    def _parse_url(self, url: str) -> tuple:
332
+        """
333
+        解析URL获取主机和端口
334
+        
335
+        Args:
336
+            url: 要解析的URL
337
+            
338
+        Returns:
339
+            (主机, 端口) 元组
340
+        """
341
+        try:
342
+            from urllib.parse import urlparse
343
+            parsed = urlparse(url)
344
+            host = parsed.hostname or "localhost"
345
+            port = parsed.port 
346
+            
347
+            # 如果没有指定端口,使用默认端口
348
+            if not port:
349
+                port = 443 if parsed.scheme == "https" else 5005
350
+                
351
+            return (host, port)
352
+        except Exception as e:
353
+            logger.warning(f"解析URL {url} 失败,使用默认值: {str(e)}")
354
+            return ("localhost", 5005)

+ 39 - 0
services/test_rasa_generator.py

@@ -0,0 +1,39 @@
1
+import logging
2
+import os
3
+from rasa_generator_by_json import RasaFileGenerator
4
+
5
+# 配置日志
6
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
7
+logger = logging.getLogger(__name__)
8
+
9
+if __name__ == "__main__":
10
+    try:
11
+        # 创建生成器实例
12
+        generator = RasaFileGenerator()
13
+        
14
+        # 定义flow.json文件路径
15
+        flow_json_path = '/home/rasa_manager/services/flow.json'
16
+        
17
+        # 检查文件是否存在
18
+        if not os.path.exists(flow_json_path):
19
+            raise Exception(f"flow.json文件不存在: {flow_json_path}")
20
+        
21
+        # 读取flow.json文件内容
22
+        with open(flow_json_path, 'r', encoding='utf-8') as f:
23
+            flow_json_str = f.read()
24
+        
25
+        logger.info(f"开始从JSON字符串生成Rasa文件...")
26
+        
27
+        # 生成所有文件
28
+        result = generator.generate_all_files(flow_json_str, "酒店预定")
29
+        
30
+        if result['success']:
31
+            logger.info(f"文件生成成功!输出目录: {result['output_dir']}")
32
+            logger.info(f"生成的文件:")
33
+            for file_type, file_path in result['files'].items():
34
+                logger.info(f"  - {file_type}: {file_path}")
35
+        else:
36
+            logger.error(f"文件生成失败: {result['message']}")
37
+            
38
+    except Exception as e:
39
+        logger.error(f"测试过程中发生错误: {str(e)}")

+ 244 - 0
sql/schema.sql

@@ -0,0 +1,244 @@
1
+-- ----------------------------
2
+-- 数据库表结构和示例数据
3
+-- 特性:无外键约束、无版本管理
4
+-- 所有关联关系由应用代码控制
5
+-- ----------------------------
6
+
7
+-- ----------------------------
8
+-- 1. 意图与实体相关表
9
+-- ----------------------------
10
+
11
+-- 意图表:存储Rasa意图定义
12
+CREATE TABLE `intent` (
13
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '意图ID,自增主键',
14
+  `name` varchar(100) NOT NULL COMMENT '意图名称,如greet、query_bill',
15
+  `description` varchar(500) DEFAULT NULL COMMENT '意图描述',
16
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
17
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
18
+  PRIMARY KEY (`id`),
19
+  UNIQUE KEY `uk_intent_name` (`name`) COMMENT '确保意图名称唯一'
20
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储Rasa意图定义';
21
+
22
+-- 意图样本表:存储意图对应的用户输入示例
23
+CREATE TABLE `intent_example` (
24
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '样本ID,自增主键',
25
+  `intent_id` bigint NOT NULL COMMENT '关联的意图ID(代码控制关联)',
26
+  `text` varchar(1000) NOT NULL COMMENT '用户输入示例文本',
27
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
28
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
29
+  PRIMARY KEY (`id`),
30
+  KEY `idx_intent_id` (`intent_id`) COMMENT '意图ID索引,加速查询'
31
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储意图对应的用户输入示例';
32
+
33
+-- 实体表:存储Rasa实体定义
34
+CREATE TABLE `entity` (
35
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '实体ID,自增主键',
36
+  `name` varchar(100) NOT NULL COMMENT '实体名称,如account_number、date',
37
+  `description` varchar(500) DEFAULT NULL COMMENT '实体描述',
38
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
39
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
40
+  PRIMARY KEY (`id`),
41
+  UNIQUE KEY `uk_entity_name` (`name`) COMMENT '确保实体名称唯一'
42
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储Rasa实体定义';
43
+
44
+-- ----------------------------
45
+-- 2. 对话流程相关表
46
+-- ----------------------------
47
+
48
+-- 故事表:存储Rasa对话流程定义
49
+CREATE TABLE `story` (
50
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '故事ID,自增主键',
51
+  `name` varchar(200) NOT NULL COMMENT '故事名称',
52
+  `description` varchar(500) DEFAULT NULL COMMENT '故事描述',
53
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
54
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
55
+  PRIMARY KEY (`id`),
56
+  UNIQUE KEY `uk_story_name` (`name`) COMMENT '确保故事名称唯一'
57
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储Rasa对话流程定义';
58
+
59
+-- 故事步骤表:存储故事的具体步骤
60
+CREATE TABLE `story_step` (
61
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '步骤ID,自增主键',
62
+  `story_id` bigint NOT NULL COMMENT '关联的故事ID(代码控制关联)',
63
+  `step_order` int NOT NULL COMMENT '步骤顺序',
64
+  `step_type` varchar(20) NOT NULL COMMENT '步骤类型(intent/action等)',
65
+  `content` text NOT NULL COMMENT '步骤内容,JSON格式',
66
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
67
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
68
+  PRIMARY KEY (`id`),
69
+  KEY `idx_story_id` (`story_id`) COMMENT '故事ID索引,加速查询'
70
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储故事的具体步骤';
71
+
72
+-- ----------------------------
73
+-- 3. 动作与响应相关表
74
+-- ----------------------------
75
+
76
+-- 自定义动作表:存储对接第三方API的自定义动作配置
77
+CREATE TABLE `custom_action` (
78
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '动作ID,自增主键',
79
+  `name` varchar(100) NOT NULL COMMENT '动作名称',
80
+  `description` varchar(500) DEFAULT NULL COMMENT '动作描述',
81
+  `http_method` varchar(10) NOT NULL COMMENT 'HTTP方法',
82
+  `api_url` varchar(500) NOT NULL COMMENT 'API地址',
83
+  `token` varchar(500) DEFAULT NULL COMMENT '鉴权Token',
84
+  `headers` text COMMENT '请求头配置,JSON格式',
85
+  `request_body` text COMMENT '请求体,JSON格式',
86
+  `response_mapping` text COMMENT '响应映射规则,JSON格式',
87
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
88
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
89
+  PRIMARY KEY (`id`),
90
+  UNIQUE KEY `uk_action_name` (`name`) COMMENT '确保动作名称唯一'
91
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储对接第三方API的自定义动作配置';
92
+
93
+-- 响应模板表:存储Rasa响应模板
94
+CREATE TABLE `response` (
95
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '响应ID,自增主键',
96
+  `name` varchar(100) NOT NULL COMMENT '响应名称,如utter_greet',
97
+  `description` varchar(500) DEFAULT NULL COMMENT '响应描述',
98
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
99
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
100
+  PRIMARY KEY (`id`),
101
+  UNIQUE KEY `uk_response_name` (`name`) COMMENT '确保响应名称唯一'
102
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储Rasa响应模板';
103
+
104
+-- 响应内容表:存储响应的具体内容
105
+CREATE TABLE `response_content` (
106
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '响应内容ID,自增主键',
107
+  `response_id` bigint NOT NULL COMMENT '关联的响应ID(代码控制关联)',
108
+  `content_type` varchar(20) NOT NULL COMMENT '内容类型,如text、image',
109
+  `text_content` varchar(2000) DEFAULT NULL COMMENT '文本内容',
110
+  `media_url` varchar(500) DEFAULT NULL COMMENT '媒体URL',
111
+  `content_order` int NOT NULL COMMENT '内容顺序',
112
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
113
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
114
+  PRIMARY KEY (`id`),
115
+  KEY `idx_response_id` (`response_id`) COMMENT '响应ID索引,加速查询'
116
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储响应的具体内容';
117
+
118
+-- ----------------------------
119
+-- 4. 系统配置表
120
+-- ----------------------------
121
+
122
+-- 系统配置表:存储系统基础配置
123
+CREATE TABLE `system_config` (
124
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '配置ID,自增主键',
125
+  `config_key` varchar(100) NOT NULL COMMENT '配置键,如rasa_server_url',
126
+  `config_value` varchar(500) NOT NULL COMMENT '配置值',
127
+  `description` varchar(500) DEFAULT NULL COMMENT '配置描述',
128
+  `updated_by` varchar(100) DEFAULT NULL COMMENT '更新人',
129
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
130
+  PRIMARY KEY (`id`),
131
+  UNIQUE KEY `uk_config_key` (`config_key`) COMMENT '确保配置键唯一'
132
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储系统配置信息';
133
+
134
+-- ----------------------------
135
+-- 示例数据插入
136
+-- ----------------------------
137
+
138
+-- 插入系统配置
139
+INSERT INTO `system_config` (`config_key`, `config_value`, `description`, `updated_by`) VALUES
140
+('rasa_server_url', 'http://localhost:5005', 'Rasa服务器地址', 'system'),
141
+('rasa_actions_url', 'http://localhost:5055', 'Rasa动作服务器地址', 'system'),
142
+('log_level', 'INFO', '系统日志级别', 'system'),
143
+('api_timeout', '30', 'API请求超时时间(秒)', 'system');
144
+
145
+-- 插入意图数据
146
+INSERT INTO `intent` (`name`, `description`) VALUES
147
+('greet', '用户打招呼或问候'),
148
+('goodbye', '用户告别或结束对话'),
149
+('query_bill', '用户查询账单信息'),
150
+('query_balance', '用户查询账户余额'),
151
+('complain', '用户投诉或反馈问题');
152
+
153
+-- 插入意图样本数据
154
+INSERT INTO `intent_example` (`intent_id`, `text`) VALUES
155
+(1, '你好'),
156
+(1, '早上好'),
157
+(1, '嗨,在吗'),
158
+(1, '您好,请问有人吗'),
159
+(2, '再见'),
160
+(2, '拜拜'),
161
+(2, '下次见'),
162
+(2, '退出对话'),
163
+(3, '我的账单是多少'),
164
+(3, '查询一下我的消费记录'),
165
+(3, '看看我这个月花了多少钱'),
166
+(3, '上个月的账单明细'),
167
+(4, '我的账户还有多少余额'),
168
+(4, '查询余额'),
169
+(4, '我的卡里还有钱吗'),
170
+(5, '我要投诉'),
171
+(5, '这个服务太差了'),
172
+(5, '我有问题要反馈');
173
+
174
+-- 插入实体数据
175
+INSERT INTO `entity` (`name`, `description`) VALUES
176
+('account_number', '银行账号'),
177
+('date', '日期'),
178
+('amount', '金额'),
179
+('product', '产品名称'),
180
+('user_id', '用户ID'),
181
+('complaint_type', '投诉类型');
182
+
183
+-- 插入故事数据
184
+INSERT INTO `story` (`name`, `description`) VALUES
185
+('greet_and_respond', '问候与回应的对话流程'),
186
+('bill_inquiry', '账单查询的对话流程'),
187
+('balance_inquiry', '余额查询的对话流程');
188
+
189
+-- 插入故事步骤数据
190
+INSERT INTO `story_step` (`story_id`, `step_order`, `step_type`, `content`) VALUES
191
+-- 问候对话流程
192
+(1, 1, 'intent', '{"name": "greet"}'),
193
+(1, 2, 'action', '{"name": "utter_greet"}'),
194
+(1, 3, 'intent', '{"name": "goodbye"}'),
195
+(1, 4, 'action', '{"name": "utter_goodbye"}'),
196
+
197
+-- 账单查询流程
198
+(2, 1, 'intent', '{"name": "query_bill"}'),
199
+(2, 2, 'action', '{"name": "utter_ask_account_number"}'),
200
+(2, 3, 'action', '{"name": "action_query_bill"}'),
201
+(2, 4, 'action', '{"name": "utter_bill_result"}'),
202
+
203
+-- 余额查询流程
204
+(3, 1, 'intent', '{"name": "query_balance"}'),
205
+(3, 2, 'action', '{"name": "utter_ask_account_number"}'),
206
+(3, 3, 'action', '{"name": "action_query_balance"}'),
207
+(3, 4, 'action', '{"name": "utter_balance_result"}');
208
+
209
+-- 插入自定义动作数据
210
+INSERT INTO `custom_action` (`name`, `description`, `http_method`, `api_url`, `token`, `headers`, `request_body`, `response_mapping`) VALUES
211
+('action_query_bill', '查询账单信息', 'POST', 'https://api.example.com/bill/query', 'secret_token_123', '{"Content-Type": "application/json"}', '{"account": "{{account_number}}", "month": "{{bill_month}}"}', '{"total_amount": "data.total", "due_date": "data.due_date", "details": "data.details"}'),
212
+('action_query_balance', '查询账户余额', 'GET', 'https://api.example.com/account/{{account_number}}/balance', 'secret_token_123', '{"Authorization": "Bearer {{token}}"}', NULL, '{"balance": "data.balance", "available": "data.available", "currency": "data.currency"}');
213
+
214
+-- 插入响应模板数据
215
+INSERT INTO `response` (`name`, `description`) VALUES
216
+('utter_greet', '问候用户的响应'),
217
+('utter_goodbye', '告别用户的响应'),
218
+('utter_ask_account_number', '询问用户账号的响应'),
219
+('utter_bill_result', '展示账单查询结果的响应'),
220
+('utter_balance_result', '展示余额查询结果的响应');
221
+
222
+-- 插入响应内容数据
223
+INSERT INTO `response_content` (`response_id`, `content_type`, `text_content`, `media_url`, `content_order`) VALUES
224
+-- 问候响应
225
+(1, 'text', '你好!有什么可以帮助您的吗?', NULL, 1),
226
+(1, 'text', '您好!很高兴为您服务。', NULL, 2),
227
+(1, 'text', '欢迎咨询,我能为您做些什么?', NULL, 3),
228
+
229
+-- 告别响应
230
+(2, 'text', '再见!祝您生活愉快。', NULL, 1),
231
+(2, 'text', '感谢您的咨询,再见!', NULL, 2),
232
+(2, 'text', '有任何问题随时联系我们,再见!', NULL, 3),
233
+
234
+-- 询问账号
235
+(3, 'text', '请提供您的账号,以便我为您查询。', NULL, 1),
236
+(3, 'text', '麻烦告诉我您的账号信息,我会尽快为您处理。', NULL, 2),
237
+
238
+-- 账单结果
239
+(4, 'text', '您{{bill_month}}的账单总金额为{{total_amount}}元,到期日为{{due_date}}。', NULL, 1),
240
+(4, 'text', '查询到您的账单信息:总额{{total_amount}}元,需在{{due_date}}前支付。', NULL, 2),
241
+
242
+-- 余额结果
243
+(5, 'text', '您的账户当前余额为{{balance}}{{currency}},可用余额{{available}}{{currency}}。', NULL, 1),
244
+(5, 'text', '查询到您的账户余额:{{balance}}{{currency}}(可用:{{available}}{{currency}})。', NULL, 2);