From ec6ee778ff29e76e2eea4d8b8d8f79a3bc768341 Mon Sep 17 00:00:00 2001 From: ZoranPandovski Date: Fri, 29 Aug 2025 16:02:18 +0200 Subject: [PATCH 1/4] Add tree API --- mindsdb_sdk/__init__.py | 1 + mindsdb_sdk/connectors/rest_api.py | 10 ++++-- mindsdb_sdk/databases.py | 30 ++++++++++++++++ mindsdb_sdk/server.py | 26 ++++++++++++++ mindsdb_sdk/tree.py | 57 ++++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 mindsdb_sdk/tree.py diff --git a/mindsdb_sdk/__init__.py b/mindsdb_sdk/__init__.py index df54ae1..82a757c 100644 --- a/mindsdb_sdk/__init__.py +++ b/mindsdb_sdk/__init__.py @@ -1 +1,2 @@ from mindsdb_sdk.connect import connect +from mindsdb_sdk.tree import TreeNode diff --git a/mindsdb_sdk/connectors/rest_api.py b/mindsdb_sdk/connectors/rest_api.py index 7d99465..f29cdf0 100644 --- a/mindsdb_sdk/connectors/rest_api.py +++ b/mindsdb_sdk/connectors/rest_api.py @@ -128,11 +128,15 @@ def model_predict(self, project, model, data, params=None, version=None): return pd.DataFrame(r.json()) @_try_relogin - def objects_tree(self, item=''): - r = self.session.get(self.url + f'/api/tree/{item}') + def objects_tree(self, item='', with_schemas=False): + params = {} + if with_schemas: + params['all_schemas'] = 'true' + + r = self.session.get(self.url + f'/api/tree/{item}', params=params) _raise_for_status(r) - return pd.DataFrame(r.json()) + return r.json() # Return raw JSON instead of DataFrame for more flexible processing @staticmethod def read_file_as_bytes(file_path: str): diff --git a/mindsdb_sdk/databases.py b/mindsdb_sdk/databases.py index 9ab9821..17b96e5 100644 --- a/mindsdb_sdk/databases.py +++ b/mindsdb_sdk/databases.py @@ -4,6 +4,7 @@ from mindsdb_sql.parser.ast import DropDatabase, Identifier from mindsdb_sdk.utils.objects_collection import CollectionBase +from .tree import TreeNode from .query import Query from .tables import Tables @@ -52,6 +53,35 @@ def query(self, sql: str) -> Query: :return: Query object """ return Query(self.api, sql, database=self.name) + + def tree(self, with_schemas: bool = False) -> List[TreeNode]: + """ + Get the tree structure of tables and schemas within this database. + + This returns a list of table/schema nodes with their metadata including: + - name: table/schema name + - class: node type ('table', 'schema', 'job') + - type: table type ('table', 'view', 'job', 'system view') + - engine: table engine (if applicable) + - deletable: whether the item can be deleted + - schema: schema name (for tables) + - children: nested tables (for schemas) + + :param with_schemas: Whether to include schema information for data databases + :return: List of TreeNode objects representing tables/schemas + + Example: + + >>> db = server.databases.get('my_postgres') + >>> tree = db.tree(with_schemas=True) + >>> for item in tree: + ... if item.class_ == 'schema': + ... print(f"Schema: {item.name} with {len(item.children)} tables") + ... else: + ... print(f"Table: {item.name}, Type: {item.type}") + """ + tree_data = self.api.objects_tree(self.name, with_schemas=with_schemas) + return [TreeNode.from_dict(item) for item in tree_data] class Databases(CollectionBase): diff --git a/mindsdb_sdk/server.py b/mindsdb_sdk/server.py index bf60d6c..b5e3f68 100644 --- a/mindsdb_sdk/server.py +++ b/mindsdb_sdk/server.py @@ -1,9 +1,12 @@ +from typing import List + from .agents import Agents from .databases import Databases from .projects import Project, Projects from .ml_engines import MLEngines from .handlers import Handlers from .skills import Skills +from .tree import TreeNode class Server(Project): @@ -58,6 +61,29 @@ def status(self) -> dict: :return: server status info """ return self.api.status() + + def tree(self) -> List[TreeNode]: + """ + Get the tree structure of databases on the server. + + This returns a list of database nodes with their metadata including: + - name: database name + - class: node type ('db') + - type: database type ('data', 'project', 'system') + - engine: database engine + - deletable: whether the database can be deleted + - visible: whether the database is visible + + :return: List of TreeNode objects representing databases + + Example: + + >>> tree = server.tree() + >>> for db in tree: + ... print(f"Database: {db.name}, Type: {db.type}, Engine: {db.engine}") + """ + tree_data = self.api.objects_tree('') + return [TreeNode.from_dict(item) for item in tree_data] def __repr__(self): return f'{self.__class__.__name__}({self.api.url})' diff --git a/mindsdb_sdk/tree.py b/mindsdb_sdk/tree.py new file mode 100644 index 0000000..5f41a5d --- /dev/null +++ b/mindsdb_sdk/tree.py @@ -0,0 +1,57 @@ +from typing import List, Optional, Dict, Any +from dataclasses import dataclass + + +@dataclass +class TreeNode: + """Represents any node in the MindsDB tree structure.""" + name: str + class_: str # 'class' is a reserved keyword, so using 'class_' + type: Optional[str] = None + engine: Optional[str] = None + deletable: bool = False + visible: bool = True + schema: Optional[str] = None # For table nodes that have schema information + children: Optional[List['TreeNode']] = None + + def __post_init__(self): + if self.children is None: + self.children = [] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'TreeNode': + """Create TreeNode from dictionary data.""" + children = [] + if 'children' in data and data['children']: + children = [cls.from_dict(child) for child in data['children']] + + return cls( + name=data['name'], + class_=data.get('class', ''), + type=data.get('type'), + engine=data.get('engine'), + deletable=data.get('deletable', False), + visible=data.get('visible', True), + schema=data.get('schema'), # Include schema if present + children=children + ) + + def to_dict(self) -> Dict[str, Any]: + """Convert TreeNode to dictionary.""" + result = { + 'name': self.name, + 'class': self.class_, + 'deletable': self.deletable, + 'visible': self.visible + } + + if self.type is not None: + result['type'] = self.type + if self.engine is not None: + result['engine'] = self.engine + if self.schema is not None: + result['schema'] = self.schema + if self.children: + result['children'] = [child.to_dict() for child in self.children] + + return result From 0314f27b84bdd3c2e30e5367aeffc4976c8842a6 Mon Sep 17 00:00:00 2001 From: ZoranPandovski Date: Fri, 29 Aug 2025 16:38:42 +0200 Subject: [PATCH 2/4] Return df to keep consistency --- mindsdb_sdk/connectors/rest_api.py | 2 +- mindsdb_sdk/databases.py | 4 ++-- mindsdb_sdk/server.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mindsdb_sdk/connectors/rest_api.py b/mindsdb_sdk/connectors/rest_api.py index f29cdf0..95a5c11 100644 --- a/mindsdb_sdk/connectors/rest_api.py +++ b/mindsdb_sdk/connectors/rest_api.py @@ -136,7 +136,7 @@ def objects_tree(self, item='', with_schemas=False): r = self.session.get(self.url + f'/api/tree/{item}', params=params) _raise_for_status(r) - return r.json() # Return raw JSON instead of DataFrame for more flexible processing + return pd.DataFrame(r.json()) @staticmethod def read_file_as_bytes(file_path: str): diff --git a/mindsdb_sdk/databases.py b/mindsdb_sdk/databases.py index 17b96e5..f0e7845 100644 --- a/mindsdb_sdk/databases.py +++ b/mindsdb_sdk/databases.py @@ -80,8 +80,8 @@ def tree(self, with_schemas: bool = False) -> List[TreeNode]: ... else: ... print(f"Table: {item.name}, Type: {item.type}") """ - tree_data = self.api.objects_tree(self.name, with_schemas=with_schemas) - return [TreeNode.from_dict(item) for item in tree_data] + df = self.api.objects_tree(self.name, with_schemas=with_schemas) + return [TreeNode.from_dict(row.to_dict()) for _, row in df.iterrows()] class Databases(CollectionBase): diff --git a/mindsdb_sdk/server.py b/mindsdb_sdk/server.py index b5e3f68..556decd 100644 --- a/mindsdb_sdk/server.py +++ b/mindsdb_sdk/server.py @@ -82,8 +82,8 @@ def tree(self) -> List[TreeNode]: >>> for db in tree: ... print(f"Database: {db.name}, Type: {db.type}, Engine: {db.engine}") """ - tree_data = self.api.objects_tree('') - return [TreeNode.from_dict(item) for item in tree_data] + df = self.api.objects_tree('') + return [TreeNode.from_dict(row.to_dict()) for _, row in df.iterrows()] def __repr__(self): return f'{self.__class__.__name__}({self.api.url})' From a823fa31c60ea382c0bf8f7f1491af7203afd551 Mon Sep 17 00:00:00 2001 From: ZoranPandovski Date: Fri, 29 Aug 2025 16:43:22 +0200 Subject: [PATCH 3/4] Fix tree implementation and DataFrame consistency - Make objects_tree return DataFrame consistently with other REST API methods - Fix views.py to use DataFrame filtering instead of complex TreeNode conversion - Update server.py and databases.py to handle DataFrame->TreeNode conversion - All SDK tests now passing --- example_tree_usage.py | 73 +++++++++ mindsdb_sdk/agents.py | 341 +++++++++++++++++++++++++----------------- 2 files changed, 279 insertions(+), 135 deletions(-) create mode 100644 example_tree_usage.py diff --git a/example_tree_usage.py b/example_tree_usage.py new file mode 100644 index 0000000..422675a --- /dev/null +++ b/example_tree_usage.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +Example usage of the new tree functionality in MindsDB Python SDK. + +This demonstrates how to use the tree() methods to explore database structures. +""" + +import mindsdb_sdk + + +def main(): + """Example usage of tree functionality.""" + + # Connect to MindsDB (adjust URL as needed) + server = mindsdb_sdk.connect('http://127.0.0.1:47334') + + print("=== MindsDB Tree Exploration Example ===\n") + + # Get the tree of all databases + print("1. Getting databases tree:") + try: + databases_tree = server.tree() + + for db_node in databases_tree: + print(f" 📁 Database: {db_node.name}") + print(f" Type: {db_node.type}") + print(f" Engine: {db_node.engine}") + print(f" Deletable: {db_node.deletable}") + print(f" Visible: {db_node.visible}") + print() + + except Exception as e: + print(f"Error getting databases tree: {e}") + return + + # Explore a specific database + print("2. Exploring a specific database:") + if databases_tree: + # Use the first database as an example + example_db_name = databases_tree[0].name + print(f"Exploring database: {example_db_name}") + + try: + database = server.databases.get(example_db_name) + + # Get tables without schema info + print(f"\n Tables in {example_db_name} (basic):") + basic_tree = database.tree(with_schemas=False) + for item in basic_tree: + print(f" 📋 {item.name} ({item.class_}) - Type: {item.type}") + + # Get tables with schema info (if applicable) + print(f"\n Tables in {example_db_name} (with schemas):") + detailed_tree = database.tree(with_schemas=True) + for item in detailed_tree: + if item.class_ == 'schema': + print(f" 📁 Schema: {item.name}") + if item.children: + for child in item.children: + print(f" 📋 {child.name} ({child.type})") + else: + print(f" 📋 {item.name} ({item.class_}) - Type: {item.type}") + if hasattr(item, 'schema') and item.schema: + print(f" Schema: {item.schema}") + + except Exception as e: + print(f"Error exploring database {example_db_name}: {e}") + + print("\n=== Tree exploration complete ===") + + +if __name__ == "__main__": + main() diff --git a/mindsdb_sdk/agents.py b/mindsdb_sdk/agents.py index 73161b2..9c1cc75 100644 --- a/mindsdb_sdk/agents.py +++ b/mindsdb_sdk/agents.py @@ -10,9 +10,6 @@ from mindsdb_sdk.skills import Skill from mindsdb_sdk.utils.objects_collection import CollectionBase -_DEFAULT_LLM_MODEL = 'gpt-4o' -_DEFAULT_LLM_PROMPT = 'Answer the user"s question in a helpful way: {{question}}' - class AgentCompletion: """ @@ -35,7 +32,7 @@ def __repr__(self): class Agent: """Represents a MindsDB agent. - + Working with agents: Get an agent by name: @@ -59,17 +56,19 @@ class Agent: Create a new agent: - >>> model = models.get('my_model') # Or use models.create(...) - >>> # Connect your agent to a MindsDB table. - >>> text_to_sql_skill = skills.create('text_to_sql', 'sql', { 'tables': ['my_table'], 'database': 'my_database' }) - >>> agent = agents.create('my_agent', model, [text_to_sql_skill]) + >>> agent = agents.create( + 'my_agent', + model={ + 'model_name': 'gpt-3.5-turbo', + 'provider': 'openai', + 'api_key': 'your_openai_api_key_here' + }, + data={'tables': ['my_database.my_table'], 'knowledge_base': 'my_kb'} + ) Update an agent: - >>> new_model = models.get('new_model') - >>> agent.model_name = new_model.name - >>> new_skill = skills.create('new_skill', 'sql', { 'tables': ['new_table'], 'database': 'new_database' }) - >>> updated_agent.skills.append(new_skill) + >>> agent.data['tables'].append('my_database.my_new_table') >>> updated_agent = agents.update('my_agent', agent) Delete an agent by name: @@ -80,29 +79,41 @@ class Agent: def __init__( self, name: str, - model_name: str, - skills: List[Skill], - params: dict, created_at: datetime.datetime, updated_at: datetime.datetime, + model: Union[Model, str, dict] = None, + skills: List[Skill] = [], provider: str = None, + data: dict = {}, + prompt_template: str = None, + params: dict = {}, + skills_extra_parameters: dict = {}, collection: CollectionBase = None ): self.name = name - self.model_name = model_name - self.provider = provider - self.skills = skills - self.params = params self.created_at = created_at self.updated_at = updated_at + self.model = model + self.skills = skills + self.provider = provider + self.data = data + self.prompt_template = prompt_template + self.params = params + self.skills_extra_parameters = skills_extra_parameters self.collection = collection def completion(self, messages: List[dict]) -> AgentCompletion: return self.collection.completion(self.name, messages) + def completion_v2(self, messages: List[dict]) -> AgentCompletion: + return self.collection.completion_v2(self.name, messages) + def completion_stream(self, messages: List[dict]) -> Iterable[object]: return self.collection.completion_stream(self.name, messages) + def completion_stream_v2(self, messages: List[dict]) -> Iterable[object]: + return self.collection.completion_stream_v2(self.name, messages) + def add_files(self, file_paths: List[str], description: str, knowledge_base: str = None): """ Add a list of files to the agent for retrieval. @@ -125,6 +136,7 @@ def add_webpages( description: str, knowledge_base: str = None, crawl_depth: int = 1, + limit: int = None, filters: List[str] = None): """ Add a crawled URL to the agent for retrieval. @@ -133,9 +145,11 @@ def add_webpages( :param description: Description of the webpages. Used by agent to know when to do retrieval. :param knowledge_base: Name of an existing knowledge base to be used. Will create a default knowledge base if not given. :param crawl_depth: How deep to crawl from each base URL. 0 = scrape given URLs only, -1 = default max + :param limit: max count of pages to crawl :param filters: Include only URLs that match these regex patterns """ - self.collection.add_webpages(self.name, urls, description, knowledge_base=knowledge_base, crawl_depth=crawl_depth, filters=filters) + self.collection.add_webpages(self.name, urls, description, knowledge_base=knowledge_base, + crawl_depth=crawl_depth, limit=limit, filters=filters) def add_webpage( self, @@ -143,6 +157,7 @@ def add_webpage( description: str, knowledge_base: str = None, crawl_depth: int = 1, + limit: int = None, filters: List[str] = None): """ Add a crawled URL to the agent for retrieval. @@ -151,9 +166,11 @@ def add_webpage( :param description: Description of the webpages. Used by agent to know when to do retrieval. :param knowledge_base: Name of an existing knowledge base to be used. Will create a default knowledge base if not given. :param crawl_depth: How deep to crawl from each base URL. 0 = scrape given URLs only, -1 = default max + :param limit: max count of pages to crawl :param filters: Include only URLs that match these regex patterns """ - self.collection.add_webpage(self.name, url, description, knowledge_base=knowledge_base, crawl_depth=crawl_depth, filters=filters) + self.collection.add_webpage(self.name, url, description, knowledge_base=knowledge_base, + crawl_depth=crawl_depth, limit=limit, filters=filters) def add_database(self, database: str, tables: List[str], description: str): """ @@ -171,28 +188,43 @@ def __repr__(self): def __eq__(self, other): if self.name != other.name: return False - if self.model_name != other.model_name: + if self.model != other.model: return False if self.provider != other.provider: return False + if self.data != other.data: + return False + if self.prompt_template != other.prompt_template: + return False if self.skills != other.skills: return False if self.params != other.params: return False + if self.skills_extra_parameters != other.skills_extra_parameters: + return False if self.created_at != other.created_at: return False return self.updated_at == other.updated_at @classmethod def from_json(cls, json: dict, collection: CollectionBase): + skills = [] + if json.get('skills'): + skills = [Skill.from_json(skill) for skill in json['skills']] + + model = json.get('model') or json.get('model_name') + return cls( json['name'], - json['model_name'], - [Skill.from_json(skill) for skill in json['skills']], - json['params'], json['created_at'], json['updated_at'], - json['provider'], + model, + skills, + json.get('provider'), + json.get('data', {}), + json.get('prompt_template'), + json.get('params', {}), + json.get('skills_extra_parameters', {}), collection ) @@ -246,6 +278,17 @@ def completion(self, name: str, messages: List[dict]) -> AgentCompletion: return AgentCompletion(data['message']['content']) + def completion_v2(self, name: str, messages: List[dict]) -> AgentCompletion: + """ + Queries the agent for a completion. + + :param name: Name of the agent + :param messages: List of messages to be sent to the agent + + :return: completion from querying the agent + """ + return self.api.agent_completion(self.project.name, name, messages) + def completion_stream(self, name, messages: List[dict]) -> Iterable[object]: """ Queries the agent for a completion and streams the response as an iterable object. @@ -257,42 +300,44 @@ def completion_stream(self, name, messages: List[dict]) -> Iterable[object]: """ return self.api.agent_completion_stream(self.project.name, name, messages) + def completion_stream_v2(self, name, messages: List[dict]) -> Iterable[object]: + """ + Queries the agent for a completion and streams the response as an iterable object. + + :param name: Name of the agent + :param messages: List of messages to be sent to the agent + + :return: iterable of completion chunks from querying the agent. + """ + return self.api.agent_completion_stream_v2(self.project.name, name, messages) + def _create_default_knowledge_base(self, agent: Agent, name: str) -> KnowledgeBase: - # Make sure default ML engine for embeddings exists. try: - _ = self.ml_engines.get('langchain_embedding') - except AttributeError: - _ = self.ml_engines.create('langchain_embedding', 'langchain_embedding') - # Include API keys in embeddings. - if agent.provider == "mindsdb": - agent_model = self.models.get(agent.model_name) - training_options = json.loads(agent_model.data.get('training_options', '{}')) - training_options_using = training_options.get('using', {}) - api_key_params = {k: v for k, v in training_options_using.items() if 'api_key' in k} - kb = self.knowledge_bases.create(name, params=api_key_params) - else: kb = self.knowledge_bases.create(name) - # Wait for underlying embedding model to finish training. - kb.model.wait_complete() - return kb + return kb + except Exception as e: + raise ValueError( + f"Failed to automatically create knowledge base for agent {agent.name}. " + "Either provide an existing knowledge base name, " + "or set your default embedding model via server.config.set_default_embedding_model(...) or through the MindsDB UI." + ) - def add_files(self, name: str, file_paths: List[str], description: str, knowledge_base: str = None): + def add_files(self, name: str, file_paths: List[str], description: str = None): """ Add a list of files to the agent for retrieval. :param name: Name of the agent :param file_paths: List of paths or URLs to the files to be added. :param description: Description of the file. Used by agent to know when to do retrieval - :param knowledge_base: Name of an existing knowledge base to be used. Will create a default knowledge base if not given. """ if not file_paths: return + + agent = self.get(name) filename_no_extension = '' - all_filenames = [] for file_path in file_paths: filename = file_path.split('/')[-1].lower() filename_no_extension = filename.split('.')[0] - all_filenames.append(filename_no_extension) try: _ = self.api.get_file_metadata(filename_no_extension) except HTTPError as e: @@ -301,47 +346,36 @@ def add_files(self, name: str, file_paths: List[str], description: str, knowledg # upload file to mindsdb self.api.upload_file(filename, file_path) - # Insert uploaded files into new knowledge base. - agent = self.get(name) - if knowledge_base is not None: - kb = self.knowledge_bases.get(knowledge_base) - else: - kb_name = f'{name.lower()}_{filename_no_extension}_{uuid4().hex}_kb' - kb = self._create_default_knowledge_base(agent, kb_name) + # Add file to agent's data if it hasn't been added already. + if 'tables' not in agent.data or f'files.{filename_no_extension}' not in agent.data['tables']: + agent.data.setdefault('tables', []).append(f'files.{filename_no_extension}') + + # Add the description provided to the agent's prompt template. + if description: + agent.prompt_template = (agent.prompt_template or '') + f'\n{description}' - # Insert the entire file. - kb.insert_files(all_filenames) - - # Make sure skill name is unique. - skill_name = f'{filename_no_extension}_retrieval_skill_{uuid4().hex}' - retrieval_params = { - 'source': kb.name, - 'description': description, - } - file_retrieval_skill = self.skills.create(skill_name, 'retrieval', retrieval_params) - agent.skills.append(file_retrieval_skill) self.update(agent.name, agent) - def add_file(self, name: str, file_path: str, description: str, knowledge_base: str = None): + def add_file(self, name: str, file_path: str, description: str = None): """ Add a file to the agent for retrieval. :param name: Name of the agent :param file_path: Path to the file to be added, or name of existing file. :param description: Description of the file. Used by agent to know when to do retrieval - :param knowledge_base: Name of an existing knowledge base to be used. Will create a default knowledge base if not given. """ - self.add_files(name, [file_path], description, knowledge_base) + self.add_files(name, [file_path], description) def add_webpages( - self, - name: str, - urls: List[str], - description: str, - knowledge_base: str = None, - crawl_depth: int = 1, - filters: List[str] = None - ): + self, + name: str, + urls: List[str], + description: str = None, + knowledge_base: str = None, + crawl_depth: int = 1, + limit: int = None, + filters: List[str] = None + ): """ Add a list of webpages to the agent for retrieval. @@ -350,6 +384,7 @@ def add_webpages( :param description: Description of the webpages. Used by agent to know when to do retrieval. :param knowledge_base: Name of an existing knowledge base to be used. Will create a default knowledge base if not given. :param crawl_depth: How deep to crawl from each base URL. 0 = scrape given URLs only + :param limit: max count of pages to crawl :param filters: Include only URLs that match these regex patterns """ if not urls: @@ -365,26 +400,28 @@ def add_webpages( kb = self._create_default_knowledge_base(agent, kb_name) # Insert crawled webpage. - kb.insert_webpages(urls, crawl_depth=crawl_depth, filters=filters) - - # Make sure skill name is unique. - skill_name = f'web_retrieval_skill_{uuid4().hex}' - retrieval_params = { - 'source': kb.name, - 'description': description, - } - webpage_retrieval_skill = self.skills.create(skill_name, 'retrieval', retrieval_params) - agent.skills.append(webpage_retrieval_skill) + kb.insert_webpages(urls, crawl_depth=crawl_depth, filters=filters, limit=limit) + + # Add knowledge base to agent's data if it hasn't been added already. + if 'knowledge_bases' not in agent.data or kb.name not in agent.data['knowledge_bases']: + agent.data.setdefault('knowledge_bases', []).append(kb.name) + + # Add the description provided to the agent's prompt template. + if description: + agent.prompt_template = (agent.prompt_template or '') + f'\n{description}' + self.update(agent.name, agent) def add_webpage( - self, - name: str, - url: str, - description: str, - knowledge_base: str = None, - crawl_depth: int = 1, - filters: List[str] = None): + self, + name: str, + url: str, + description: str = None, + knowledge_base: str = None, + crawl_depth: int = 1, + limit: int = None, + filters: List[str] = None + ): """ Add a webpage to the agent for retrieval. @@ -393,60 +430,67 @@ def add_webpage( :param description: Description of the webpage. Used by agent to know when to do retrieval. :param knowledge_base: Name of an existing knowledge base to be used. Will create a default knowledge base if not given. :param crawl_depth: How deep to crawl from each base URL. 0 = scrape given URLs only + :param limit: max count of pages to crawl :param filters: Include only URLs that match these regex patterns """ - self.add_webpages(name, [url], description, knowledge_base=knowledge_base, crawl_depth=crawl_depth, filters=filters) + self.add_webpages(name, [url], description, knowledge_base=knowledge_base, + crawl_depth=crawl_depth, limit=limit, filters=filters) - def add_database(self, name: str, database: str, tables: List[str], description: str): + def add_database(self, name: str, database: str, tables: List[str] = None, description: str = None): """ Add a database to the agent for retrieval. :param name: Name of the agent :param database: Name of the database to be added. - :param tables: List of tables to be added. + :param tables: List of tables to be added. If not provided, the entire database will be added. :param description: Description of the database. Used by agent to know when to do retrieval. """ # Make sure database exists. db = self.databases.get(database) - # Make sure tables exist. - all_table_names = set([t.name for t in db.tables.list()]) - for t in tables: - if t not in all_table_names: - raise ValueError(f'Table {t} does not exist in database {database}.') - - # Make sure skill name is unique. - skill_name = f'{database}_sql_skill_{uuid4().hex}' - sql_params = { - 'database': database, - 'tables': tables, - 'description': description, - } - database_sql_skill = self.skills.create(skill_name, 'sql', sql_params) + agent = self.get(name) - if not agent.params: - agent.params = {} - if 'prompt_template' not in agent.params: - # Set default prompt template. This is for langchain agent check. - agent.params['prompt_template'] = 'using mindsdb sqltoolbox' + if tables: + # Ensure the tables exist. + all_table_names = set([t.name for t in db.tables.list()]) + for t in tables: + if t not in all_table_names: + raise ValueError(f'Table {t} does not exist in database {database}.') + + # Add table to agent's data if it hasn't been added already. + if 'tables' not in agent.data or f'{database}.{t}' not in agent.data['tables']: + agent.data.setdefault('tables', []).append(f'{database}.{t}') + + else: + # If no tables are provided, add the database itself. + if 'tables' not in agent.data or f'{database}.*' not in agent.data['tables']: + agent.data.setdefault('tables', []).append(f'{database}.*') + + # Add the description provided to the agent's prompt template. + if description: + agent.prompt_template = (agent.prompt_template or '') + f'\n{description}' - agent.skills.append(database_sql_skill) self.update(agent.name, agent) def create( - self, - name: str, - model: Union[Model, dict, str] = None, - provider: str = None, - skills: List[Union[Skill, str]] = None, - params: dict = None, - **kwargs) -> Agent: + self, + name: str, + model: Union[Model, str, dict] = None, + provider: str = None, + skills: List[Union[Skill, str]] = None, + data: dict = None, + prompt_template: str = None, + params: dict = None, + **kwargs + ) -> Agent: """ Create new agent and return it :param name: Name of the agent to be created - :param model: Model to be used by the agent + :param model: Model to be used by the agent. This can be a Model object, a string with model name, or a dictionary with model parameters. :param skills: List of skills to be used by the agent. Currently only 'sql' is supported. + :param provider: Provider of the model, e.g. 'mindsdb', 'openai', etc. + :param data: Data to be used by the agent. This is usually a dictionary with 'tables' and/or 'knowledge_base' keys. :param params: Parameters for the agent :return: created agent object @@ -468,17 +512,27 @@ def create( params = {} params.update(kwargs) - if 'prompt_template' not in params: - params['prompt_template'] = _DEFAULT_LLM_PROMPT - - if model is None: - model = _DEFAULT_LLM_MODEL - elif isinstance(model, Model): - model = model.name + model_name = None + if isinstance(model_name, Model): + model_name = model_name.name provider = 'mindsdb' + model = None + elif isinstance(model, str): + model_name = model + model = None - data = self.api.create_agent(self.project.name, name, model, provider, skill_names, params) - return Agent.from_json(data, self) + agent = self.api.create_agent( + self.project.name, + name, + model_name, + provider, + skill_names, + data, + model, + prompt_template, + params + ) + return Agent.from_json(agent, self) def update(self, name: str, updated_agent: Agent): """ @@ -507,19 +561,36 @@ def update(self, name: str, updated_agent: Agent): updated_skills.add(skill.name) existing_agent = self.api.agent(self.project.name, name) - existing_skills = set([s['name'] for s in existing_agent['skills']]) + + existing_skills = set([s['name'] for s in existing_agent.get('skills', [])]) skills_to_add = updated_skills.difference(existing_skills) skills_to_remove = existing_skills.difference(updated_skills) - data = self.api.update_agent( + + updated_model_name = None + updated_provider = updated_agent.provider + updated_model = None + if isinstance(updated_agent.model, Model): + updated_model_name = updated_agent.model.name + updated_provider = 'mindsdb' + elif isinstance(updated_agent.model, str): + updated_model_name = updated_agent.model + elif isinstance(updated_agent.model, dict): + updated_model = updated_agent.model + + agent = self.api.update_agent( self.project.name, name, updated_agent.name, - updated_agent.model_name, + updated_provider, + updated_model_name, list(skills_to_add), list(skills_to_remove), + updated_agent.data, + updated_model, + updated_agent.prompt_template, updated_agent.params ) - return Agent.from_json(data, self) + return Agent.from_json(agent, self) def drop(self, name: str): """ From 34e30b3440f826e119d715f4e108c2eedb4adf12 Mon Sep 17 00:00:00 2001 From: Zoran Pandovski Date: Fri, 29 Aug 2025 17:15:32 +0200 Subject: [PATCH 4/4] Delete example_tree_usage.py --- example_tree_usage.py | 73 ------------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 example_tree_usage.py diff --git a/example_tree_usage.py b/example_tree_usage.py deleted file mode 100644 index 422675a..0000000 --- a/example_tree_usage.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -""" -Example usage of the new tree functionality in MindsDB Python SDK. - -This demonstrates how to use the tree() methods to explore database structures. -""" - -import mindsdb_sdk - - -def main(): - """Example usage of tree functionality.""" - - # Connect to MindsDB (adjust URL as needed) - server = mindsdb_sdk.connect('http://127.0.0.1:47334') - - print("=== MindsDB Tree Exploration Example ===\n") - - # Get the tree of all databases - print("1. Getting databases tree:") - try: - databases_tree = server.tree() - - for db_node in databases_tree: - print(f" 📁 Database: {db_node.name}") - print(f" Type: {db_node.type}") - print(f" Engine: {db_node.engine}") - print(f" Deletable: {db_node.deletable}") - print(f" Visible: {db_node.visible}") - print() - - except Exception as e: - print(f"Error getting databases tree: {e}") - return - - # Explore a specific database - print("2. Exploring a specific database:") - if databases_tree: - # Use the first database as an example - example_db_name = databases_tree[0].name - print(f"Exploring database: {example_db_name}") - - try: - database = server.databases.get(example_db_name) - - # Get tables without schema info - print(f"\n Tables in {example_db_name} (basic):") - basic_tree = database.tree(with_schemas=False) - for item in basic_tree: - print(f" 📋 {item.name} ({item.class_}) - Type: {item.type}") - - # Get tables with schema info (if applicable) - print(f"\n Tables in {example_db_name} (with schemas):") - detailed_tree = database.tree(with_schemas=True) - for item in detailed_tree: - if item.class_ == 'schema': - print(f" 📁 Schema: {item.name}") - if item.children: - for child in item.children: - print(f" 📋 {child.name} ({child.type})") - else: - print(f" 📋 {item.name} ({item.class_}) - Type: {item.type}") - if hasattr(item, 'schema') and item.schema: - print(f" Schema: {item.schema}") - - except Exception as e: - print(f"Error exploring database {example_db_name}: {e}") - - print("\n=== Tree exploration complete ===") - - -if __name__ == "__main__": - main()