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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 00:36 +0000
1import datetime
2from typing import Dict, List, Optional
4from sqlalchemy import null, func
5from sqlalchemy.orm.attributes import flag_modified
7from mindsdb.interfaces.storage import db
8from mindsdb.interfaces.database.projects import ProjectController
9from mindsdb.utilities.config import config
10from mindsdb.utilities import log
13logger = log.getLogger(__name__)
15default_project = config.get("default_project")
18class SkillsController:
19 """Handles CRUD operations at the database level for Skills"""
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
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.
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.
37 Returns:
38 skill (Optional[db.Skills]): The database skill object
40 Raises:
41 ValueError: If `project_name` does not exist
42 """
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))
54 return query.first()
56 def get_skills(self, project_name: Optional[str]) -> List[dict]:
57 """
58 Gets all skills in a project.
60 Parameters:
61 project_name (Optional[str]): The name of the containing project
63 Returns:
64 all_skills (List[db.Skills]): List of database skill object
66 Raises:
67 ValueError: If `project_name` does not exist
68 """
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]
77 query = db.session.query(db.Skills).filter(
78 db.Skills.project_id.in_(project_ids), db.Skills.deleted_at == null()
79 )
81 return query.all()
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.
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
93 Returns:
94 bot (db.Skills): The created skill
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)
103 skill = self.get_skill(name, project_name, strict_case=True)
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}")
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()
117 return new_skill
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.
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
137 Returns:
138 bot (db.Skills): The updated skill
140 Raises:
141 ValueError: If `project_name` does not exist or skill doesn't exist
142 """
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")
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")
165 db.session.commit()
167 return existing_skill
169 def delete_skill(self, skill_name: str, project_name: str = default_project, strict_case: bool = False):
170 """
171 Deletes a skill by name.
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
178 Raises:
179 ValueError: If `project_name` does not exist or skill doesn't exist
180 """
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()