From 31c305ebc86d152d9805edc884d5e09420c71e71 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 15:54:52 +0530 Subject: [PATCH 01/24] added versioning to the API client --- minds/client.py | 4 ++-- minds/rest_api.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/minds/client.py b/minds/client.py index b7b7fcd..75553fb 100644 --- a/minds/client.py +++ b/minds/client.py @@ -8,9 +8,9 @@ class Client: - def __init__(self, api_key, base_url=None): + def __init__(self, api_key, base_url=None, version=None): - self.api = RestAPI(api_key, base_url) + self.api = RestAPI(api_key, base_url, version) self.datasources = Datasources(self) self.knowledge_bases = KnowledgeBases(self) diff --git a/minds/rest_api.py b/minds/rest_api.py index 95ff625..32845ad 100644 --- a/minds/rest_api.py +++ b/minds/rest_api.py @@ -18,13 +18,15 @@ def _raise_for_status(response): class RestAPI: - def __init__(self, api_key, base_url=None): + def __init__(self, api_key, base_url=None, version=None): if base_url is None: base_url = 'https://mdb.ai' base_url = base_url.rstrip('/') if not base_url.endswith('/api'): base_url = base_url + '/api' + if version is not None: + base_url = base_url + '/' + version.lstrip('/') self.api_key = api_key self.base_url = base_url From ec5ba824d81fc20b2872643a41373f4c4decae67 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 16:30:14 +0530 Subject: [PATCH 02/24] removed uses of projects/ in base URL --- minds/minds.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/minds/minds.py b/minds/minds.py index 75679d2..2f11c96 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -21,7 +21,6 @@ def __init__( ): self.api = client.api self.client = client - self.project = 'mindsdb' self.name = name self.model_name = model_name @@ -116,7 +115,7 @@ def update( data['parameters']['prompt_template'] = prompt_template self.api.patch( - f'/projects/{self.project}/minds/{self.name}', + f'/minds/{self.name}', data=data ) @@ -252,8 +251,6 @@ def __init__(self, client): self.api = client.api self.client = client - self.project = 'mindsdb' - def list(self) -> List[Mind]: """ Returns list of minds @@ -261,7 +258,7 @@ def list(self) -> List[Mind]: :return: iterable """ - data = self.api.get(f'/projects/{self.project}/minds').json() + data = self.api.get(f'/minds').json() minds_list = [] for item in data: minds_list.append(Mind(self.client, **item)) @@ -275,7 +272,7 @@ def get(self, name: str) -> Mind: :return: a mind object """ - item = self.api.get(f'/projects/{self.project}/minds/{name}').json() + item = self.api.get(f'/minds/{name}').json() return Mind(self.client, **item) def _check_datasource(self, ds) -> dict: @@ -383,10 +380,10 @@ def create( if update: method = self.api.put - url = f'/projects/{self.project}/minds/{name}' + url = f'/minds/{name}' else: method = self.api.post - url = f'/projects/{self.project}/minds' + url = f'/minds' method( url, @@ -410,4 +407,4 @@ def drop(self, name: str): :param name: name of the mind """ - self.api.delete(f'/projects/{self.project}/minds/{name}') + self.api.delete(f'/minds/{name}') From 7e4f8bb90174a98a5dea9ab497b5e8b5ff8a279e Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 17:32:45 +0530 Subject: [PATCH 03/24] added function to the API client to handle POST with streaming --- minds/rest_api.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/minds/rest_api.py b/minds/rest_api.py index 32845ad..9871785 100644 --- a/minds/rest_api.py +++ b/minds/rest_api.py @@ -1,3 +1,4 @@ +import json import requests import minds.exceptions as exc @@ -59,6 +60,29 @@ def post(self, url, data={}): _raise_for_status(resp) return resp + def post_stream(self, url, data={}): + """Makes a POST request and yields chunks as they arrive (OpenAI-style streaming).""" + with requests.post( + self.base_url + url, + headers=self._headers(), + json=data, + stream=True, + ) as resp: + _raise_for_status(resp) + for line in resp.iter_lines(): + if not line: + continue + decoded = line.decode("utf-8") + if decoded.startswith("data: "): + payload = decoded[len("data: "):] + if payload.strip() == "[DONE]": + break + try: + obj = json.loads(payload) + yield obj + except json.JSONDecodeError: + yield {"error": f"Failed to parse: {decoded}"} + def put(self, url, data={}): resp = requests.put( self.base_url + url, From 37ea7dd09ee7344c0a4c868b0464ba9fbe1b0229 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 18:12:34 +0530 Subject: [PATCH 04/24] updated the operations for Minds --- minds/minds.py | 117 ++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/minds/minds.py b/minds/minds.py index 2f11c96..b179c92 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -1,15 +1,15 @@ from typing import List, Union, Iterable -from openai import OpenAI import minds.utils as utils import minds.exceptions as exc from minds.datasources import Datasource, DatabaseConfig, DatabaseTables, DatabaseConfigBase from minds.knowledge_bases import KnowledgeBase, KnowledgeBaseConfig -DEFAULT_PROMPT_TEMPLATE = 'Use your database tools to answer the user\'s question: {{question}}' class Mind: def __init__( - self, client, name, + self, + client, + name, model_name=None, provider=None, parameters=None, @@ -25,17 +25,9 @@ def __init__( self.name = name self.model_name = model_name self.provider = provider - if parameters is None: - parameters = {} - self.prompt_template = parameters.pop('prompt_template', None) - self.parameters = parameters + self.parameters = parameters if parameters is not None else {} self.created_at = created_at self.updated_at = updated_at - base_url = utils.get_openai_base_url(self.api.base_url) - self.openai_client = OpenAI( - api_key=self.api.api_key, - base_url=base_url - ) self.datasources = datasources self.knowledge_bases = knowledge_bases @@ -54,7 +46,6 @@ def update( name: str = None, model_name: str = None, provider=None, - prompt_template=None, datasources=None, knowledge_bases=None, parameters=None, @@ -77,7 +68,6 @@ def update( :param name: new name of the mind, optional :param model_name: new llm model name, optional :param provider: new llm provider, optional - :param prompt_template: new prompt template, optional :param datasources: alter list of datasources used by mind, optional :param knowledge_bases: alter list of knowledge bases used by mind, optional :param parameters, dict: alter other parameters of the mind, optional @@ -106,15 +96,10 @@ def update( data['model_name'] = model_name if provider is not None: data['provider'] = provider - if parameters is None: - parameters = {} - - data['parameters'] = parameters + if parameters is not None: + data['parameters'] = parameters - if prompt_template is not None: - data['parameters']['prompt_template'] = prompt_template - - self.api.patch( + self.api.put( f'/minds/{self.name}', data=data ) @@ -125,12 +110,11 @@ def update( refreshed_mind = self.client.minds.get(self.name) self.model_name = refreshed_mind.model_name self.provider = refreshed_mind.provider - self.prompt_template = refreshed_mind.prompt_template self.parameters = refreshed_mind.parameters self.created_at = refreshed_mind.created_at self.updated_at = refreshed_mind.updated_at self.datasources = refreshed_mind.datasources - + self.knowledge_bases = refreshed_mind.knowledge_bases def add_datasource(self, datasource: Datasource): """ @@ -142,13 +126,13 @@ def add_datasource(self, datasource: Datasource): :param datasource: input datasource """ - ds_name = self.client.minds._check_datasource(datasource)['name'] + existing_datasources = self.datasources or [] - self.api.post( - f'/projects/{self.project}/minds/{self.name}/datasources', + self.api.put( + f'/minds/{self.name}', data={ - 'name': ds_name, + 'datasources': existing_datasources + [{'name': ds_name}] } ) updated = self.client.minds.get(self.name) @@ -169,8 +153,16 @@ def del_datasource(self, datasource: Union[Datasource, str]): datasource = datasource.name elif not isinstance(datasource, str): raise ValueError(f'Unknown type of datasource: {datasource}') - self.api.delete( - f'/projects/{self.project}/minds/{self.name}/datasources/{datasource}', + + updated_datasources = [ds for ds in (self.datasources or []) if ds['name'] != datasource] + if len(updated_datasources) == len(self.datasources or []): + raise exc.ObjectNotFound(f'Datasource {datasource} not found in mind {self.name}') + + self.api.put( + f'/minds/{self.name}', + data={ + 'datasources': updated_datasources + } ) updated = self.client.minds.get(self.name) @@ -229,21 +221,34 @@ def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[ :return: string if stream mode is off or iterator of ChoiceDelta objects (by openai) """ - response = self.openai_client.chat.completions.create( - model=self.name, - messages=[ - {'role': 'user', 'content': message} - ], - stream=stream - ) if stream: + response = self.api.post_stream( + '/chat/completions', + data={ + 'model': self.name, + 'messages': [ + {'role': 'user', 'content': message} + ], + 'stream': stream + } + ) return self._stream_response(response) else: - return response.choices[0].message.content + response = self.api.post( + '/chat/completions', + data={ + 'model': self.name, + 'messages': [ + {'role': 'user', 'content': message} + ], + 'stream': stream + } + ) + return response.json()['choices'][0]['message']['content'] def _stream_response(self, response): for chunk in response: - yield chunk.choices[0].delta + yield chunk['choices'][0]['delta']['content'] class Minds: @@ -297,7 +302,6 @@ def _check_datasource(self, ds) -> dict: return res - def _check_knowledge_base(self, knowledge_base) -> str: if isinstance(knowledge_base, KnowledgeBase): knowledge_base = knowledge_base.name @@ -314,10 +318,10 @@ def _check_knowledge_base(self, knowledge_base) -> str: return knowledge_base def create( - self, name, + self, + name, model_name=None, provider=None, - prompt_template=None, datasources=None, knowledge_bases=None, parameters=None, @@ -340,7 +344,6 @@ def create( :param name: name of the mind :param model_name: llm model name, optional :param provider: llm provider, optional - :param prompt_template: instructions to llm, optional :param datasources: list of datasources used by mind, optional :param knowledge_bases: alter list of knowledge bases used by mind, optional :param parameters, dict: other parameters of the mind, optional @@ -370,14 +373,6 @@ def create( kb = self._check_knowledge_base(kb) kb_names.append(kb) - if parameters is None: - parameters = {} - - if prompt_template is not None: - parameters['prompt_template'] = prompt_template - if 'prompt_template' not in parameters: - parameters['prompt_template'] = DEFAULT_PROMPT_TEMPLATE - if update: method = self.api.put url = f'/minds/{name}' @@ -385,16 +380,20 @@ def create( method = self.api.post url = f'/minds' + data = { + 'name': name, + 'model_name': model_name, + 'provider': provider, + 'datasources': ds_list, + } + if parameters: + data['parameters'] = parameters + if kb_names: + data['knowledge_bases'] = kb_names + method( url, - data={ - 'name': name, - 'model_name': model_name, - 'provider': provider, - 'parameters': parameters, - 'datasources': ds_list, - 'knowledge_bases': kb_names - } + data=data ) mind = self.get(name) From 4d7e669790fe5723e9d145ed04695704398f1203 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 18:13:13 +0530 Subject: [PATCH 05/24] set the default client version to V1 --- minds/client.py | 2 +- minds/rest_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/minds/client.py b/minds/client.py index 75553fb..a8269e5 100644 --- a/minds/client.py +++ b/minds/client.py @@ -8,7 +8,7 @@ class Client: - def __init__(self, api_key, base_url=None, version=None): + def __init__(self, api_key, base_url=None, version='v1'): self.api = RestAPI(api_key, base_url, version) diff --git a/minds/rest_api.py b/minds/rest_api.py index 9871785..a9b6984 100644 --- a/minds/rest_api.py +++ b/minds/rest_api.py @@ -19,7 +19,7 @@ def _raise_for_status(response): class RestAPI: - def __init__(self, api_key, base_url=None, version=None): + def __init__(self, api_key, base_url=None, version='v1'): if base_url is None: base_url = 'https://mdb.ai' From a119233baa4ee5c02bd096dfdfb147f2340d263c Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 18:35:42 +0530 Subject: [PATCH 06/24] commented out uses of KBs --- minds/minds.py | 152 ++++++++++++++++++++++++------------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/minds/minds.py b/minds/minds.py index b179c92..51cf708 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -14,7 +14,7 @@ def __init__( provider=None, parameters=None, datasources=None, - knowledge_bases=None, + # knowledge_bases=None, created_at=None, updated_at=None, **kwargs @@ -29,7 +29,7 @@ def __init__( self.created_at = created_at self.updated_at = updated_at self.datasources = datasources - self.knowledge_bases = knowledge_bases + # self.knowledge_bases = knowledge_bases def __repr__(self): return (f'Mind(name={self.name}, ' @@ -38,7 +38,7 @@ def __repr__(self): f'created_at="{self.created_at}", ' f'updated_at="{self.updated_at}", ' f'parameters={self.parameters}, ' - f'knowledge_bases={self.knowledge_bases}, ' + # f'knowledge_bases={self.knowledge_bases}, ' f'datasources={self.datasources})') def update( @@ -47,7 +47,7 @@ def update( model_name: str = None, provider=None, datasources=None, - knowledge_bases=None, + # knowledge_bases=None, parameters=None, ): """ @@ -83,12 +83,12 @@ def update( ds_list.append(self.client.minds._check_datasource(ds)) data['datasources'] = ds_list - if knowledge_bases is not None: - kb_names = [] - for kb in knowledge_bases: - kb = self.client.minds._check_knowledge_base(kb) - kb_names.append(kb) - data['knowledge_bases'] = kb_names + # if knowledge_bases is not None: + # kb_names = [] + # for kb in knowledge_bases: + # kb = self.client.minds._check_knowledge_base(kb) + # kb_names.append(kb) + # data['knowledge_bases'] = kb_names if name is not None: data['name'] = name @@ -114,7 +114,7 @@ def update( self.created_at = refreshed_mind.created_at self.updated_at = refreshed_mind.updated_at self.datasources = refreshed_mind.datasources - self.knowledge_bases = refreshed_mind.knowledge_bases + # self.knowledge_bases = refreshed_mind.knowledge_bases def add_datasource(self, datasource: Datasource): """ @@ -168,49 +168,49 @@ def del_datasource(self, datasource: Union[Datasource, str]): self.datasources = updated.datasources - def add_knowledge_base(self, knowledge_base: Union[str, KnowledgeBase, KnowledgeBaseConfig]): - """ - Add knowledge base to mind - Knowledge base can be passed as - - name, str - - Knowledge base object (minds.knowledge_bases.KnowledgeBase) - - Knowledge base config (minds.knowledge_bases.KnowledgeBaseConfig), in this case knowledge base will be created - - :param knowledge_base: input knowledge base - """ - - kb_name = self.client.minds._check_knowledge_base(knowledge_base) - - self.api.post( - f'/projects/{self.project}/minds/{self.name}/knowledge_bases', - data={ - 'name': kb_name, - } - ) - updated = self.client.minds.get(self.name) - - self.knowledge_bases = updated.knowledge_bases - - def del_knowledge_base(self, knowledge_base: Union[KnowledgeBase, str]): - """ - Delete knowledge base from mind - - Knowledge base can be passed as - - name, str - - KnowledgeBase object (minds.knowledge_bases.KnowledgeBase) - - :param knowledge_base: Knowledge base to delete - """ - if isinstance(knowledge_base, KnowledgeBase): - knowledge_base = knowledge_base.name - elif not isinstance(knowledge_base, str): - raise ValueError(f'Unknown type of knowledge base: {knowledge_base}') - self.api.delete( - f'/projects/{self.project}/minds/{self.name}/knowledge_bases/{knowledge_base}', - ) - updated = self.client.minds.get(self.name) - - self.knowledge_bases = updated.knowledge_bases + # def add_knowledge_base(self, knowledge_base: Union[str, KnowledgeBase, KnowledgeBaseConfig]): + # """ + # Add knowledge base to mind + # Knowledge base can be passed as + # - name, str + # - Knowledge base object (minds.knowledge_bases.KnowledgeBase) + # - Knowledge base config (minds.knowledge_bases.KnowledgeBaseConfig), in this case knowledge base will be created + + # :param knowledge_base: input knowledge base + # """ + + # kb_name = self.client.minds._check_knowledge_base(knowledge_base) + + # self.api.post( + # f'/projects/{self.project}/minds/{self.name}/knowledge_bases', + # data={ + # 'name': kb_name, + # } + # ) + # updated = self.client.minds.get(self.name) + + # self.knowledge_bases = updated.knowledge_bases + + # def del_knowledge_base(self, knowledge_base: Union[KnowledgeBase, str]): + # """ + # Delete knowledge base from mind + + # Knowledge base can be passed as + # - name, str + # - KnowledgeBase object (minds.knowledge_bases.KnowledgeBase) + + # :param knowledge_base: Knowledge base to delete + # """ + # if isinstance(knowledge_base, KnowledgeBase): + # knowledge_base = knowledge_base.name + # elif not isinstance(knowledge_base, str): + # raise ValueError(f'Unknown type of knowledge base: {knowledge_base}') + # self.api.delete( + # f'/projects/{self.project}/minds/{self.name}/knowledge_bases/{knowledge_base}', + # ) + # updated = self.client.minds.get(self.name) + + # self.knowledge_bases = updated.knowledge_bases def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[object]]: """ @@ -302,20 +302,20 @@ def _check_datasource(self, ds) -> dict: return res - def _check_knowledge_base(self, knowledge_base) -> str: - if isinstance(knowledge_base, KnowledgeBase): - knowledge_base = knowledge_base.name - elif isinstance(knowledge_base, KnowledgeBaseConfig): - # if not exists - create - try: - self.client.knowledge_bases.get(knowledge_base.name) - except exc.ObjectNotFound: - self.client.knowledge_bases.create(knowledge_base) - - knowledge_base = knowledge_base.name - elif not isinstance(knowledge_base, str): - raise ValueError(f'Unknown type of knowledge base: {knowledge_base}') - return knowledge_base + # def _check_knowledge_base(self, knowledge_base) -> str: + # if isinstance(knowledge_base, KnowledgeBase): + # knowledge_base = knowledge_base.name + # elif isinstance(knowledge_base, KnowledgeBaseConfig): + # # if not exists - create + # try: + # self.client.knowledge_bases.get(knowledge_base.name) + # except exc.ObjectNotFound: + # self.client.knowledge_bases.create(knowledge_base) + + # knowledge_base = knowledge_base.name + # elif not isinstance(knowledge_base, str): + # raise ValueError(f'Unknown type of knowledge base: {knowledge_base}') + # return knowledge_base def create( self, @@ -323,7 +323,7 @@ def create( model_name=None, provider=None, datasources=None, - knowledge_bases=None, + # knowledge_bases=None, parameters=None, replace=False, update=False, @@ -367,11 +367,11 @@ def create( for ds in datasources: ds_list.append(self._check_datasource(ds)) - kb_names = [] - if knowledge_bases: - for kb in knowledge_bases: - kb = self._check_knowledge_base(kb) - kb_names.append(kb) + # kb_names = [] + # if knowledge_bases: + # for kb in knowledge_bases: + # kb = self._check_knowledge_base(kb) + # kb_names.append(kb) if update: method = self.api.put @@ -388,8 +388,8 @@ def create( } if parameters: data['parameters'] = parameters - if kb_names: - data['knowledge_bases'] = kb_names + # if kb_names: + # data['knowledge_bases'] = kb_names method( url, From e583bf0deceb8c4105971a038a8e4d508380befd Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 18:36:16 +0530 Subject: [PATCH 07/24] bumped the major version number --- minds/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minds/__about__.py b/minds/__about__.py index 29e9953..9fcc6cf 100644 --- a/minds/__about__.py +++ b/minds/__about__.py @@ -1,6 +1,6 @@ __title__ = 'minds_sdk' __package_name__ = 'minds' -__version__ = "1.3.3" +__version__ = "2.0.0" __description__ = 'An AI-Data Mind is an LLM with the built-in power to answer data questions for Agents' __email__ = 'hello@mindsdb.com' __author__ = 'MindsDB Inc' From 397c955d45600c4223b8676d8e6ba155ce17e01e Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 19:17:27 +0530 Subject: [PATCH 08/24] removed the OpenAI SDK dependency --- minds/minds.py | 4 ++-- minds/utils.py | 13 ------------- requirements.txt | 1 - 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/minds/minds.py b/minds/minds.py index 51cf708..f00a685 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -212,14 +212,14 @@ def del_datasource(self, datasource: Union[Datasource, str]): # self.knowledge_bases = updated.knowledge_bases - def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[object]]: + def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[str]]: """ Call mind completion :param message: input question :param stream: to enable stream mode - :return: string if stream mode is off or iterator of ChoiceDelta objects (by openai) + :return: string if stream mode is off or a generator of strings if stream mode is on """ if stream: response = self.api.post_stream( diff --git a/minds/utils.py b/minds/utils.py index cea50a0..91ad9a6 100644 --- a/minds/utils.py +++ b/minds/utils.py @@ -2,19 +2,6 @@ import minds.exceptions as exc from urllib.parse import urlparse, urlunparse -def get_openai_base_url(base_url: str) -> str: - parsed = urlparse(base_url) - - netloc = parsed.netloc - if netloc == 'mdb.ai': - llm_host = 'llm.mdb.ai' - else: - llm_host = 'ai.' + netloc - - parsed = parsed._replace(path='', netloc=llm_host) - - return urlunparse(parsed) - def validate_mind_name(mind_name): """ diff --git a/requirements.txt b/requirements.txt index a18fe41..9fc4e94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ pydantic>=2.10 requests -openai >= 1.75.0 From c164700e3136edf63e5bcc3fcef2cbdfd5206c8b Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 19:18:41 +0530 Subject: [PATCH 09/24] commented out the KnowledgeBase client module --- minds/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minds/client.py b/minds/client.py index a8269e5..443c1d9 100644 --- a/minds/client.py +++ b/minds/client.py @@ -13,6 +13,6 @@ def __init__(self, api_key, base_url=None, version='v1'): self.api = RestAPI(api_key, base_url, version) self.datasources = Datasources(self) - self.knowledge_bases = KnowledgeBases(self) + # self.knowledge_bases = KnowledgeBases(self) self.minds = Minds(self) From 07e4122569d35e2c3ede802fcc0433ba0aaab435 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 19:25:04 +0530 Subject: [PATCH 10/24] introduced the overall status field to Minds --- minds/minds.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/minds/minds.py b/minds/minds.py index f00a685..5d5cc67 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -17,6 +17,7 @@ def __init__( # knowledge_bases=None, created_at=None, updated_at=None, + status=None, **kwargs ): self.api = client.api @@ -30,6 +31,7 @@ def __init__( self.updated_at = updated_at self.datasources = datasources # self.knowledge_bases = knowledge_bases + self.status = status def __repr__(self): return (f'Mind(name={self.name}, ' @@ -39,7 +41,8 @@ def __repr__(self): f'updated_at="{self.updated_at}", ' f'parameters={self.parameters}, ' # f'knowledge_bases={self.knowledge_bases}, ' - f'datasources={self.datasources})') + f'datasources={self.datasources}, ' + f'status={self.status})') def update( self, From ae2d48f36c42cb9022c5659a3cb8524d95b2309c Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 2 Oct 2025 23:59:58 +0530 Subject: [PATCH 11/24] made description an optional parameter --- minds/datasources/datasources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py index e26a680..fd5cae0 100644 --- a/minds/datasources/datasources.py +++ b/minds/datasources/datasources.py @@ -25,7 +25,7 @@ class DatabaseConfig(DatabaseConfigBase): Used to define datasource before creating it. """ engine: str - description: str + description: Union[str, None] = '' connection_data: Union[dict, None] = {} From 902ab07a49f1629fb0a64f47f9a4aad434718e91 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Fri, 3 Oct 2025 13:08:05 +0530 Subject: [PATCH 12/24] improved base_url parsing --- minds/rest_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minds/rest_api.py b/minds/rest_api.py index a9b6984..a9c00ef 100644 --- a/minds/rest_api.py +++ b/minds/rest_api.py @@ -24,9 +24,9 @@ def __init__(self, api_key, base_url=None, version='v1'): base_url = 'https://mdb.ai' base_url = base_url.rstrip('/') - if not base_url.endswith('/api'): + if not base_url.endswith('/api') and not base_url.endswith(f'/api/{version}'): base_url = base_url + '/api' - if version is not None: + if version and not base_url.endswith(f'/api/{version}'): base_url = base_url + '/' + version.lstrip('/') self.api_key = api_key self.base_url = base_url From 96cc6931d0c5797314ae82966b11eda5851009f1 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Fri, 3 Oct 2025 18:32:23 +0530 Subject: [PATCH 13/24] reverted completions to run through OpenAI --- minds/minds.py | 33 ++++++++++----------------------- requirements.txt | 2 +- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/minds/minds.py b/minds/minds.py index 5d5cc67..dbd2266 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -222,36 +222,23 @@ def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[ :param message: input question :param stream: to enable stream mode - :return: string if stream mode is off or a generator of strings if stream mode is on + :return: string if stream mode is off or iterator of strings if stream mode is on """ + response = self.openai_client.chat.completions.create( + model=self.name, + messages=[ + {'role': 'user', 'content': message} + ], + stream=stream + ) if stream: - response = self.api.post_stream( - '/chat/completions', - data={ - 'model': self.name, - 'messages': [ - {'role': 'user', 'content': message} - ], - 'stream': stream - } - ) return self._stream_response(response) else: - response = self.api.post( - '/chat/completions', - data={ - 'model': self.name, - 'messages': [ - {'role': 'user', 'content': message} - ], - 'stream': stream - } - ) - return response.json()['choices'][0]['message']['content'] + return response.choices[0].message.content def _stream_response(self, response): for chunk in response: - yield chunk['choices'][0]['delta']['content'] + yield chunk.choices[0].delta.content class Minds: diff --git a/requirements.txt b/requirements.txt index 9fc4e94..4ab63e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pydantic>=2.10 requests - +openai >= 1.75.0 \ No newline at end of file From 02794325fc64dbb8389744289b37eadb58b66449 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 7 Oct 2025 10:55:45 +0530 Subject: [PATCH 14/24] removed unused post_stream() method --- minds/rest_api.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/minds/rest_api.py b/minds/rest_api.py index a9c00ef..d618bd7 100644 --- a/minds/rest_api.py +++ b/minds/rest_api.py @@ -60,29 +60,6 @@ def post(self, url, data={}): _raise_for_status(resp) return resp - def post_stream(self, url, data={}): - """Makes a POST request and yields chunks as they arrive (OpenAI-style streaming).""" - with requests.post( - self.base_url + url, - headers=self._headers(), - json=data, - stream=True, - ) as resp: - _raise_for_status(resp) - for line in resp.iter_lines(): - if not line: - continue - decoded = line.decode("utf-8") - if decoded.startswith("data: "): - payload = decoded[len("data: "):] - if payload.strip() == "[DONE]": - break - try: - obj = json.loads(payload) - yield obj - except json.JSONDecodeError: - yield {"error": f"Failed to parse: {decoded}"} - def put(self, url, data={}): resp = requests.put( self.base_url + url, From 0796d8ef21a944075357f161169f278812f5a576 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 7 Oct 2025 15:21:33 +0530 Subject: [PATCH 15/24] updated Datasource models and operations --- minds/datasources/datasources.py | 102 +++++++++++++------------------ 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py index fd5cae0..cb0f4a9 100644 --- a/minds/datasources/datasources.py +++ b/minds/datasources/datasources.py @@ -1,67 +1,63 @@ -from typing import List, Optional, Union +from typing import List, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel import minds.utils as utils import minds.exceptions as exc -class DatabaseConfigBase(BaseModel): +class Datasource(BaseModel): """ - Base class + Existed datasource. It is returned by this SDK when datasource is queried from server """ name: str - tables: Union[List[str], None] = [] - - -class DatabaseTables(DatabaseConfigBase): - """ - Used when only database and tables are required to be defined. For example in minds.create - """ - ... - - -class DatabaseConfig(DatabaseConfigBase): - """ - Used to define datasource before creating it. - """ engine: str - description: Union[str, None] = '' - connection_data: Union[dict, None] = {} - - -class Datasource(DatabaseConfig): - """ - Existed datasource. It is returned by this SDK when datasource is queried from server - """ - ... + description: Optional[str] = None + connection_data: Optional[dict] = None class Datasources: def __init__(self, client): self.api = client.api - def create(self, ds_config: DatabaseConfig, update=False): - """ - Create new datasource and return it - - :param ds_config: datasource configuration, properties: - - name: str, name of datatasource - - engine: str, type of database handler, for example 'postgres', 'mysql', ... - - description: str, description of the database. Used by mind to know what data can be got from it. - - connection_data: dict, optional, credentials to connect to database - - tables: list of str, optional, list of allowed tables - :param update: if true - to update datasourse if exists, default is false - :return: datasource object + def create( + self, + name: str, + engine: str, + description: Optional[str] = None, + connection_data: Optional[dict] = None, + replace: bool = False, + ): """ + Create new datasource. - name = ds_config.name - + :param name: name of datasource. + :param engine: type of database handler, for example 'postgres', 'mysql', ... + :param description: str, optional, description of the database. Used by mind to know what data can be got from it. + :param connection_data: dict, optional, credentials to connect to database. + :return: Datasource object. + """ utils.validate_datasource_name(name) - if update: - self.api.put(f'/datasources/{name}', data=ds_config.model_dump()) - else: - self.api.post('/datasources', data=ds_config.model_dump()) + if replace: + try: + self.get(name) + self.drop(name) + except exc.ObjectNotFound: + ... + + data = { + 'name': name, + 'engine': engine, + } + if connection_data is not None: + data['connection_data'] = connection_data + if description is not None: + data['description'] = description + + self.api.post( + '/datasources', + data=data + ) return self.get(name) def list(self) -> List[Datasource]: @@ -74,9 +70,6 @@ def list(self) -> List[Datasource]: data = self.api.get('/datasources').json() ds_list = [] for item in data: - # TODO skip not sql skills - if item.get('engine') is None: - continue ds_list.append(Datasource(**item)) return ds_list @@ -89,21 +82,12 @@ def get(self, name: str) -> Datasource: """ data = self.api.get(f'/datasources/{name}').json() - - # TODO skip not sql skills - if data.get('engine') is None: - raise exc.ObjectNotSupported(f'Wrong type of datasource: {name}') return Datasource(**data) - def drop(self, name: str, force=False): + def drop(self, name: str): """ Drop datasource by name :param name: name of datasource - :param force: if True - remove from all minds, default: False """ - data = None - if force: - data = {'cascade': True} - - self.api.delete(f'/datasources/{name}', data=data) + self.api.delete(f'/datasources/{name}') From c589a564a9253cd2c2d28a0803f16fc2c4c276fa Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 12:43:21 +0530 Subject: [PATCH 16/24] updated the Mind models and opreations --- minds/datasources/datasources.py | 4 +- minds/minds.py | 259 +++++++++++-------------------- 2 files changed, 89 insertions(+), 174 deletions(-) diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py index cb0f4a9..dace977 100644 --- a/minds/datasources/datasources.py +++ b/minds/datasources/datasources.py @@ -54,11 +54,11 @@ def create( if description is not None: data['description'] = description - self.api.post( + response = self.api.post( '/datasources', data=data ) - return self.get(name) + return Datasource(**response.json()) def list(self) -> List[Datasource]: """ diff --git a/minds/minds.py b/minds/minds.py index dbd2266..0a96248 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -1,8 +1,9 @@ -from typing import List, Union, Iterable +from openai import OpenAI +from typing import Dict, List, Optional, Union, Iterable + import minds.utils as utils import minds.exceptions as exc -from minds.datasources import Datasource, DatabaseConfig, DatabaseTables, DatabaseConfigBase -from minds.knowledge_bases import KnowledgeBase, KnowledgeBaseConfig +# from minds.knowledge_bases import KnowledgeBase, KnowledgeBaseConfig class Mind: @@ -44,132 +45,38 @@ def __repr__(self): f'datasources={self.datasources}, ' f'status={self.status})') - def update( - self, - name: str = None, - model_name: str = None, - provider=None, - datasources=None, - # knowledge_bases=None, - parameters=None, - ): - """ - Update mind - - If parameter is set it will be applied to mind - - Datasource can be passed as - - name, str - - Datasource object (minds.datasources.Database) - - datasource config (minds.datasources.DatabaseConfig), in this case datasource will be created - - Knowledge base can be passed as - - name, str - - KnowledgeBase object (minds.knowledge_bases.KnowledgeBase) - - Knowledge base config (minds.knowledge_bases.KnowledgeBaseConfig), in this case knowledge base will be created - - :param name: new name of the mind, optional - :param model_name: new llm model name, optional - :param provider: new llm provider, optional - :param datasources: alter list of datasources used by mind, optional - :param knowledge_bases: alter list of knowledge bases used by mind, optional - :param parameters, dict: alter other parameters of the mind, optional - """ - data = {} - - if name is not None: - utils.validate_mind_name(name) - - if datasources is not None: - ds_list = [] - for ds in datasources: - ds_list.append(self.client.minds._check_datasource(ds)) - data['datasources'] = ds_list - - # if knowledge_bases is not None: - # kb_names = [] - # for kb in knowledge_bases: - # kb = self.client.minds._check_knowledge_base(kb) - # kb_names.append(kb) - # data['knowledge_bases'] = kb_names - - if name is not None: - data['name'] = name - if model_name is not None: - data['model_name'] = model_name - if provider is not None: - data['provider'] = provider - if parameters is not None: - data['parameters'] = parameters - - self.api.put( - f'/minds/{self.name}', - data=data - ) - - if name is not None and name != self.name: - self.name = name - - refreshed_mind = self.client.minds.get(self.name) - self.model_name = refreshed_mind.model_name - self.provider = refreshed_mind.provider - self.parameters = refreshed_mind.parameters - self.created_at = refreshed_mind.created_at - self.updated_at = refreshed_mind.updated_at - self.datasources = refreshed_mind.datasources - # self.knowledge_bases = refreshed_mind.knowledge_bases - - def add_datasource(self, datasource: Datasource): + def add_datasource(self, datasource_name: str, tables: Optional[List[str]] = None) -> None: """ - Add datasource to mind - Datasource can be passed as - - name, str - - Datasource object (minds.datasources.Database) - - datasource config (minds.datasources.DatabaseConfig), in this case datasource will be created + Add an existing Datasource to a Mind. - :param datasource: input datasource + :param datasource_name: name of the datasource to add. + :param tables: list of tables to use from the datasource, optional. """ - ds_name = self.client.minds._check_datasource(datasource)['name'] - existing_datasources = self.datasources or [] - - self.api.put( + response = self.api.put( f'/minds/{self.name}', data={ - 'datasources': existing_datasources + [{'name': ds_name}] + 'datasources': self.datasources + [{'name': datasource_name, 'tables': tables}] } ) - updated = self.client.minds.get(self.name) + updated_mind = response.json() + self.datasources = updated_mind['datasources'] + self.status = updated_mind['status'] - self.datasources = updated.datasources - - def del_datasource(self, datasource: Union[Datasource, str]): + def remove_datasource(self, datasource_name: str) -> None: """ - Delete datasource from mind - - Datasource can be passed as - - name, str - - Datasource object (minds.datasources.Database) + Remove a datasource from a Mind. - :param datasource: datasource to delete + :param datasource_name: name of the datasource to remove. """ - if isinstance(datasource, Datasource): - datasource = datasource.name - elif not isinstance(datasource, str): - raise ValueError(f'Unknown type of datasource: {datasource}') - - updated_datasources = [ds for ds in (self.datasources or []) if ds['name'] != datasource] - if len(updated_datasources) == len(self.datasources or []): - raise exc.ObjectNotFound(f'Datasource {datasource} not found in mind {self.name}') - - self.api.put( + response = self.api.put( f'/minds/{self.name}', data={ - 'datasources': updated_datasources + 'datasources': [ds for ds in (self.datasources or []) if ds['name'] != datasource_name] } ) - updated = self.client.minds.get(self.name) - - self.datasources = updated.datasources + updated_mind = response.json() + self.datasources = updated_mind['datasources'] + self.status = updated_mind['status'] # def add_knowledge_base(self, knowledge_base: Union[str, KnowledgeBase, KnowledgeBaseConfig]): # """ @@ -224,7 +131,11 @@ def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[ :return: string if stream mode is off or iterator of strings if stream mode is on """ - response = self.openai_client.chat.completions.create( + openai_client = OpenAI( + api_key=self.api.api_key, + base_url=self.api.base_url + ) + response = openai_client.chat.completions.create( model=self.name, messages=[ {'role': 'user', 'content': message} @@ -236,7 +147,7 @@ def completion(self, message: str, stream: bool = False) -> Union[str, Iterable[ else: return response.choices[0].message.content - def _stream_response(self, response): + def _stream_response(self, response) -> Iterable[str]: for chunk in response: yield chunk.choices[0].delta.content @@ -252,7 +163,6 @@ def list(self) -> List[Mind]: :return: iterable """ - data = self.api.get(f'/minds').json() minds_list = [] for item in data: @@ -266,32 +176,9 @@ def get(self, name: str) -> Mind: :param name: name of the mind :return: a mind object """ - item = self.api.get(f'/minds/{name}').json() return Mind(self.client, **item) - def _check_datasource(self, ds) -> dict: - if isinstance(ds, DatabaseConfigBase): - res = {'name': ds.name} - - if isinstance(ds, DatabaseTables): - if ds.tables: - res['tables'] = ds.tables - - if isinstance(ds, DatabaseConfig): - # if not exists - create - try: - self.client.datasources.get(ds.name) - except exc.ObjectNotFound: - self.client.datasources.create(ds) - - elif isinstance(ds, str): - res = {'name': ds} - else: - raise ValueError(f'Unknown type of datasource: {ds}') - - return res - # def _check_knowledge_base(self, knowledge_base) -> str: # if isinstance(knowledge_base, KnowledgeBase): # knowledge_base = knowledge_base.name @@ -309,22 +196,20 @@ def _check_datasource(self, ds) -> dict: def create( self, - name, - model_name=None, - provider=None, - datasources=None, + name: str, + model_name: Optional[str] = None, + provider: Optional[str] = None, + datasources: Optional[List[Dict[str, Union[str, List[str]]]]] = None, # knowledge_bases=None, parameters=None, replace=False, - update=False, ) -> Mind: """ Create a new mind and return it - Datasource can be passed as - - name, str - - Datasource object (minds.datasources.Database) - - datasource config (minds.datasources.DatabaseConfig), in this case datasource will be created + Datasources should be a list of dicts with keys: + - name: str + - tables: Optional[List[str]] Knowledge base can be passed as - name, str @@ -338,12 +223,9 @@ def create( :param knowledge_bases: alter list of knowledge bases used by mind, optional :param parameters, dict: other parameters of the mind, optional :param replace: if true - to remove existing mind, default is false - :param update: if true - to update mind if exists, default is false :return: created mind """ - - if name is not None: - utils.validate_mind_name(name) + utils.validate_mind_name(name) if replace: try: @@ -352,48 +234,81 @@ def create( except exc.ObjectNotFound: ... - ds_list = [] - if datasources: - for ds in datasources: - ds_list.append(self._check_datasource(ds)) - # kb_names = [] # if knowledge_bases: # for kb in knowledge_bases: # kb = self._check_knowledge_base(kb) # kb_names.append(kb) - if update: - method = self.api.put - url = f'/minds/{name}' - else: - method = self.api.post - url = f'/minds' - data = { 'name': name, 'model_name': model_name, 'provider': provider, - 'datasources': ds_list, + 'datasources': datasources or [], } if parameters: data['parameters'] = parameters # if kb_names: # data['knowledge_bases'] = kb_names - method( - url, + response = self.api.post( + '/minds', + data=data + ) + + return Mind(self.client, **response.json()) + + def update( + self, + name: str, + new_name: Optional[str] = None, + model_name: Optional[str] = None, + provider: Optional[str] = None, + datasources: Optional[List[Dict[str, Union[str, List[str]]]]] = None, + parameters: Optional[Dict] = None, + ) -> Mind: + """ + Update an existing Mind and return it + + Datasources should be a list of dicts with keys: + - name: str + - tables: Optional[List[str]] + + :param name: name of the mind to update + :param new_name: new name of the mind, optional + :param model_name: llm model name, optional + :param provider: llm provider, optional + :param datasources: list of datasources used by mind, optional + :param parameters, dict: other parameters of the mind, optional + :return: updated mind + """ + utils.validate_mind_name(name) + if new_name: + utils.validate_mind_name(new_name) + + data = {} + if new_name: + data['name'] = new_name + if model_name is not None: + data['model_name'] = model_name + if provider is not None: + data['provider'] = provider + if datasources is not None: + data['datasources'] = datasources + if parameters is not None: + data['parameters'] = parameters + + response = self.api.put( + f'/minds/{name}', data=data ) - mind = self.get(name) - return mind + return Mind(self.client, **response.json()) - def drop(self, name: str): + def drop(self, name: str) -> None: """ Drop mind by name :param name: name of the mind """ - self.api.delete(f'/minds/{name}') From 44ea99a53fdb9044b4e2fa4c291d32c21175358c Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 13:36:25 +0530 Subject: [PATCH 17/24] updated the base usage examples --- examples/base_usage.py | 163 +++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 56 deletions(-) diff --git a/examples/base_usage.py b/examples/base_usage.py index 3296801..100c670 100644 --- a/examples/base_usage.py +++ b/examples/base_usage.py @@ -1,19 +1,22 @@ from minds.client import Client +from openai import OpenAI -# --- connect --- -client = Client("YOUR_API_KEY") -# or use not default server -base_url = 'https://custom_cloud.mdb.ai/' -client = Client("YOUR_API_KEY", base_url) +# Basic Setup and Workflow +# connect +API_KEY = "YOUR_API_KEY" +BASE_URL = 'https://custom_cloud.mdb.ai/api/v1' # optional, if you use custom server -# create datasource -from minds.datasources import DatabaseConfig +client = Client(API_KEY) -postgres_config = DatabaseConfig( +# or with custom base URL +client = Client(API_KEY, base_url=BASE_URL) + +# create Datasource +datasource = client.datasources.create( name='my_datasource', - description='', + description='House sales data', engine='postgres', connection_data={ 'user': 'demo_user', @@ -22,80 +25,128 @@ 'port': 5432, 'database': 'demo', 'schema': 'demo_data' - }, - tables=['', ''] + } ) -# using sample config -from minds.datasources.examples import example_ds - -# create mind -# with datasource at the same time -mind = client.minds.create(name='mind_name', datasources=[postgres_config] ) - -# or separately -datasource = client.datasources.create(postgres_config) -mind = client.minds.create(name='mind_name', datasources=[datasource] ) +# create Mind +mind = client.minds.create( + name='mind_name', + datasources=[ + { + 'name': datasource.name, + 'tables': ['house_sales'] + } + ] +) -# with prompt template -mind = client.minds.create(name='mind_name', prompt_template='You are codding assistant') +# or add to existing Mind +mind = client.minds.create(name='mind_name') +mind.add_datasource(datasource.name, tables=['house_sales']) + +# chat with the Mind using the OpenAI-compatible Completions API (without streaming) +openai_client = OpenAI(api_key=API_KEY, base_url=BASE_URL) +completion = openai_client.chat.completions.create( + model=mind.name, + messages=[ + {'role': 'user', 'content': 'How many three-bedroom houses were sold in 2008?'} + ], + stream=False +) +print(completion.choices[0].message.content) + +# with streaming +completion_stream = openai_client.chat.completions.create( + model=mind.name, + messages=[ + {'role': 'user', 'content': 'How many three-bedroom houses were sold in 2008?'} + ], + stream=True +) +for chunk in completion_stream: + print(chunk.choices[0].delta) -# restrict tables for datasource in context of the mind: -from minds.datasources.datasources import DatabaseTables -datasource = DatabaseTables(name='my_datasource', tables=['table1', 'table1']) -mind = client.minds.create(name='mind_name', datasources=[datasource]) +# or chat with the Mind directly (without streaming) +response = mind.completion('How many three-bedroom houses were sold in 2008?') +print(response) -# or add to existed mind -mind = client.minds.create(name='mind_name') -# by config -mind.add_datasource(postgres_config) -# or by datasource -mind.add_datasource(datasource) +# with streaming +for chunk in mind.completion('How many three-bedroom houses were sold in 2008?', stream=True): + print(chunk) -# --- managing minds --- +# Mind Management # create or replace -mind = client.minds.create(name='mind_name', replace=True, datasources=[postgres_config] ) +mind = client.minds.create( + name='mind_name', + datasources=[ + { + 'name': datasource.name, + 'tables': ['house_sales'] + } + ], + replace=True +) # update -mind.update( - name='mind_name', # is required - datasources=[postgres_config] # it will replace current datasource list +mind = client.minds.update( + name='mind_name', # required + new_name='new_mind_name', # optional + datasources=[ # optional + { + 'name': datasource.name, + 'tables': ['home_rentals'] + } + ], ) # list -print(client.minds.list()) +minds = client.minds.list() -# get by name +# get mind = client.minds.get('mind_name') -# removing datasource -mind.del_datasource(datasource) - -# remove mind +# remove client.minds.drop('mind_name') -# call completion -print(mind.completion('2+3')) - -# stream completion -for chunk in mind.completion('2+3', stream=True): - print(chunk.content) - -# --- managing datasources --- +# Datasource Management # create or replace -datasource = client.datasources.create(postgres_config, replace=True) +datasource = client.datasources.create( + name='my_datasource', + description='House sales data', + engine='postgres', + connection_data={ + 'user': 'demo_user', + 'password': 'demo_password', + 'host': 'samples.mindsdb.com', + 'port': 5432, + 'database': 'demo', + 'schema': 'demo_data' + }, + replace=True +) +# update +datasource = client.datasources.update( + name='my_datasource', + new_name='updated_datasource', + description='Updated House sales data', + connection_data={ + 'user': 'demo_user', + 'password': 'demo_password', + 'host': 'samples.mindsdb.com', + 'port': 5432, + 'database': 'demo', + 'schema': 'demo_data' + } +) # list -print(client.datasources.list()) +datasources = client.datasources.list() # get datasource = client.datasources.get('my_datasource') # remove client.datasources.drop('my_datasource') - - From b9670678627952b5057de9a1179e923ea62811db Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 13:38:24 +0530 Subject: [PATCH 18/24] added the function for updating a Datasource --- minds/datasources/datasources.py | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py index dace977..4a51a07 100644 --- a/minds/datasources/datasources.py +++ b/minds/datasources/datasources.py @@ -59,6 +59,40 @@ def create( data=data ) return Datasource(**response.json()) + + def update( + self, + name: str, + new_name: Optional[str] = None, + description: Optional[str] = None, + connection_data: Optional[dict] = None, + ): + """ + Update existing datasource. + + :param name: name of datasource to update. + :param new_name: new name of datasource. + :param description: str, optional, description of the database. Used by mind to know what data can be got from it. + :param connection_data: dict, optional, credentials to connect to database. + :return: Datasource object. + """ + utils.validate_datasource_name(name) + if new_name is not None: + utils.validate_datasource_name(new_name) + + data = {} + if new_name is not None: + data['name'] = new_name + if connection_data is not None: + data['connection_data'] = connection_data + if description is not None: + data['description'] = description + + response = self.api.put( + f'/datasources/{name}', + data=data + ) + return Datasource(**response.json()) def list(self) -> List[Datasource]: """ From e49a839f6ab8c8de0661909bf3649a88d4124fd1 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 13:46:42 +0530 Subject: [PATCH 19/24] updated several type hints --- minds/datasources/datasources.py | 6 ++++-- minds/minds.py | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py index 4a51a07..19e2e70 100644 --- a/minds/datasources/datasources.py +++ b/minds/datasources/datasources.py @@ -1,8 +1,10 @@ from typing import List, Optional from pydantic import BaseModel -import minds.utils as utils + +from minds.client import Client import minds.exceptions as exc +import minds.utils as utils class Datasource(BaseModel): @@ -16,7 +18,7 @@ class Datasource(BaseModel): class Datasources: - def __init__(self, client): + def __init__(self, client: Client): self.api = client.api def create( diff --git a/minds/minds.py b/minds/minds.py index 0a96248..59b875f 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -1,24 +1,25 @@ from openai import OpenAI from typing import Dict, List, Optional, Union, Iterable -import minds.utils as utils +from minds.client import Client import minds.exceptions as exc +import minds.utils as utils # from minds.knowledge_bases import KnowledgeBase, KnowledgeBaseConfig class Mind: def __init__( self, - client, - name, - model_name=None, - provider=None, - parameters=None, - datasources=None, + client: Client, + name: str, + model_name: str, + provider: str, # knowledge_bases=None, - created_at=None, - updated_at=None, - status=None, + created_at: str, + updated_at: str, + status: str, + datasources: Optional[List[Dict]] = [], + parameters: Optional[Dict] = {}, **kwargs ): self.api = client.api @@ -153,7 +154,7 @@ def _stream_response(self, response) -> Iterable[str]: class Minds: - def __init__(self, client): + def __init__(self, client: Client): self.api = client.api self.client = client From 97248bf95dd7b49b085d31b7d018bc5c1c449a26 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 14:19:34 +0530 Subject: [PATCH 20/24] updated base usage to account for async loader --- examples/base_usage.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/examples/base_usage.py b/examples/base_usage.py index 100c670..61df689 100644 --- a/examples/base_usage.py +++ b/examples/base_usage.py @@ -1,6 +1,9 @@ -from minds.client import Client +import time + from openai import OpenAI +from minds.client import Client + # Basic Setup and Workflow @@ -43,8 +46,25 @@ mind = client.minds.create(name='mind_name') mind.add_datasource(datasource.name, tables=['house_sales']) +# wait until Mind is ready +def wait_for_mind(mind): + status = mind.status + while status != 'COMPLETED': + print(f'Mind status: {status}') + time.sleep(3) + mind = client.minds.get(mind.name) + status = mind.status + + if status == 'FAILED': + raise Exception('Mind creation failed') + + print('Mind creation successful!') + +wait_for_mind(mind) + # chat with the Mind using the OpenAI-compatible Completions API (without streaming) openai_client = OpenAI(api_key=API_KEY, base_url=BASE_URL) + completion = openai_client.chat.completions.create( model=mind.name, messages=[ @@ -63,7 +83,7 @@ stream=True ) for chunk in completion_stream: - print(chunk.choices[0].delta) + print(chunk.choices[0].delta.content) # or chat with the Mind directly (without streaming) response = mind.completion('How many three-bedroom houses were sold in 2008?') @@ -82,11 +102,12 @@ datasources=[ { 'name': datasource.name, - 'tables': ['house_sales'] + 'tables': ['home_rentals'] } ], replace=True ) +wait_for_mind(mind) # update mind = client.minds.update( @@ -99,6 +120,7 @@ } ], ) +wait_for_mind(mind) # list minds = client.minds.list() From 354c2488e1ff97f6ad64d9aabc5732378c198e2f Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 14:19:44 +0530 Subject: [PATCH 21/24] fixed circular imports and added missing params --- minds/datasources/datasources.py | 10 +++++++--- minds/minds.py | 22 +++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/minds/datasources/datasources.py b/minds/datasources/datasources.py index 19e2e70..b883408 100644 --- a/minds/datasources/datasources.py +++ b/minds/datasources/datasources.py @@ -1,11 +1,13 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from pydantic import BaseModel -from minds.client import Client import minds.exceptions as exc import minds.utils as utils +if TYPE_CHECKING: + from minds.client import Client + class Datasource(BaseModel): """ @@ -15,10 +17,12 @@ class Datasource(BaseModel): engine: str description: Optional[str] = None connection_data: Optional[dict] = None + created_at: str + modified_at: str class Datasources: - def __init__(self, client: Client): + def __init__(self, client: 'Client'): self.api = client.api def create( diff --git a/minds/minds.py b/minds/minds.py index 59b875f..90c0c9a 100644 --- a/minds/minds.py +++ b/minds/minds.py @@ -1,22 +1,24 @@ from openai import OpenAI -from typing import Dict, List, Optional, Union, Iterable +from typing import Dict, List, Optional, Union, Iterable, TYPE_CHECKING -from minds.client import Client import minds.exceptions as exc import minds.utils as utils # from minds.knowledge_bases import KnowledgeBase, KnowledgeBaseConfig +if TYPE_CHECKING: + from minds.client import Client + class Mind: def __init__( self, - client: Client, + client: 'Client', name: str, model_name: str, provider: str, # knowledge_bases=None, created_at: str, - updated_at: str, + modified_at: str, status: str, datasources: Optional[List[Dict]] = [], parameters: Optional[Dict] = {}, @@ -30,7 +32,7 @@ def __init__( self.provider = provider self.parameters = parameters if parameters is not None else {} self.created_at = created_at - self.updated_at = updated_at + self.modified_at = modified_at self.datasources = datasources # self.knowledge_bases = knowledge_bases self.status = status @@ -40,7 +42,7 @@ def __repr__(self): f'model_name={self.model_name}, ' f'provider={self.provider}, ' f'created_at="{self.created_at}", ' - f'updated_at="{self.updated_at}", ' + f'modified_at="{self.modified_at}", ' f'parameters={self.parameters}, ' # f'knowledge_bases={self.knowledge_bases}, ' f'datasources={self.datasources}, ' @@ -154,7 +156,7 @@ def _stream_response(self, response) -> Iterable[str]: class Minds: - def __init__(self, client: Client): + def __init__(self, client: 'Client'): self.api = client.api self.client = client @@ -243,10 +245,12 @@ def create( data = { 'name': name, - 'model_name': model_name, - 'provider': provider, 'datasources': datasources or [], } + if model_name: + data['model_name'] = model_name + if provider: + data['provider'] = provider if parameters: data['parameters'] = parameters # if kb_names: From 836fd54d41b2cee4927780b69957b7c277cd7601 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 23:21:01 +0530 Subject: [PATCH 22/24] fixed unit tests --- tests/unit/test_unit.py | 651 +++++++++++++++++++--------------------- 1 file changed, 314 insertions(+), 337 deletions(-) diff --git a/tests/unit/test_unit.py b/tests/unit/test_unit.py index 3f33fa1..f048ec6 100644 --- a/tests/unit/test_unit.py +++ b/tests/unit/test_unit.py @@ -1,24 +1,44 @@ from unittest.mock import Mock from unittest.mock import patch +import pytest +from minds import rest_api +from minds.client import Client +from minds.datasources import Datasource +# from minds.knowledge_bases import EmbeddingConfig, KnowledgeBaseConfig, VectorStoreConfig -from minds.datasources.datasources import DatabaseTables -from minds.datasources.examples import example_ds -from minds.knowledge_bases import EmbeddingConfig, KnowledgeBaseConfig, VectorStoreConfig - - -def get_client(): - from minds.client import Client - return Client(API_KEY) +# patch _raise_for_status for testing +rest_api._raise_for_status = Mock() -from minds import rest_api -# patch _raise_for_status -rest_api._raise_for_status = Mock() +@pytest.fixture +def minds_client(): + """Test client instance""" + return Client('1234567890abc') +@pytest.fixture +def example_ds(): + """Example datasource object (as would be returned from the API)""" + return Datasource( + name='example_ds', + engine='postgres', + description='Minds example database', + connection_data={ + "user": "demo_user", + "password": "demo_password", + "host": "samples.mindsdb.com", + "port": "5432", + "database": "demo", + "schema": "demo_data" + }, + created_at='2024-01-01T00:00:00Z', + modified_at='2024-01-01T00:00:00Z' + ) + + def response_mock(mock, data): def side_effect(*args, **kwargs): r_mock = Mock() @@ -28,9 +48,6 @@ def side_effect(*args, **kwargs): mock.side_effect = side_effect -API_KEY = '1234567890abc' - - class TestDatasources: def _compare_ds(self, ds1, ds2): @@ -38,242 +55,242 @@ def _compare_ds(self, ds1, ds2): assert ds1.engine == ds2.engine assert ds1.description == ds2.description assert ds1.connection_data == ds2.connection_data - assert ds1.tables == ds2.tables + # Note: tables field removed from current Datasource model @patch('requests.get') - @patch('requests.put') @patch('requests.post') - @patch('requests.delete') - def test_create_datasources(self, mock_del, mock_post, mock_put, mock_get): - client = get_client() - response_mock(mock_get, example_ds.model_dump()) + def test_create_datasources(self, mock_post, mock_get, minds_client, example_ds): + response_mock(mock_post, example_ds.model_dump()) # POST response for create + response_mock(mock_get, example_ds.model_dump()) # GET response for replace check + + # Extract config data from example_ds for create call + ds_config = { + 'name': example_ds.name, + 'engine': example_ds.engine, + 'description': example_ds.description, + 'connection_data': example_ds.connection_data + } - ds = client.datasources.create(example_ds) + ds = minds_client.datasources.create(**ds_config) - def check_ds_created(ds, mock_post, url): + def check_ds_created(ds, mock_method, url): self._compare_ds(ds, example_ds) - args, kwargs = mock_post.call_args + args, kwargs = mock_method.call_args - assert kwargs['headers'] == {'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json'} - assert kwargs['json'] == example_ds.model_dump() + assert kwargs['headers'] == {'Authorization': 'Bearer 1234567890abc', 'Content-Type': 'application/json'} + assert kwargs['json'] == ds_config assert args[0] == url - check_ds_created(ds, mock_post, 'https://mdb.ai/api/datasources') + check_ds_created(ds, mock_post, 'https://mdb.ai/api/v1/datasources') - # with update - ds = client.datasources.create(example_ds, update=True) - check_ds_created(ds, mock_put, f'https://mdb.ai/api/datasources/{ds.name}') + # with replace (should still use POST after delete) + ds = minds_client.datasources.create(**ds_config, replace=True) + check_ds_created(ds, mock_post, 'https://mdb.ai/api/v1/datasources') @patch('requests.get') - def test_get_datasource(self, mock_get): - client = get_client() - + def test_get_datasource(self, mock_get, minds_client, example_ds): response_mock(mock_get, example_ds.model_dump()) - ds = client.datasources.get(example_ds.name) + ds = minds_client.datasources.get(example_ds.name) self._compare_ds(ds, example_ds) args, _ = mock_get.call_args - assert args[0].endswith(f'/api/datasources/{example_ds.name}') + assert args[0].endswith(f'/api/v1/datasources/{example_ds.name}') @patch('requests.delete') - def test_delete_datasource(self, mock_del): - client = get_client() - - client.datasources.drop('ds_name') + def test_delete_datasource(self, mock_del, minds_client): + minds_client.datasources.drop('ds_name') args, _ = mock_del.call_args - assert args[0].endswith('/api/datasources/ds_name') + assert args[0].endswith('/api/v1/datasources/ds_name') @patch('requests.get') - def test_list_datasources(self, mock_get): - client = get_client() - + def test_list_datasources(self, mock_get, minds_client, example_ds): response_mock(mock_get, [example_ds.model_dump()]) - ds_list = client.datasources.list() + ds_list = minds_client.datasources.list() assert len(ds_list) == 1 ds = ds_list[0] self._compare_ds(ds, example_ds) args, _ = mock_get.call_args - assert args[0].endswith('/api/datasources') - - -class TestKnowledgeBases: - - def _compare_knowledge_base(self, knowledge_base, config): - assert knowledge_base.name == config.name - - @patch('requests.get') - @patch('requests.post') - def test_create_knowledge_bases(self, mock_post, mock_get): - client = get_client() - - test_embedding_config = EmbeddingConfig( - provider='openai', - model='gpt-4o', - params={ - 'k1': 'v1' - } - ) - test_vector_store_connection_data = { - 'user': 'test_user', - 'password': 'test_password', - 'host': 'boop.mindsdb.com', - 'port': '5432', - 'database': 'test', - } - test_vector_store_config = VectorStoreConfig( - engine='pgvector', - connection_data=test_vector_store_connection_data, - table='test_table' - ) - test_knowledge_base_config = KnowledgeBaseConfig( - name='test_kb', - description='Test knowledge base', - vector_store_config=test_vector_store_config, - embedding_config=test_embedding_config, - params={ - 'k1': 'v1' - } - ) - response_mock(mock_get, test_knowledge_base_config.model_dump()) - - created_knowledge_base = client.knowledge_bases.create(test_knowledge_base_config) - self._compare_knowledge_base(created_knowledge_base, test_knowledge_base_config) - - args, kwargs = mock_post.call_args - - assert kwargs['headers'] == {'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json'} - - expected_create_request = { - 'name': test_knowledge_base_config.name, - 'description': test_knowledge_base_config.description, - 'vector_store': { - 'engine': test_vector_store_config.engine, - 'connection_data': test_vector_store_config.connection_data, - 'table': test_vector_store_config.table - }, - 'embedding_model': { - 'provider': test_embedding_config.provider, - 'name': test_embedding_config.model, - 'k1': 'v1' - }, - 'params': { - 'k1': 'v1' - } - } - - assert kwargs['json'] == expected_create_request - assert args[0] == 'https://mdb.ai/api/knowledge_bases' - - @patch('requests.get') - def test_get_knowledge_base(self, mock_get): - client = get_client() - - test_embedding_config = EmbeddingConfig( - provider='openai', - model='gpt-4o', - params={ - 'k1': 'v1' - } - ) - test_vector_store_connection_data = { - "user": "test_user", - "password": "test_password", - "host": "boop.mindsdb.com", - "port": "5432", - "database": "test", - } - test_vector_store_config = VectorStoreConfig( - engine='pgvector', - connection_data=test_vector_store_connection_data, - table='test_table' - ) - test_knowledge_base_config = KnowledgeBaseConfig( - name='test_kb', - description='Test knowledge base', - vector_store_config=test_vector_store_config, - embedding_config=test_embedding_config - ) - - # Expected response from MindsDB API server. - get_response = { - 'created_at': '2024-11-15', - 'embedding_model': 'test_kb_embeddings', - 'id': 1, - 'name': 'test_kb', - 'params': {}, - 'project_id': 1, - 'updated_at': '2024-11-15', - 'vector_database': 'test_kb_vector_store', - 'vector_database_table': 'test_table' - } - response_mock(mock_get, get_response) - get_knowledge_base = client.knowledge_bases.get(test_knowledge_base_config.name) - self._compare_knowledge_base(get_knowledge_base, test_knowledge_base_config) - - args, _ = mock_get.call_args - assert args[0].endswith(f'/api/knowledge_bases/{test_knowledge_base_config.name}') - - @patch('requests.delete') - def test_delete_knowledge_base(self, mock_del): - client = get_client() - - client.knowledge_bases.drop('test_kb') - - args, _ = mock_del.call_args - assert args[0].endswith('/api/knowledge_bases/test_kb') - - @patch('requests.get') - def test_list_knowledge_bases(self, mock_get): - client = get_client() - - test_embedding_config = EmbeddingConfig( - provider='openai', - model='gpt-4o', - params={ - 'k1': 'v1' - } - ) - test_vector_store_connection_data = { - "user": "test_user", - "password": "test_password", - "host": "boop.mindsdb.com", - "port": "5432", - "database": "test", - } - test_vector_store_config = VectorStoreConfig( - engine='pgvector', - connection_data=test_vector_store_connection_data, - table='test_table' - ) - test_knowledge_base_config = KnowledgeBaseConfig( - name='test_kb', - description='Test knowledge base', - vector_store_config=test_vector_store_config, - embedding_config=test_embedding_config - ) - - # Expected response from MindsDB API server. - get_response = { - 'created_at': '2024-11-15', - 'embedding_model': 'test_kb_embeddings', - 'id': 1, - 'name': 'test_kb', - 'params': {}, - 'project_id': 1, - 'updated_at': '2024-11-15', - 'vector_database': 'test_kb_vector_store', - 'vector_database_table': 'test_table' - } - response_mock(mock_get, [get_response]) - knowledge_base_list = client.knowledge_bases.list() - assert len(knowledge_base_list) == 1 - knowledge_base = knowledge_base_list[0] - self._compare_knowledge_base(knowledge_base, test_knowledge_base_config) - - args, _ = mock_get.call_args - assert args[0].endswith('/api/knowledge_bases') + assert args[0].endswith('/api/v1/datasources') + + +# class TestKnowledgeBases: + +# def _compare_knowledge_base(self, knowledge_base, config): +# assert knowledge_base.name == config.name + +# @patch('requests.get') +# @patch('requests.post') +# def test_create_knowledge_bases(self, mock_post, mock_get): +# client = get_client() + +# test_embedding_config = EmbeddingConfig( +# provider='openai', +# model='gpt-4o', +# params={ +# 'k1': 'v1' +# } +# ) +# test_vector_store_connection_data = { +# 'user': 'test_user', +# 'password': 'test_password', +# 'host': 'boop.mindsdb.com', +# 'port': '5432', +# 'database': 'test', +# } +# test_vector_store_config = VectorStoreConfig( +# engine='pgvector', +# connection_data=test_vector_store_connection_data, +# table='test_table' +# ) +# test_knowledge_base_config = KnowledgeBaseConfig( +# name='test_kb', +# description='Test knowledge base', +# vector_store_config=test_vector_store_config, +# embedding_config=test_embedding_config, +# params={ +# 'k1': 'v1' +# } +# ) +# response_mock(mock_get, test_knowledge_base_config.model_dump()) + +# created_knowledge_base = client.knowledge_bases.create(test_knowledge_base_config) +# self._compare_knowledge_base(created_knowledge_base, test_knowledge_base_config) + +# args, kwargs = mock_post.call_args + +# assert kwargs['headers'] == {'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json'} + +# expected_create_request = { +# 'name': test_knowledge_base_config.name, +# 'description': test_knowledge_base_config.description, +# 'vector_store': { +# 'engine': test_vector_store_config.engine, +# 'connection_data': test_vector_store_config.connection_data, +# 'table': test_vector_store_config.table +# }, +# 'embedding_model': { +# 'provider': test_embedding_config.provider, +# 'name': test_embedding_config.model, +# 'k1': 'v1' +# }, +# 'params': { +# 'k1': 'v1' +# } +# } + +# assert kwargs['json'] == expected_create_request +# assert args[0] == 'https://mdb.ai/api/knowledge_bases' + +# @patch('requests.get') +# def test_get_knowledge_base(self, mock_get): +# client = get_client() + +# test_embedding_config = EmbeddingConfig( +# provider='openai', +# model='gpt-4o', +# params={ +# 'k1': 'v1' +# } +# ) +# test_vector_store_connection_data = { +# "user": "test_user", +# "password": "test_password", +# "host": "boop.mindsdb.com", +# "port": "5432", +# "database": "test", +# } +# test_vector_store_config = VectorStoreConfig( +# engine='pgvector', +# connection_data=test_vector_store_connection_data, +# table='test_table' +# ) +# test_knowledge_base_config = KnowledgeBaseConfig( +# name='test_kb', +# description='Test knowledge base', +# vector_store_config=test_vector_store_config, +# embedding_config=test_embedding_config +# ) + +# # Expected response from MindsDB API server. +# get_response = { +# 'created_at': '2024-11-15', +# 'embedding_model': 'test_kb_embeddings', +# 'id': 1, +# 'name': 'test_kb', +# 'params': {}, +# 'project_id': 1, +# 'updated_at': '2024-11-15', +# 'vector_database': 'test_kb_vector_store', +# 'vector_database_table': 'test_table' +# } +# response_mock(mock_get, get_response) +# get_knowledge_base = client.knowledge_bases.get(test_knowledge_base_config.name) +# self._compare_knowledge_base(get_knowledge_base, test_knowledge_base_config) + +# args, _ = mock_get.call_args +# assert args[0].endswith(f'/api/knowledge_bases/{test_knowledge_base_config.name}') + +# @patch('requests.delete') +# def test_delete_knowledge_base(self, mock_del): +# client = get_client() + +# client.knowledge_bases.drop('test_kb') + +# args, _ = mock_del.call_args +# assert args[0].endswith('/api/knowledge_bases/test_kb') + +# @patch('requests.get') +# def test_list_knowledge_bases(self, mock_get): +# client = get_client() + +# test_embedding_config = EmbeddingConfig( +# provider='openai', +# model='gpt-4o', +# params={ +# 'k1': 'v1' +# } +# ) +# test_vector_store_connection_data = { +# "user": "test_user", +# "password": "test_password", +# "host": "boop.mindsdb.com", +# "port": "5432", +# "database": "test", +# } +# test_vector_store_config = VectorStoreConfig( +# engine='pgvector', +# connection_data=test_vector_store_connection_data, +# table='test_table' +# ) +# test_knowledge_base_config = KnowledgeBaseConfig( +# name='test_kb', +# description='Test knowledge base', +# vector_store_config=test_vector_store_config, +# embedding_config=test_embedding_config +# ) + +# # Expected response from MindsDB API server. +# get_response = { +# 'created_at': '2024-11-15', +# 'embedding_model': 'test_kb_embeddings', +# 'id': 1, +# 'name': 'test_kb', +# 'params': {}, +# 'project_id': 1, +# 'updated_at': '2024-11-15', +# 'vector_database': 'test_kb_vector_store', +# 'vector_database_table': 'test_table' +# } +# response_mock(mock_get, [get_response]) +# knowledge_base_list = client.knowledge_bases.list() +# assert len(knowledge_base_list) == 1 +# knowledge_base = knowledge_base_list[0] +# self._compare_knowledge_base(knowledge_base, test_knowledge_base_config) + +# args, _ = mock_get.call_args +# assert args[0].endswith('/api/knowledge_bases') class TestMinds: @@ -281,189 +298,149 @@ class TestMinds: mind_json = { 'model_name': 'gpt-4o', 'name': 'test_mind', - 'datasources': ['example_ds'], - 'knowledge_bases': ['example_kb'], + 'datasources': [{'name': 'example_ds'}], 'provider': 'openai', 'parameters': { 'prompt_template': "Answer the user's question" }, 'created_at': 'Thu, 26 Sep 2024 13:40:57 GMT', - 'updated_at': 'Thu, 26 Sep 2024 13:40:57 GMT', + 'modified_at': 'Thu, 26 Sep 2024 13:40:57 GMT', + 'status': 'COMPLETED' } - def compare_mind(self, mind, mind_json): + def _compare_mind(self, mind, mind_json): assert mind.name == mind_json['name'] assert mind.model_name == mind_json['model_name'] assert mind.provider == mind_json['provider'] assert mind.parameters == mind_json['parameters'] @patch('requests.get') - @patch('requests.put') @patch('requests.post') @patch('requests.delete') - def test_create(self, mock_del, mock_post, mock_put, mock_get): - client = get_client() - + def test_create(self, mock_del, mock_post, mock_get, minds_client): mind_name = 'test_mind' prompt_template = 'always agree' - datasources = ['my_ds'] - knowledge_bases = ['example_kb'] + datasources = [{'name': 'my_ds'}] provider = 'openai' - response_mock(mock_get, self.mind_json) + response_mock(mock_post, self.mind_json) create_params = { 'name': mind_name, - 'prompt_template': prompt_template, + 'parameters': {'prompt_template': prompt_template}, 'datasources': datasources, - 'knowledge_bases': knowledge_bases + 'provider': provider } - mind = client.minds.create(**create_params) + mind = minds_client.minds.create(**create_params) - def check_mind_created(mind, mock_post, create_params, url): - args, kwargs = mock_post.call_args + def check_mind_created(mind, mock_method, create_params, url): + args, kwargs = mock_method.call_args assert args[0].endswith(url) request = kwargs['json'] - for key in ('name', 'datasources', 'knowledge_bases', 'provider', 'model_name'): - req, param = request.get(key), create_params.get(key) - if key == 'datasources': - param = [{'name': param[0]}] + + # Check basic fields + assert request['name'] == create_params['name'] + if 'datasources' in create_params: + assert request['datasources'] == create_params['datasources'] + if 'provider' in create_params: + assert request['provider'] == create_params['provider'] + if 'parameters' in create_params: + assert request['parameters'] == create_params['parameters'] - assert req == param - - assert create_params.get('prompt_template') == request.get('parameters', {}).get('prompt_template') - - self.compare_mind(mind, self.mind_json) - - check_mind_created(mind, mock_post, create_params, '/api/projects/mindsdb/minds') - - # -- datasource with tables -- - knowledge_bases = ['example_kb'] - provider = 'openai' - - response_mock(mock_get, self.mind_json) - - ds_conf = DatabaseTables( - name='my_db', - tables=['table1', 'table1'], - ) - create_params = { - 'name': mind_name, - 'prompt_template': prompt_template, - 'datasources': [ds_conf], - 'knowledge_bases': knowledge_bases - } - mind = client.minds.create(**create_params) - - def check_mind_created(mind, mock_post, create_params, url): - args, kwargs = mock_post.call_args - assert args[0].endswith(url) - request = kwargs['json'] - for key in ('name', 'datasources', 'knowledge_bases', 'provider', 'model_name'): - req, param = request.get(key), create_params.get(key) - if key == 'datasources': - if param is not None: - ds = param[0] - param = [{'name': ds.name, 'tables': ds.tables}] - else: - param = [] - if key == 'knowledge_bases' and param is None: - param = [] - assert req == param - assert create_params.get('prompt_template') == request.get('parameters', {}).get('prompt_template') - - self.compare_mind(mind, self.mind_json) - - check_mind_created(mind, mock_post, create_params, '/api/projects/mindsdb/minds') + self._compare_mind(mind, self.mind_json) + check_mind_created(mind, mock_post, create_params, '/api/v1/minds') # -- with replace -- create_params = { 'name': mind_name, - 'prompt_template': prompt_template, + 'parameters': {'prompt_template': prompt_template}, 'provider': provider, } - mind = client.minds.create(replace=True, **create_params) + + # Mock the GET request for checking if mind exists (for replace) + response_mock(mock_get, self.mind_json) + + mind = minds_client.minds.create(replace=True, **create_params) # was deleted args, _ = mock_del.call_args - assert args[0].endswith(f'/api/projects/mindsdb/minds/{mind_name}') - - check_mind_created(mind, mock_post, create_params, '/api/projects/mindsdb/minds') + assert args[0].endswith(f'/api/v1/minds/{mind_name}') - # -- with update -- - mock_del.reset_mock() - mind = client.minds.create(update=True, **create_params) - # is not deleted - assert not mock_del.called - - check_mind_created(mind, mock_put, create_params, f'/api/projects/mindsdb/minds/{mind_name}') + check_mind_created(mind, mock_post, create_params, '/api/v1/minds') @patch('requests.get') - @patch('requests.patch') - def test_update(self, mock_patch, mock_get): - client = get_client() - + @patch('requests.put') + def test_update(self, mock_put, mock_get, minds_client): response_mock(mock_get, self.mind_json) - mind = client.minds.get('mind_name') + mind = minds_client.minds.get('mind_name') update_params = dict( - name='mind_name2', - datasources=['ds_name'], - knowledge_bases=['kb_name'], + name='mind_name', # current name (required for update) + new_name='mind_name2', + datasources=[{'name': 'ds_name'}], provider='ollama', model_name='llama', parameters={ 'prompt_template': 'be polite' } ) - mind.update(**update_params) - - args, kwargs = mock_patch.call_args - assert args[0].endswith(f'/api/projects/mindsdb/minds/{self.mind_json["name"]}') - - params = update_params.copy() - params['datasources'] = [{'name': 'ds_name'}] - assert kwargs['json'] == params + + # Mock the PUT response for update + updated_mind_json = self.mind_json.copy() + updated_mind_json.update({ + 'name': 'mind_name2', + 'provider': 'ollama', + 'model_name': 'llama', + 'parameters': {'prompt_template': 'be polite'}, + 'datasources': [{'name': 'ds_name'}] + }) + response_mock(mock_put, updated_mind_json) + + updated_mind = minds_client.minds.update(**update_params) + + args, kwargs = mock_put.call_args + assert args[0].endswith(f'/api/v1/minds/{update_params["name"]}') # Use the name we passed to update + + # Remove current name from expected request + expected_request = update_params.copy() + del expected_request['name'] # name is in URL, not request body + expected_request['name'] = update_params['new_name'] # new_name becomes name in request + del expected_request['new_name'] + + assert kwargs['json'] == expected_request @patch('requests.get') - def test_get(self, mock_get): - client = get_client() - + def test_get(self, mock_get, minds_client): response_mock(mock_get, self.mind_json) - mind = client.minds.get('my_mind') - self.compare_mind(mind, self.mind_json) + mind = minds_client.minds.get('my_mind') + self._compare_mind(mind, self.mind_json) args, _ = mock_get.call_args - assert args[0].endswith('/api/projects/mindsdb/minds/my_mind') + assert args[0].endswith('/api/v1/minds/my_mind') @patch('requests.get') - def test_list(self, mock_get): - client = get_client() - + def test_list(self, mock_get, minds_client): response_mock(mock_get, [self.mind_json]) - minds_list = client.minds.list() + minds_list = minds_client.minds.list() assert len(minds_list) == 1 - self.compare_mind(minds_list[0], self.mind_json) + self._compare_mind(minds_list[0], self.mind_json) args, _ = mock_get.call_args - assert args[0].endswith('/api/projects/mindsdb/minds') + assert args[0].endswith('/api/v1/minds') @patch('requests.delete') - def test_delete(self, mock_del): - client = get_client() - client.minds.drop('my_name') + def test_delete(self, mock_del, minds_client): + minds_client.minds.drop('my_name') args, _ = mock_del.call_args - assert args[0].endswith('/api/projects/mindsdb/minds/my_name') + assert args[0].endswith('/api/v1/minds/my_name') @patch('requests.get') @patch('minds.minds.OpenAI') - def test_completion(self, mock_openai, mock_get): - client = get_client() - + def test_completion(self, mock_openai, mock_get, minds_client): response_mock(mock_get, self.mind_json) - mind = client.minds.get('mind_name') + mind = minds_client.minds.get('mind_name') def openai_completion_f(messages, *args, **kwargs): # echo question @@ -489,6 +466,6 @@ def openai_completion_f(messages, *args, **kwargs): success = False for chunk in mind.completion(question, stream=True): - if question == chunk.content.lower(): + if question == chunk.lower(): success = True assert success is True From bf942296fe10a76903bb080ed4f9588889b224d2 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 23:57:09 +0530 Subject: [PATCH 23/24] fixed integration tests --- tests/integration/test_base_flow.py | 144 +++++++++++++++------------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/tests/integration/test_base_flow.py b/tests/integration/test_base_flow.py index 50e9f8a..e415c36 100644 --- a/tests/integration/test_base_flow.py +++ b/tests/integration/test_base_flow.py @@ -1,16 +1,14 @@ import os -import copy import pytest from minds.client import Client +from minds.exceptions import ObjectNotFound, MindNameInvalid, DatasourceNameInvalid import logging logging.basicConfig(level=logging.DEBUG) -from minds.datasources.examples import example_ds -from minds.datasources import DatabaseConfig, DatabaseTables -from minds.exceptions import ObjectNotFound, MindNameInvalid, DatasourceNameInvalid +# TODO: Validate these tests and ensure coverage def get_client(): @@ -20,6 +18,23 @@ def get_client(): return Client(api_key, base_url=base_url) +def get_example_datasource_config(): + """Get example datasource configuration parameters""" + return { + 'name': 'example_ds', + 'engine': 'postgres', + 'description': 'Minds example database', + 'connection_data': { + "user": "demo_user", + "password": "demo_password", + "host": "samples.mindsdb.com", + "port": "5432", + "database": "demo", + "schema": "demo_data" + } + } + + def test_wrong_api_key(): base_url = 'https://dev.mdb.ai' client = Client('api_key', base_url=base_url) @@ -29,29 +44,30 @@ def test_wrong_api_key(): def test_datasources(): client = get_client() + example_ds_config = get_example_datasource_config() # remove previous object try: - client.datasources.drop(example_ds.name, force=True) + client.datasources.drop(example_ds_config['name']) except ObjectNotFound: ... # create - ds = client.datasources.create(example_ds) - assert ds.name == example_ds.name - ds = client.datasources.create(example_ds, update=True) - assert ds.name == example_ds.name - - valid_ds_name = example_ds.name + ds = client.datasources.create(**example_ds_config) + assert ds.name == example_ds_config['name'] + + # create with replace + ds = client.datasources.create(**example_ds_config, replace=True) + assert ds.name == example_ds_config['name'] + # test invalid datasource name with pytest.raises(DatasourceNameInvalid): - example_ds.name = "invalid-ds-name" - client.datasources.create(example_ds) - - example_ds.name = valid_ds_name + invalid_config = example_ds_config.copy() + invalid_config['name'] = "invalid-ds-name" + client.datasources.create(**invalid_config) # get - ds = client.datasources.get(example_ds.name) + ds = client.datasources.get(example_ds_config['name']) # list ds_list = client.datasources.list() @@ -63,14 +79,13 @@ def test_datasources(): def test_minds(): client = get_client() + example_ds_config = get_example_datasource_config() ds_all_name = 'test_datasource_' # unlimited tables ds_rentals_name = 'test_datasource2_' # limited to home rentals mind_name = 'int_test_mind_' invalid_mind_name = 'mind-123' mind_name2 = 'int_test_mind2_' - prompt1 = 'answer in spanish' - prompt2 = 'you are data expert' # remove previous objects for name in (mind_name, mind_name2): @@ -80,66 +95,69 @@ def test_minds(): ... # prepare datasources - ds_all_cfg = copy.copy(example_ds) - ds_all_cfg.name = ds_all_name - ds_all = client.datasources.create(ds_all_cfg, update=True) + ds_all_config = example_ds_config.copy() + ds_all_config['name'] = ds_all_name + ds_all = client.datasources.create(**ds_all_config, replace=True) - # second datasource - ds_rentals_cfg = copy.copy(example_ds) - ds_rentals_cfg.name = ds_rentals_name - ds_rentals_cfg.tables = ['home_rentals'] + # second datasource + ds_rentals_config = example_ds_config.copy() + ds_rentals_config['name'] = ds_rentals_name + # Note: In the new API, tables are specified when adding datasource to mind, not when creating datasource - # create + # create mind with invalid name should fail with pytest.raises(MindNameInvalid): client.minds.create( invalid_mind_name, - datasources=[ds_all], + datasources=[{'name': ds_all.name}], provider='openai' ) + # create mind mind = client.minds.create( mind_name, - datasources=[ds_all], + datasources=[{'name': ds_all.name}], provider='openai' ) + + # create mind with replace mind = client.minds.create( mind_name, replace=True, - datasources=[ds_all.name, ds_rentals_cfg], - prompt_template=prompt1 - ) - mind = client.minds.create( - mind_name, - update=True, - datasources=[ds_all.name, ds_rentals_cfg], - prompt_template=prompt1 + datasources=[ + {'name': ds_all.name}, + {'name': ds_rentals_name, 'tables': ['home_rentals']} + ] ) + # Create the second datasource that will be used later + ds_rentals = client.datasources.create(**ds_rentals_config, replace=True) + # get mind = client.minds.get(mind_name) assert len(mind.datasources) == 2 - assert mind.prompt_template == prompt1 # list mind_list = client.minds.list() assert len(mind_list) > 0 - # completion with prompt 1 + # completion test answer = mind.completion('say hello') - assert 'hola' in answer.lower() + assert len(answer) > 0 # Just check that we get a response - # rename & update - mind.update( - name=mind_name2, - datasources=[ds_all.name], - prompt_template=prompt2 + # rename & update using client.minds.update + updated_mind = client.minds.update( + name=mind_name, + new_name=mind_name2, + datasources=[{'name': ds_all.name}] ) + assert updated_mind.name == mind_name2 + assert len(updated_mind.datasources) == 1 with pytest.raises(MindNameInvalid): - mind.update( - name=invalid_mind_name, - datasources=[ds_all.name], - prompt_template=prompt2 + client.minds.update( + name=mind_name2, + new_name=invalid_mind_name, + datasources=[{'name': ds_all.name}] ) with pytest.raises(ObjectNotFound): @@ -148,14 +166,13 @@ def test_minds(): mind = client.minds.get(mind_name2) assert len(mind.datasources) == 1 - assert mind.prompt_template == prompt2 # add datasource - mind.add_datasource(ds_rentals_cfg) + mind.add_datasource(ds_rentals.name) assert len(mind.datasources) == 2 - # del datasource - mind.del_datasource(ds_rentals_cfg.name) + # remove datasource + mind.remove_datasource(ds_rentals.name) assert len(mind.datasources) == 1 # ask about data @@ -163,27 +180,18 @@ def test_minds(): assert '5602' in answer.replace(' ', '').replace(',', '') # limit tables - mind.del_datasource(ds_all.name) - mind.add_datasource(ds_rentals_name) + mind.remove_datasource(ds_all.name) + mind.add_datasource(ds_rentals.name, tables=['home_rentals']) assert len(mind.datasources) == 1 check_mind_can_see_only_rentals(mind) - # test ds with limited tables - ds_all_limited = DatabaseTables( - name=ds_all_name, - tables=['home_rentals'] - ) - # mind = client.minds.create( - # 'mind_ds_limited_', - # replace=True, - # datasources=[ds_all], - # prompt_template=prompt2 - # ) - mind.update( + # test ds with limited tables - use client.minds.update instead of DatabaseTables + client.minds.update( name=mind.name, - datasources=[ds_all_limited], + datasources=[{'name': ds_all.name, 'tables': ['home_rentals']}] ) + mind = client.minds.get(mind.name) # refresh mind object check_mind_can_see_only_rentals(mind) # stream completion @@ -196,7 +204,7 @@ def test_minds(): # drop client.minds.drop(mind_name2) client.datasources.drop(ds_all.name) - client.datasources.drop(ds_rentals_cfg.name) + client.datasources.drop(ds_rentals.name) def check_mind_can_see_only_rentals(mind): answer = mind.completion('what is max rental price in home rental?') From 250a2e7c198118210c274fcedc782c7517fbc31e Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 8 Oct 2025 23:59:34 +0530 Subject: [PATCH 24/24] removed unusued examples module --- minds/datasources/examples.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 minds/datasources/examples.py diff --git a/minds/datasources/examples.py b/minds/datasources/examples.py deleted file mode 100644 index 13be5e9..0000000 --- a/minds/datasources/examples.py +++ /dev/null @@ -1,16 +0,0 @@ - -from minds.datasources import DatabaseConfig - -example_ds = DatabaseConfig( - name='example_ds', - engine='postgres', - description='Minds example database', - connection_data={ - "user": "demo_user", - "password": "demo_password", - "host": "samples.mindsdb.com", - "port": "5432", - "database": "demo", - "schema": "demo_data" - } -)