1
1
"""AIMind tool."""
2
2
3
- import secrets
4
- from typing import Any , Dict , List , Optional , Text
3
+ import os
4
+ from typing import Any , Dict , List , Optional , Union
5
5
6
6
import openai
7
7
from langchain_core .callbacks import (
11
11
from langchain_core .utils import convert_to_secret_str , get_from_dict_or_env
12
12
from minds .client import Client
13
13
from minds .datasources import DatabaseConfig
14
+ from minds .exceptions import ObjectNotFound
14
15
from pydantic import BaseModel , Field , SecretStr
15
16
16
17
18
+ class AIMindEnvVar :
19
+ """
20
+ The loader for environment variables used by the AIMindTool.
21
+ """
22
+
23
+ value : Union [str , SecretStr ]
24
+
25
+ def __init__ (self , name : str , is_secret : bool = False ) -> None :
26
+ if is_secret :
27
+ self .value = convert_to_secret_str (os .environ [name ])
28
+ else :
29
+ self .value = os .environ [name ]
30
+
31
+
17
32
class AIMindDataSource (BaseModel ):
18
33
"""
19
34
The configuration for data sources used by the AIMindTool.
20
35
"""
21
36
22
- name : Optional [Text ] = Field (default = None , description = "Name of the data source" )
23
- engine : Text = Field (description = "Engine (type) of the data source" )
24
- description : Text = Field (
25
- description = "Description of the data contained in the data source"
37
+ name : str = Field (default = None , description = "Name of the data source" )
38
+ minds_api_key : Optional [SecretStr ] = Field (
39
+ default = None , description = "API key for the Minds API"
40
+ )
41
+ engine : Optional [str ] = Field (
42
+ default = None , description = "Engine (type) of the data source"
26
43
)
27
- connection_data : Dict [ Text , Any ] = Field (
28
- description = "Connection parameters to establish a connection to the data source"
44
+ description : Optional [ str ] = Field (
45
+ default = "" , description = "Description of the data contained in the data source"
29
46
)
30
- tables : Optional [List [Text ]] = Field (
47
+ connection_data : Optional [Dict [str , Any ]] = Field (
48
+ default = {},
49
+ description = "Connection parameters to connect to the data source" ,
50
+ )
51
+ tables : Optional [List [str ]] = Field (
31
52
default = [],
32
53
description = "List of tables from the data source to be accessible by the Mind" ,
33
54
)
34
55
35
56
def __init__ (self , ** data : Any ) -> None :
36
57
"""
37
- Initializes the configuration for data sources to be used by the Mind.
38
- Sets the name if not provided.
58
+ Initializes the data source configuration.
59
+ Validates the API key is available and the name is set.
60
+ Creates the data source if it does not exist.
61
+
62
+ There are two ways to initialize the data source:
63
+ 1. If the data source already exists, only the name is required.
64
+ 2. If the data source does not exist, the following are required:
65
+ - name
66
+ - engine
67
+ - description
68
+ - connection_data
69
+
70
+ The tables are optional and can be provided if the data source does not exist.
39
71
"""
40
72
super ().__init__ (** data )
41
73
42
- # If a name is not provided, generate a random one.
43
- if not self .name :
44
- self .name = f"lc_datasource_{ secrets .token_hex (5 )} "
74
+ # Validate that the API key is provided.
75
+ self .minds_api_key = convert_to_secret_str (
76
+ get_from_dict_or_env (
77
+ data ,
78
+ "minds_api_key" ,
79
+ "MINDS_API_KEY" ,
80
+ )
81
+ )
82
+
83
+ # Create a Minds client.
84
+ minds_client = Client (
85
+ self .minds_api_key .get_secret_value (),
86
+ # self.minds_api_base
87
+ )
88
+
89
+ # Check if the data source already exists.
90
+ try :
91
+ # If the data source already exists, only the name is required.
92
+ if minds_client .datasources .get (self .name ) and (
93
+ self .engine or self .description or self .connection_data
94
+ ):
95
+ raise ValueError (
96
+ f"The data source with the name '{ self .name } ' already exists."
97
+ "Only the name is required to initialize an existing data source."
98
+ )
99
+ return
100
+ except ObjectNotFound :
101
+ # If the parameters for creating the data source are not provided,
102
+ # raise an error.
103
+ if not self .engine or not self .connection_data :
104
+ raise ValueError (
105
+ "The required parameters for creating the data source are not"
106
+ " provided."
107
+ )
108
+
109
+ # Convert the parameters set as environment variables to the actual values.
110
+ connection_data = {}
111
+ for key , value in (self .connection_data or {}).items ():
112
+ if isinstance (value , AIMindEnvVar ):
113
+ connection_data [key ] = (
114
+ value .value .get_secret_value ()
115
+ if isinstance (value .value , SecretStr )
116
+ else value .value
117
+ )
118
+ else :
119
+ connection_data [key ] = value
120
+
121
+ # Create the data source.
122
+ minds_client .datasources .create (
123
+ DatabaseConfig (
124
+ name = self .name ,
125
+ engine = self .engine ,
126
+ description = self .description ,
127
+ connection_data = connection_data ,
128
+ tables = self .tables ,
129
+ )
130
+ )
45
131
46
132
47
133
class AIMindAPIWrapper (BaseModel ):
48
134
"""
49
135
The API wrapper for the Minds API.
50
136
"""
51
137
52
- name : Optional [Text ] = Field (default = None )
53
- minds_api_key : SecretStr = Field (default = None )
54
- datasources : List [AIMindDataSource ] = Field (default = None )
138
+ name : str = Field (description = "Name of the Mind" )
139
+ minds_api_key : Optional [SecretStr ] = Field (
140
+ default = None , description = "API key for the Minds API"
141
+ )
142
+ datasources : Optional [List [AIMindDataSource ]] = Field (
143
+ default = [], description = "List of data sources to be accessible by the Mind"
144
+ )
55
145
56
146
# Not set by the user, but used internally.
57
147
openai_client : Any = Field (default = None , exclude = True )
58
148
59
149
def __init__ (self , ** data : Any ) -> None :
60
150
"""
61
151
Initializes the API wrapper for the Minds API.
62
- Validates the API key is available and sets the name if not provided .
63
- Validates the required packages can be imported and creates the Mind .
152
+ Validates the API key is available and the name is set .
153
+ Creates the Mind and adds the data sources to it .
64
154
Initializes the OpenAI client used to interact with the created Mind.
155
+
156
+ There are two ways to initialize the API wrapper:
157
+ 1. If the Mind already exists, only the name is required.
158
+ 2. If the Mind does not exist, data sources are required.
65
159
"""
66
160
super ().__init__ (** data )
67
161
68
- # Validate that the API key and base URL are available .
162
+ # Validate that the API key is provided .
69
163
self .minds_api_key = convert_to_secret_str (
70
164
get_from_dict_or_env (
71
165
data ,
@@ -74,10 +168,6 @@ def __init__(self, **data: Any) -> None:
74
168
)
75
169
)
76
170
77
- # If a name is not provided, generate a random one.
78
- if not self .name :
79
- self .name = f"lc_mind_{ secrets .token_hex (5 )} "
80
-
81
171
# Create an OpenAI client to run queries against the Mind.
82
172
self .openai_client = openai .OpenAI (
83
173
api_key = self .minds_api_key .get_secret_value (), base_url = "https://mdb.ai/"
@@ -89,23 +179,30 @@ def __init__(self, **data: Any) -> None:
89
179
# self.minds_api_base
90
180
)
91
181
92
- # Create DatabaseConfig objects for each data source.
93
- datasources = []
94
- for ds in self .datasources :
95
- datasources .append (
96
- DatabaseConfig (
97
- name = ds .name ,
98
- engine = ds .engine ,
99
- description = ds .description ,
100
- connection_data = ds .connection_data ,
101
- tables = ds .tables ,
182
+ # Check if the Mind already exists.
183
+ try :
184
+ # If the Mind already exists, only the name is required.
185
+ if minds_client .minds .get (self .name ) and self .datasources :
186
+ raise ValueError (
187
+ f"The Mind with the name '{ self .name } ' already exists."
188
+ "Only the name is required to initialize an existing Mind."
102
189
)
103
- )
190
+ return
191
+ except ObjectNotFound :
192
+ # If the data sources are not provided, raise an error.
193
+ if not self .datasources :
194
+ raise ValueError (
195
+ "At least one data source should be configured to create a Mind."
196
+ )
197
+
198
+ # Create the Mind.
199
+ mind = minds_client .minds .create (name = self .name )
104
200
105
- # Create the Mind if it does not exist and set the mind attribute.
106
- minds_client .minds .create (name = self .name , datasources = datasources , replace = True )
201
+ # Add the data sources to the Mind.
202
+ for data_source in self .datasources or []:
203
+ mind .add_datasource (data_source .name )
107
204
108
- def run (self , query : Text ) -> Text :
205
+ def run (self , query : str ) -> str :
109
206
"""
110
207
Run the query against the Minds API and return the response.
111
208
"""
@@ -119,67 +216,12 @@ def run(self, query: Text) -> Text:
119
216
120
217
121
218
class AIMindTool (BaseTool ): # type: ignore[override]
122
- """AIMind tool.
123
-
124
- Setup:
125
- Install ``langchain-minds`` and set environment variable ``MINDS_API_KEY``.
126
-
127
- .. code-block:: bash
128
-
129
- pip install -U langchain-minds
130
- export MINDS_API_KEY="your-api-key"
131
-
132
- Instantiation:
133
- .. code-block:: python
134
- from langchain_minds import AIMindDataSource, AIMindAPIWrapper, AIMindTool
135
-
136
-
137
- # Create a data source that your Mind will have access to.
138
- # To configure additional data sources, simply create additional instances of AIMindDataSource and pass it to the wrapper below.
139
- data_source = AIMindDataSource(
140
- engine="postgres",
141
- description="House sales data",
142
- connection_data={
143
- 'user': 'demo_user',
144
- 'password': 'demo_password',
145
- 'host': 'samples.mindsdb.com',
146
- 'port': 5432,
147
- 'database': 'demo',
148
- 'schema': 'demo_data'
149
- }
150
- tables=["house_sales"],
151
- )
152
-
153
- # Create the wrapper for the Minds API by passing in the data sources created above.
154
- api_wrapper = AIMindAPIWrapper(
155
- datasources=[data_source]
156
- )
157
-
158
- # Create the tool by simply passing in the API wrapper from before.
159
- tool = AIMindTool(api_wrapper=api_wrapper)
160
-
161
- Invocation with args:
162
- .. code-block:: python
163
-
164
- tool.invoke({"query": "How many three-bedroom houses were sold in 2008?"})
165
-
166
- .. code-block:: python
167
-
168
- 'The number of three-bedroom houses sold in 2008 was 8.'
169
-
170
- Invocation with ToolCall:
171
-
172
- .. code-block:: python
173
-
174
- tool.invoke({"args": {"query": "How many three-bedroom houses were sold in 2008?"}, "id": "1", "name": tool.name, "type": "tool_call"})
175
-
176
- .. code-block:: python
177
-
178
- ToolMessage(content='The query has been executed successfully. A total of 8 three-bedroom houses were sold in 2008.', name='ai_mind', tool_call_id='1')
179
- """ # noqa: E501
219
+ """
220
+ The AIMind tool.
221
+ """
180
222
181
223
name : str = "ai_mind"
182
- description : Text = (
224
+ description : str = (
183
225
"A wrapper around [AI-Minds](https://mindsdb.com/minds). "
184
226
"Useful for when you need answers to questions from your data, stored in "
185
227
"data sources including PostgreSQL, MySQL, MariaDB, ClickHouse, Snowflake "
@@ -189,7 +231,7 @@ class AIMindTool(BaseTool): # type: ignore[override]
189
231
api_wrapper : AIMindAPIWrapper
190
232
191
233
def _run (
192
- self , query : Text , * , run_manager : Optional [CallbackManagerForToolRun ] = None
234
+ self , query : str , * , run_manager : Optional [CallbackManagerForToolRun ] = None
193
235
) -> str :
194
236
return self .api_wrapper .run (query )
195
237
0 commit comments