Skip to content

Commit d27146b

Browse files
authored
Develop support for external database as datasource (#42)
1 parent 88fc70b commit d27146b

File tree

15 files changed

+335
-63
lines changed

15 files changed

+335
-63
lines changed

apps/consumers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ async def _respond_to_event(self, text_data):
6666
output_stream = await AppViewSet().run_app_internal_async(self.app_id, self._session_id, request_uuid, request, self.preview)
6767
async for output in output_stream:
6868
if 'errors' in output or 'session' in output:
69-
await self.send(text_data=output)
69+
await self.send(text_data=json.dumps(output))
7070
else:
71-
await self.send(text_data="{\"output\":" + output + '}')
71+
await self.send(text_data=json.dumps({'output': output}))
7272

7373
await self.send(text_data=json.dumps({'event': 'done'}))
7474
except Exception as e:

apps/handlers/app_runnner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ async def stream_output():
219219
await asyncio.sleep(0.0001)
220220
if not metadata_sent:
221221
metadata_sent = True
222-
yield json.dumps({'session': {'id': app_session['uuid']}, 'csp': csp, 'template': template}) + '\n'
222+
yield {'session': {'id': app_session['uuid']}, 'csp': csp, 'template': template}
223223
output = next(output_iter)
224-
yield json.dumps(output) + '\n'
224+
yield output
225225
except StopIteration:
226226
pass
227227
except Exception as e:

apps/tasks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def resync_data_entry_task(datasource: DataSource, entry_data: DataSourceEntry):
114114

115115
def delete_data_source_task(datasource):
116116
datasource_type = datasource.type
117+
if datasource_type.is_external_datasource:
118+
return
117119
datasource_entry_handler_cls = DataSourceTypeFactory.get_datasource_type_handler(
118120
datasource_type,
119121
)

client/src/components/datasource/AddDataSourceModal.jsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,21 @@ export function AddDataSourceModal({
136136
.post("/api/datasources", {
137137
name: dataSourceName,
138138
type: dataSourceType.id,
139+
config: dataSourceType.is_external_datasource ? formData : {},
139140
})
140141
.then((response) => {
141-
const dataSource = response.data;
142-
setDataSources([...dataSources, dataSource]);
143-
axios()
144-
.post(`/api/datasources/${dataSource.uuid}/add_entry`, {
145-
entry_data: formData,
146-
})
147-
.then((response) => {
148-
dataSourceAddedCb(dataSource);
149-
});
142+
// External data sources do not support adding entries
143+
if (!dataSourceType.is_external_datasource) {
144+
const dataSource = response.data;
145+
setDataSources([...dataSources, dataSource]);
146+
axios()
147+
.post(`/api/datasources/${dataSource.uuid}/add_entry`, {
148+
entry_data: formData,
149+
})
150+
.then((response) => {
151+
dataSourceAddedCb(dataSource);
152+
});
153+
}
150154
});
151155
handleCancelCb();
152156
enqueueSnackbar(

client/src/pages/data.jsx

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {
99
Chip,
1010
Grid,
1111
Stack,
12+
Tooltip,
1213
} from "@mui/material";
1314

1415
import { TextareaAutosize } from "@mui/base";
1516

1617
import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
1718
import AddOutlinedIcon from "@mui/icons-material/AddOutlined";
1819
import SyncOutlinedIcon from "@mui/icons-material/SyncOutlined";
20+
import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet";
1921
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
2022
import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined";
2123

@@ -192,51 +194,64 @@ export default function DataPage() {
192194
{
193195
title: "Action",
194196
key: "operation",
195-
render: (record) => (
196-
<Box>
197-
<IconButton
198-
disabled={!record.isUserOwned}
199-
onClick={() => {
200-
setModalTitle("Add New Data Entry");
201-
setSelectedDataSource(record);
202-
setAddDataSourceModalOpen(true);
203-
}}
204-
>
205-
<AddOutlinedIcon />
206-
</IconButton>
207-
<IconButton
208-
disabled={!record.isUserOwned}
209-
onClick={() => {
210-
setDeleteId(record);
211-
setDeleteModalTitle("Delete Data Source");
212-
setDeleteModalMessage(
213-
<div>
214-
Are you sure you want to delete{" "}
215-
<span style={{ fontWeight: "bold" }}>{record.name}</span> ?
216-
</div>,
217-
);
218-
setDeleteConfirmationModalOpen(true);
219-
}}
220-
>
221-
<DeleteOutlineOutlinedIcon />
222-
</IconButton>
223-
{profileFlags.IS_ORGANIZATION_MEMBER && record.isUserOwned && (
197+
render: (record) => {
198+
return (
199+
<Box>
200+
{!record?.type?.is_external_datasource && (
201+
<IconButton
202+
disabled={!record.isUserOwned}
203+
onClick={() => {
204+
setModalTitle("Add New Data Entry");
205+
setSelectedDataSource(record);
206+
setAddDataSourceModalOpen(true);
207+
}}
208+
>
209+
<AddOutlinedIcon />
210+
</IconButton>
211+
)}
212+
{record?.type?.is_external_datasource && (
213+
<Tooltip title="External Connection">
214+
<span>
215+
<IconButton disabled={true}>
216+
<SettingsEthernetIcon />
217+
</IconButton>
218+
</span>
219+
</Tooltip>
220+
)}
224221
<IconButton
222+
disabled={!record.isUserOwned}
225223
onClick={() => {
226-
setModalTitle("Share Datasource");
227-
setSelectedDataSource(record);
228-
setShareDataSourceModalOpen(true);
224+
setDeleteId(record);
225+
setDeleteModalTitle("Delete Data Source");
226+
setDeleteModalMessage(
227+
<div>
228+
Are you sure you want to delete{" "}
229+
<span style={{ fontWeight: "bold" }}>{record.name}</span> ?
230+
</div>,
231+
);
232+
setDeleteConfirmationModalOpen(true);
229233
}}
230234
>
231-
{record.visibility === 0 ? (
232-
<PersonOutlineOutlinedIcon />
233-
) : (
234-
<PeopleOutlineOutlinedIcon />
235-
)}
235+
<DeleteOutlineOutlinedIcon />
236236
</IconButton>
237-
)}
238-
</Box>
239-
),
237+
{profileFlags.IS_ORGANIZATION_MEMBER && record.isUserOwned && (
238+
<IconButton
239+
onClick={() => {
240+
setModalTitle("Share Datasource");
241+
setSelectedDataSource(record);
242+
setShareDataSourceModalOpen(true);
243+
}}
244+
>
245+
{record.visibility === 0 ? (
246+
<PersonOutlineOutlinedIcon />
247+
) : (
248+
<PeopleOutlineOutlinedIcon />
249+
)}
250+
</IconButton>
251+
)}
252+
</Box>
253+
);
254+
},
240255
},
241256
];
242257

common/blocks/data/store/vectorstore/weaviate.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ class WeaviateConfiguration(BaseModel):
8080
weaviate_rw_api_key: Optional[str] = None
8181
embeddings_rate_limit: Optional[int] = 3000
8282
default_batch_size: Optional[int] = 20
83+
username: Optional[str] = None
84+
password: Optional[str] = None
85+
api_key: Optional[str] = None
86+
additional_headers: Optional[dict] = {}
8387

8488

8589
class Weaviate(VectorStoreInterface):
@@ -130,8 +134,8 @@ def check_batch_result(results: Optional[List[Dict[str, Any]]]) -> None:
130134
json.dumps(result['result']['errors']),
131135
),
132136
)
133-
134-
headers = {}
137+
138+
headers = configuration.additional_headers
135139
if configuration.openai_key is not None:
136140
headers['X-OpenAI-Api-Key'] = configuration.openai_key
137141
if configuration.cohere_api_key is not None:
@@ -144,10 +148,25 @@ def check_batch_result(results: Optional[List[Dict[str, Any]]]) -> None:
144148
headers['authorization'] = 'Bearer ' + \
145149
configuration.weaviate_rw_api_key
146150

147-
self._client = weaviate.Client(
148-
url=configuration.url,
149-
additional_headers=headers,
150-
)
151+
if configuration.username is not None and configuration.password is not None:
152+
self._client = weaviate.Client(
153+
url=configuration.url,
154+
auth_client_secret=weaviate.AuthClientPassword(
155+
username=configuration.username, password=configuration.password),
156+
additional_headers=headers,
157+
)
158+
elif configuration.api_key is not None:
159+
self._client = weaviate.Client(
160+
url=configuration.url,
161+
auth_client_secret=weaviate.AuthApiKey(
162+
api_key=configuration.api_key),
163+
additional_headers=headers,
164+
)
165+
else:
166+
self._client = weaviate.Client(
167+
url=configuration.url,
168+
additional_headers=headers,
169+
)
151170

152171
self.client.batch.configure(
153172
batch_size=DEFAULT_BATCH_SIZE,
@@ -234,6 +253,7 @@ def similarity_search(self, index_name: str, document_query: DocumentQuery, **kw
234253
properties = [document_query.page_content_key]
235254
for key in document_query.metadata.get('additional_properties', []):
236255
properties.append(key)
256+
additional_metadata_properties = document_query.metadata.get('metadata_properties', ['id', 'certainty', 'distance'])
237257

238258
if kwargs.get('search_distance'):
239259
nearText['certainty'] = kwargs.get('search_distance')
@@ -254,7 +274,7 @@ def similarity_search(self, index_name: str, document_query: DocumentQuery, **kw
254274
query_obj = query_obj.with_where(whereFilter)
255275
query_response = query_obj.with_near_text(nearText).with_limit(
256276
document_query.limit,
257-
).with_additional(['id', 'certainty', 'distance']).do()
277+
).with_additional(additional_metadata_properties).do()
258278
except Exception as e:
259279
logger.error('Error in similarity search: %s' % e)
260280
raise e

common/utils/models.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from typing import Optional
2+
3+
import orjson as json
4+
from pydantic import BaseModel
5+
6+
class Config(BaseModel):
7+
"""
8+
Base class for config type models stored in the database. Supports optional encryption.
9+
"""
10+
config_type: str
11+
is_encrypted: bool = False
12+
data: str = ''
13+
14+
def to_dict(self, encrypt_fn):
15+
return {
16+
'config_type': self.config_type,
17+
'is_encrypted': self.is_encrypted,
18+
'data': self.get_data(encrypt_fn),
19+
}
20+
21+
def from_dict(self, dict_data, decrypt_fn):
22+
self.config_type = dict_data.get('config_type')
23+
self.is_encrypted = dict_data.get('is_encrypted')
24+
self.set_data(dict_data.get('data'), decrypt_fn)
25+
26+
# Use the data from the dict to populate the fields
27+
self.__dict__.update(json.loads(self.data))
28+
29+
return self.dict(exclude={'is_encrypted', 'config_type', 'data'})
30+
31+
def get_data(self, encrypt_fn):
32+
data = self.json(exclude={'is_encrypted', 'config_type', 'data'})
33+
return encrypt_fn(data).decode('utf-8') if self.is_encrypted else data
34+
35+
def set_data(self, data, decrypt_fn):
36+
self.data = data
37+
if self.is_encrypted:
38+
self.data = decrypt_fn(data)

datasources/apis.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,28 @@ def post(self, request):
117117
datasource_type = get_object_or_404(
118118
DataSourceType, id=request.data['type'],
119119
)
120-
datasource = DataSource.objects.create(
120+
121+
datasource = DataSource(
121122
name=request.data['name'],
122123
owner=owner,
123124
type=datasource_type,
124125
)
126+
# If this is an external data source, then we need to save the config in datasource object
127+
if datasource_type.is_external_datasource:
128+
datasource_type_cls = DataSourceTypeFactory.get_datasource_type_handler(datasource.type)
129+
if not datasource_type_cls:
130+
logger.error(
131+
'No handler found for data source type {datasource.type}',
132+
)
133+
return DRFResponse({'errors': ['No handler found for data source type']}, status=400)
134+
135+
datasource_handler: DataSourceProcessor = datasource_type_cls(datasource)
136+
if not datasource_handler:
137+
logger.error(f'Error while creating handler for data source {datasource.name}')
138+
return DRFResponse({'errors': ['Error while creating handler for data source type']}, status=400)
139+
config = datasource_type_cls.process_validate_config(request.data['config'], datasource)
140+
datasource.config = config
141+
125142
datasource.save()
126143
return DRFResponse(DataSourceSerializer(instance=datasource).data, status=201)
127144

@@ -147,6 +164,9 @@ def add_entry(self, request, uid):
147164
datasource = get_object_or_404(
148165
DataSource, uuid=uuid.UUID(uid), owner=request.user,
149166
)
167+
if datasource and datasource.type.is_external_datasource:
168+
return DRFResponse({'errors': ['Cannot add entry to external data source']}, status=400)
169+
150170
entry_data = request.data['entry_data']
151171
entry_metadata = dict(map(lambda x: (f'md_{x}', request.data['entry_metadata'][x]), request.data['entry_metadata'].keys())) if 'entry_metadata' in request.data else {
152172
}

datasources/handlers/databases/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)