Coverage for mindsdb / interfaces / skills / skills_controller.py: 79%

67 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-21 00:36 +0000

1import datetime 

2from typing import Dict, List, Optional 

3 

4from sqlalchemy import null, func 

5from sqlalchemy.orm.attributes import flag_modified 

6 

7from mindsdb.interfaces.storage import db 

8from mindsdb.interfaces.database.projects import ProjectController 

9from mindsdb.utilities.config import config 

10from mindsdb.utilities import log 

11 

12 

13logger = log.getLogger(__name__) 

14 

15default_project = config.get("default_project") 

16 

17 

18class SkillsController: 

19 """Handles CRUD operations at the database level for Skills""" 

20 

21 def __init__(self, project_controller: ProjectController = None): 

22 if project_controller is None: 22 ↛ 24line 22 didn't jump to line 24 because the condition on line 22 was always true

23 project_controller = ProjectController() 

24 self.project_controller = project_controller 

25 

26 def get_skill( 

27 self, skill_name: str, project_name: str = default_project, strict_case: bool = False 

28 ) -> Optional[db.Skills]: 

29 """ 

30 Gets a skill by name. Skills are expected to have unique names. 

31 

32 Parameters: 

33 skill_name (str): The name of the skill 

34 project_name (str): The name of the containing project 

35 strict_case (bool): If True, the skill name is case-sensitive. Defaults to False. 

36 

37 Returns: 

38 skill (Optional[db.Skills]): The database skill object 

39 

40 Raises: 

41 ValueError: If `project_name` does not exist 

42 """ 

43 

44 project = self.project_controller.get(name=project_name) 

45 query = db.Skills.query.filter( 

46 db.Skills.project_id == project.id, 

47 db.Skills.deleted_at == null(), 

48 ) 

49 if strict_case: 

50 query = query.filter(db.Skills.name == skill_name) 

51 else: 

52 query = query.filter(func.lower(db.Skills.name) == func.lower(skill_name)) 

53 

54 return query.first() 

55 

56 def get_skills(self, project_name: Optional[str]) -> List[dict]: 

57 """ 

58 Gets all skills in a project. 

59 

60 Parameters: 

61 project_name (Optional[str]): The name of the containing project 

62 

63 Returns: 

64 all_skills (List[db.Skills]): List of database skill object 

65 

66 Raises: 

67 ValueError: If `project_name` does not exist 

68 """ 

69 

70 if project_name is None: 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true

71 projects = self.project_controller.get_list() 

72 project_ids = list([p.id for p in projects]) 

73 else: 

74 project = self.project_controller.get(name=project_name) 

75 project_ids = [project.id] 

76 

77 query = db.session.query(db.Skills).filter( 

78 db.Skills.project_id.in_(project_ids), db.Skills.deleted_at == null() 

79 ) 

80 

81 return query.all() 

82 

83 def add_skill(self, name: str, project_name: str, type: str, params: Dict[str, str] = {}) -> db.Skills: 

84 """ 

85 Adds a skill to the database. 

86 

87 Parameters: 

88 name (str): The name of the new skill 

89 project_name (str): The containing project 

90 type (str): The type of the skill (e.g. Knowledge Base) 

91 params: (Dict[str, str]): Parameters associated with the skill 

92 

93 Returns: 

94 bot (db.Skills): The created skill 

95 

96 Raises: 

97 ValueError: If `project_name` does not exist or skill already exists 

98 """ 

99 if project_name is None: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 project_name = default_project 

101 project = self.project_controller.get(name=project_name) 

102 

103 skill = self.get_skill(name, project_name, strict_case=True) 

104 

105 if skill is not None: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true

106 raise ValueError(f"Skill with name already exists: {name}") 

107 

108 new_skill = db.Skills( 

109 name=name, 

110 project_id=project.id, 

111 type=type, 

112 params=params, 

113 ) 

114 db.session.add(new_skill) 

115 db.session.commit() 

116 

117 return new_skill 

118 

119 def update_skill( 

120 self, 

121 skill_name: str, 

122 new_name: str = None, 

123 project_name: str = default_project, 

124 type: str = None, 

125 params: Dict[str, str] = None, 

126 ): 

127 """ 

128 Updates an existing skill in the database. 

129 

130 Parameters: 

131 skill_name (str): The name of the new skill, or existing skill to update 

132 new_name (str): Updated name of the skill 

133 project_name (str): The containing project 

134 type (str): The type of the skill (e.g. Knowledge Base) 

135 params: (Dict[str, str]): Parameters associated with the skill 

136 

137 Returns: 

138 bot (db.Skills): The updated skill 

139 

140 Raises: 

141 ValueError: If `project_name` does not exist or skill doesn't exist 

142 """ 

143 

144 existing_skill = self.get_skill(skill_name, project_name) 

145 if existing_skill is None: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true

146 raise ValueError(f"Skill with name not found: {skill_name}") 

147 if isinstance(existing_skill.params, dict) and existing_skill.params.get("is_demo") is True: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true

148 raise ValueError("It is forbidden to change properties of the demo object") 

149 

150 if new_name is not None: 150 ↛ 152line 150 didn't jump to line 152 because the condition on line 150 was always true

151 existing_skill.name = new_name 

152 if type is not None: 152 ↛ 154line 152 didn't jump to line 154 because the condition on line 152 was always true

153 existing_skill.type = type 

154 if params is not None: 154 ↛ 165line 154 didn't jump to line 165 because the condition on line 154 was always true

155 # Merge params on update 

156 existing_params = {} if not existing_skill.params else existing_skill.params 

157 existing_params.update(params) 

158 # Remove None values entirely. 

159 params = {k: v for k, v in existing_params.items() if v is not None} 

160 existing_skill.params = params 

161 # Some versions of SQL Alchemy won't handle JSON updates correctly without this. 

162 # See: https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.attributes.flag_modified 

163 flag_modified(existing_skill, "params") 

164 

165 db.session.commit() 

166 

167 return existing_skill 

168 

169 def delete_skill(self, skill_name: str, project_name: str = default_project, strict_case: bool = False): 

170 """ 

171 Deletes a skill by name. 

172 

173 Parameters: 

174 skill_name (str): The name of the skill to delete 

175 project_name (str): The name of the containing project 

176 strict_case (bool): If true, then skill_name is case sensitive 

177 

178 Raises: 

179 ValueError: If `project_name` does not exist or skill doesn't exist 

180 """ 

181 

182 skill = self.get_skill(skill_name, project_name, strict_case) 

183 if skill is None: 183 ↛ 184line 183 didn't jump to line 184 because the condition on line 183 was never true

184 raise ValueError(f"Skill with name doesn't exist: {skill_name}") 

185 if isinstance(skill.params, dict) and skill.params.get("is_demo") is True: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true

186 raise ValueError("Unable to delete demo object") 

187 skill.deleted_at = datetime.datetime.now() 

188 db.session.commit()