Skip to content

Commit 9485319

Browse files
Merge pull request #2 from MinuraPunchihewa/feature/mandatory_names_and_other_improvements
Updated the Mandatory Parameters and Initialization Logic
2 parents 2b8a53c + 40e2b0e commit 9485319

File tree

3 files changed

+170
-127
lines changed

3 files changed

+170
-127
lines changed

langchain_minds/tools.py

+140-98
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""AIMind tool."""
22

3-
import secrets
4-
from typing import Any, Dict, List, Optional, Text
3+
import os
4+
from typing import Any, Dict, List, Optional, Union
55

66
import openai
77
from langchain_core.callbacks import (
@@ -11,61 +11,155 @@
1111
from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env
1212
from minds.client import Client
1313
from minds.datasources import DatabaseConfig
14+
from minds.exceptions import ObjectNotFound
1415
from pydantic import BaseModel, Field, SecretStr
1516

1617

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+
1732
class AIMindDataSource(BaseModel):
1833
"""
1934
The configuration for data sources used by the AIMindTool.
2035
"""
2136

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"
2643
)
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"
2946
)
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(
3152
default=[],
3253
description="List of tables from the data source to be accessible by the Mind",
3354
)
3455

3556
def __init__(self, **data: Any) -> None:
3657
"""
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.
3971
"""
4072
super().__init__(**data)
4173

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+
)
45131

46132

47133
class AIMindAPIWrapper(BaseModel):
48134
"""
49135
The API wrapper for the Minds API.
50136
"""
51137

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+
)
55145

56146
# Not set by the user, but used internally.
57147
openai_client: Any = Field(default=None, exclude=True)
58148

59149
def __init__(self, **data: Any) -> None:
60150
"""
61151
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.
64154
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.
65159
"""
66160
super().__init__(**data)
67161

68-
# Validate that the API key and base URL are available.
162+
# Validate that the API key is provided.
69163
self.minds_api_key = convert_to_secret_str(
70164
get_from_dict_or_env(
71165
data,
@@ -74,10 +168,6 @@ def __init__(self, **data: Any) -> None:
74168
)
75169
)
76170

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-
81171
# Create an OpenAI client to run queries against the Mind.
82172
self.openai_client = openai.OpenAI(
83173
api_key=self.minds_api_key.get_secret_value(), base_url="https://mdb.ai/"
@@ -89,23 +179,30 @@ def __init__(self, **data: Any) -> None:
89179
# self.minds_api_base
90180
)
91181

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."
102189
)
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)
104200

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)
107204

108-
def run(self, query: Text) -> Text:
205+
def run(self, query: str) -> str:
109206
"""
110207
Run the query against the Minds API and return the response.
111208
"""
@@ -119,67 +216,12 @@ def run(self, query: Text) -> Text:
119216

120217

121218
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+
"""
180222

181223
name: str = "ai_mind"
182-
description: Text = (
224+
description: str = (
183225
"A wrapper around [AI-Minds](https://mindsdb.com/minds). "
184226
"Useful for when you need answers to questions from your data, stored in "
185227
"data sources including PostgreSQL, MySQL, MariaDB, ClickHouse, Snowflake "
@@ -189,7 +231,7 @@ class AIMindTool(BaseTool): # type: ignore[override]
189231
api_wrapper: AIMindAPIWrapper
190232

191233
def _run(
192-
self, query: Text, *, run_manager: Optional[CallbackManagerForToolRun] = None
234+
self, query: str, *, run_manager: Optional[CallbackManagerForToolRun] = None
193235
) -> str:
194236
return self.api_wrapper.run(query)
195237

tests/integration_tests/test_tools.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,12 @@ def tool_constructor(self) -> Type[AIMindTool]:
1212

1313
@property
1414
def tool_constructor_params(self) -> dict:
15-
datasource = AIMindDataSource(
16-
description="house sales",
17-
engine="postgres",
18-
connection_data={
19-
"user": "demo_user",
20-
"password": "demo_password",
21-
"host": "samples.mindsdb.com",
22-
"port": 5432,
23-
"database": "demo",
24-
"schema": "demo_data",
25-
},
26-
tables=["house_sales"],
15+
# Re-uses an existing data source and API wrapper.
16+
# Only the name is required to initialize existing objects.
17+
AIMindDataSource(
18+
name="test_datasource",
2719
)
28-
api_wrapper = AIMindAPIWrapper(datasources=[datasource])
20+
api_wrapper = AIMindAPIWrapper(name="test_mind")
2921
return {"api_wrapper": api_wrapper}
3022

3123
@property

0 commit comments

Comments
 (0)