diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 937d2eb11..172189084 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -23,6 +23,13 @@ services: - ${DOCKER_VOLUME_DIRECTORY:-.}/mysql/conf/my.cnf:/etc/mysql/my.cnf - ${DOCKER_VOLUME_DIRECTORY:-.}/mysql/data:/var/lib/mysql + office: + image: onlyoffice/documentserver:7.1.1 + ports: + - "8701:80" + volumes: + - ${DOCKER_VOLUME_DIRECTORY:-.}/office/bisheng:/var/www/onlyoffice/documentserver/sdkjs-plugins/bisheng + backend: image: dataelement/bisheng-backend:latest healthcheck: @@ -42,6 +49,7 @@ services: restart: on-failure depends_on: - "mysql" + - "office" - "redis" nginx: diff --git a/docker/office/bisheng/all.js b/docker/office/bisheng/all.js new file mode 100644 index 000000000..93560514a --- /dev/null +++ b/docker/office/bisheng/all.js @@ -0,0 +1,487 @@ +(function (window, undefined) { + let selectText = '' + + window.Asc.plugin.init = function (e) { + selectText = e + } + window.Asc.plugin.event_onClick = function () { + selectText = '' + } + + window.Asc.plugin.button = function (id) { + } + + const EventMap = { + sendToParent (method, data) { + let params = { + type: 'onExternalFrameMessage', + method, + data + } + window.top.postMessage(JSON.stringify(params), location.origin) + }, + focusInDocument (data) { + window.Asc.scope.field = { + id: data.id, + fieldFlag: data.fieldFlag, + $index: data.$index || 1 + } + window.Asc.plugin.callCommand(function () { + let field = Asc.scope.field || {} + let index = field.$index ? field.$index : 1 + let oDoc = Api.GetDocument() + let flag = `{{${field.fieldFlag}}}` + let oRange = oDoc.Search(flag) + let cur = 1 + for (let i = 0; i < oRange.length; i++) { + if (oRange[i].GetText() === flag) { + if (cur === index) { + oRange[i].Select() + break + } + cur = cur + 1 + } + } + }) + }, + focusTableInDoc (data) { + window.Asc.scope.marker = data.marker + window.Asc.plugin.callCommand(function () { + let flag = Asc.scope.marker || '' + let oDoc = Api.GetDocument() + let oRange = oDoc.GetBookmarkRange(flag) + oRange.Select() + }) + }, + addMarker (data) { + let flag = '{{' + data.fieldFlag + '}}' + window.Asc.plugin.executeMethod('PasteText', [flag]) + }, + addBookMarker (data) { + window.Asc.scope.value = data + window.Asc.plugin.callCommand(function () { + let oDoc = Api.GetDocument() + let range = oDoc.GetRangeBySelect() + let params = { + type: 'onExternalFrameMessage', + method: 'addBookMarker' + } + let marker = Asc.scope.value + let markers = [] + if (range) { + let texts = range.GetText() + let pars = range.GetAllParagraphs() || [] + let txtList = [] + for (let i = 0; i < pars.length; i++) { + let text = pars[i].GetText() + txtList.push(text) + } + let table = pars[0] ? pars[0].GetParentTable() : null + let count = table ? table.GetRowsCount() : 0 + for (let i = 0; i < count; i++) { + let row = table.GetRow(i) + let firstCell = row.GetCell(0) + let cellText = firstCell ? firstCell.GetContent().GetElement(0).GetText() : '' + // 序号 + let isNumbering = false + if (firstCell.GetContent().GetElement(0).GetNumbering()) { + isNumbering = true + let cellCount = row.GetCellsCount() + for (let j = 1; j < cellCount; j++) { + let cellItem = row.GetCell(j) + if (!cellItem.GetContent().GetElement(0).GetNumbering()) { + cellText = cellItem.GetContent().GetElement(0).GetText() + firstCell = cellItem + break + } + } + } + if (cellText && txtList.includes(cellText)) { + let cRange = firstCell.Search(cellText)[0] + cRange.AddBookmark(marker.key + i) + markers.push(marker.key + i) + } + } + // range.AddBookmark(Asc.scope.value.key) + params.data = Object.assign(marker, { + key: markers.join(','), + texts + }) + } else { + params.data = false + } + window.top.postMessage(JSON.stringify(params), location.origin) + }) + }, + deleteBookMarker (data) { + window.Asc.scope.value = data + window.Asc.plugin.callCommand(function () { + let oDoc = Api.GetDocument() + let markers = Asc.scope.value || [] + for (let i = 0; i < markers.length; i++) { + oDoc.DeleteBookmark(markers[i]) + } + }) + }, + // 批量删除循环应用内标签 + deleteLoopApp (list) { + window.Asc.scope.value = list + window.Asc.plugin.callCommand(function () { + let list = window.Asc.scope.value || [] + let oDoc = Api.GetDocument() + list.forEach(row => { + if (row.loopType === 0) { + oDoc.SearchAndReplace({ searchString: `{{${row.startTag}}}`, replaceString: '' }, `{{${row.startTag}}}`, '') + oDoc.SearchAndReplace({ searchString: `{{${row.endTag}}}`, replaceString: '' }, `{{${row.endTag}}}`, '') + } else if (row.loopType === 1) { + oDoc.DeleteBookmark(row.bookmark) + } + }) + }) + }, + // 更新占位符 + replaceMarker (data) { + window.Asc.scope.st = '{{' + data.newValue + '}}' + // 原来的值 + if (data.oldValue) { + window.Asc.scope.old = '{{' + data.oldValue + '}}' + } else { + this.addMarker(data) + return + } + window.Asc.plugin.callCommand(function () { + let oDocument = Api.GetDocument() + oDocument.SearchAndReplace({ searchString: Asc.scope.old, replaceString: Asc.scope.st }, Asc.scope.old, Asc.scope.st) + }, false) + }, + // 查找并插入占位符 + findAndInsertMarker (data) { + window.Asc.scope.st = '{{' + data.fieldName + '}}' + window.Asc.scope.searchStr = data.fieldValue + window.Asc.plugin.callCommand(function () { + let oDocument = Api.GetDocument() + oDocument.SearchAndReplace({ searchString: Asc.scope.searchStr, replaceString: Asc.scope.st }, Asc.scope.searchStr, Asc.scope.st) + }, false) + }, + insertPosition (data) { + if (!selectText) { + let postData = { + text: selectText, + ...data, + selected: false + } + this.sendToParent('addRange', postData) + return false + } + window.Asc.scope.postData = data + window.Asc.plugin.callCommand(function() { + let postData = Asc.scope.postData || {} + let oDoc = Api.GetDocument() + let oRange = oDoc.GetRangeBySelect() + let selectText = oRange.GetText() + let oAllPar = oRange.GetAllParagraphs() + let oPar = oAllPar[oAllPar.length - 1] + let parText = oPar.GetText() + if (oAllPar.length > 1) { + oRange.AddText(`{{${postData.start}}}`, 'before') + if (selectText.includes(parText)) { + let newRange = oPar.GetRange(0, parText.length - 1) + newRange.AddText(`{{${postData.end}}}`, 'after') + } else { + oRange.AddText(`{{${postData.end}}}`, 'after') + } + } else { + let isEnd = parText.substr(0 - selectText.length) === selectText + isEnd = isEnd || selectText.includes(parText) + console.log('end = ', isEnd) + let start = Math.max(parText.indexOf(selectText), 0) + let end = start + Math.min(parText.length, selectText.length) - 1 + let newRange = oPar.GetRange(start, end) + oRange.AddText(`{{${postData.start}}}`, 'before') + newRange.AddText(`{{${postData.end}}}`, 'after') + } + + postData.selected = true + postData.text = selectText + let params = { + type: 'onExternalFrameMessage', + method: 'addRange', + data: postData + } + window.top.postMessage(JSON.stringify(params), location.origin) + }) + }, + deletePosition (data) { + window.Asc.scope.range = data + window.Asc.plugin.callCommand(function () { + let oDocument = Api.GetDocument() + let { start, end } = Asc.scope.range + let markers = [`{{${start}}}`, `{{${end}}}`] + for (let j = 0; j < markers.length; j++) { + oDocument.SearchAndReplace({ searchString: markers[j], replaceString: '' }, markers[j], '') + } + }) + }, + deletePositionMarker (data) { + window.Asc.scope.data = data + window.Asc.plugin.callCommand(function () { + let oDocument = Api.GetDocument() + let markers = Asc.scope.data || [] + for (let j = 0; j < markers.length; j++) { + oDocument.SearchAndReplace({ searchString: markers[j], replaceString: '' }, markers[j], '') + } + }) + }, + deletePositionArray (data) { + window.Asc.scope.data = data + window.Asc.plugin.callCommand(function () { + let oDocument = Api.GetDocument() + let markers = Asc.scope.data || [] + for (let j = 0; j < markers.length; j++) { + oDocument.SearchAndReplace({ searchString: markers[j], replaceString: '' }, markers[j], '') + } + }) + }, + replaceRangePosition (data) { + window.Asc.scope.list = data + window.Asc.plugin.callCommand(function () { + let list = Asc.scope.list || [] + let oDocument = Api.GetDocument() + list.forEach(row => { + oDocument.SearchAndReplace({ searchString: row.str, replaceString: row.newStr }, row.str, row.newStr) + }) + }) + }, + delMarker (data) { + let fields = [] + data.forEach(item => { + fields.push({ + text: '{{' + item.fieldFlag + '}}', + type: 'field' + }) + }) + window.Asc.scope.st = fields + window.Asc.plugin.callCommand(function () { + let oDocument = Api.GetDocument() + let markers = Asc.scope.st.slice(0) + for (let j = 0; j < markers.length; j++) { + let marker = markers[j] + if (marker.type === 'field') { + oDocument.SearchAndReplace({ searchString: marker.text, replaceString: '' }) + } + } + }) + }, + delMarkerGroup (data) { + this.delMarker(data.fields) + }, + // excel + insertCellName (field) { + window.Asc.scope.field = field + window.Asc.plugin.callCommand(function () { + let fieldItem = Asc.scope.field + let sheetObj = Api.GetActiveSheet() + let sheetName = sheetObj.GetName() + let oRange = Api.GetSelection() + let oCount = oRange.GetCount() + let params = { + type: 'onExternalFrameMessage', + method: 'addCellName' + } + if (oCount !== 1) { + params.data = false + } else { + let oAddr = oRange.GetAddress(true, true, '', false) + let sheetFlag = `${sheetName}!${oAddr}` + // let name = [fieldItem.fieldName, 'DEF', fieldItem.id].join('') + // let nameObj = sheetObj.GetDefName(name) + // console.log('inser cell before = ', name, sheetFlag, nameObj) + // let result = sheetObj.AddDefName(name, sheetFlag) + // console.log('insert cell ', name, sheetFlag, result) + fieldItem.fieldFlag = sheetFlag + fieldItem.$success = oAddr !== '' + params.data = fieldItem + } + window.top.postMessage(JSON.stringify(params), location.origin) + }) + }, + getFocusedCell () { + window.Asc.plugin.callCommand(function () { + let sheetObj = Api.GetActiveSheet() + let sheetName = sheetObj.GetName() + let oRange = Api.GetSelection() + let params = { + type: 'onExternalFrameMessage', + method: 'getFocusedCell' + } + let oAddr = oRange.GetAddress(true, true, '', false) + let sheetFlag = `${sheetName}!${oAddr}` + params.data = sheetFlag + window.top.postMessage(JSON.stringify(params), location.origin) + }) + }, + loadFileFlags (data) { + window.Asc.scope.list = data + window.Asc.plugin.callCommand(function () { + let list = Asc.scope.list || [] + let oDocument = Api.GetDocument() + let oParCount = oDocument.GetElementsCount() + let dataMap = {} + for (let i = 0; i < oParCount; i++) { + let oPar = oDocument.GetElement(i) + let ctype = oPar.GetClassType() + if (ctype === 'table') { + list.forEach(row => { + let flag = `{{${row.fieldFlag}}}` + let rs = oPar.Search(flag) + for (let i = 0; i < rs.length; i++) { + let oRange = rs[i] + if (oRange && oRange.GetText() === flag) { + if (dataMap[row.id]) { + dataMap[row.id] = dataMap[row.id] + 1 + } else { + dataMap[row.id] = 1 + } + } + } + }) + } else if (ctype === 'paragraph') { + let oParText = oPar.GetText() + list.forEach(row => { + let count = oParText.split(`{{${row.fieldFlag}}}`).length - 1 + if (dataMap[row.id]) { + dataMap[row.id] = dataMap[row.id] + count + } else { + dataMap[row.id] = count + } + }) + } + } + let params = { + type: 'onExternalFrameMessage', + method: 'loadFieldFlagCount', + data: dataMap + } + window.top.postMessage(JSON.stringify(params), location.origin) + }) + }, + /** + * data.sheetName: 要聚焦的sheet名称 + * data.cellName: 要聚焦的cell名称,如C1, D3等 + */ + focusCell (data) { + window.Asc.scope.data = data + window.Asc.plugin.callCommand(function () { + const theData = Asc.scope.data + const theSheet = Api.GetSheet(theData.sheetName || '') + if (theSheet) { + theSheet.SetActive() + + const theCell = theSheet.GetRange(theData.cellName || '') + if (theCell) { + theCell.Select() + } + } + }) + }, + + getSelectedText (data) { + window.Asc.scope.data = data + window.Asc.plugin.callCommand(function () { + const theData = Asc.scope.data + const oDoc = Api.GetDocument() + const oRange = oDoc.GetRangeBySelect() + if (oRange) { + const oParas = oRange.GetAllParagraphs() + // 只能选择一个段落,否则认为不成功 + if (oParas.length === 1) { + + const params = { + type: 'onExternalFrameMessage', + method: 'getSelectedText', + data: { + id: theData.id, + text: oRange.GetText() + } + } + window.top.postMessage(JSON.stringify(params), location.origin) + } + } + }) + } + } + + function receiveMessage (e) { + let data = e.data ? JSON.parse(e.data) : {} + if (data.type === 'onExternalPluginMessage') { + switch (data.method) { + case 'focus': + EventMap.focusInDocument(data.data) + break + case 'focusTable': + EventMap.focusTableInDoc(data.data) + break + case 'insert': + EventMap.addMarker(data.data) + break + case 'addBookMarker': + EventMap.addBookMarker(data.data) + break + case 'delBookMarker': + EventMap.deleteBookMarker(data.data) + break + case 'delLoopApp': + EventMap.deleteLoopApp(data.data) + break + case 'update': + EventMap.replaceMarker(data.data) + break + case 'findAndInsertMarker': + EventMap.findAndInsertMarker(data.data) + break + case 'addRange': + EventMap.insertPosition(data.data) + break + case 'updateRange': + EventMap.replaceRangePosition(data.data) + break + case 'delRange': + EventMap.deletePosition(data.data) + break + case 'delRangeArray': + EventMap.deletePositionArray(data.data) + break + case 'delQuoteGroup': + EventMap.deletePositionMarker(data.data) + break + case 'remove': + EventMap.delMarker([ data.data ]) + break + case 'removeQuestion': + EventMap.delMarkerGroup(data.data) + break + // excel + case 'addCellName': + EventMap.insertCellName(data.data) + break + case 'loadFieldFlagCount': + EventMap.loadFileFlags(data.data) + break + // 聚焦到某个单元格 + case 'focusCell': + EventMap.focusCell(data.data) + break + // 获取当前选中的单元格 + case 'getFocusedCell': + EventMap.getFocusedCell() + break + // 获取当前选中的文字 + case 'getSelectedText': + EventMap.getSelectedText(data.data) + break + } + } + } + + window.addEventListener('message', receiveMessage, false) +})(window, undefined) diff --git a/docker/office/bisheng/bisheng.js b/docker/office/bisheng/bisheng.js new file mode 100644 index 000000000..03507467e --- /dev/null +++ b/docker/office/bisheng/bisheng.js @@ -0,0 +1,15 @@ +(function () { + window.Asc.plugin.init = function (e) {} + window.Asc.plugin.event_onClick = function () {} + window.Asc.plugin.button = function (id) {} + + function onMessage(e) { + var data = e.data ? JSON.parse(e.data) : {} + if (data.action === 'insetMarker') { + const flag = '{{' + data.data + '}}' + window.Asc.plugin.executeMethod('PasteText', [flag]) + } + } + + window.addEventListener('message', onMessage, false) +})() \ No newline at end of file diff --git a/docker/office/bisheng/config.json b/docker/office/bisheng/config.json new file mode 100644 index 000000000..b6f13abbd --- /dev/null +++ b/docker/office/bisheng/config.json @@ -0,0 +1,35 @@ +{ + "name": "文档自动化", + "guid": "asc.{D2A0F3BE-CC8D-4956-BCD9-6CBEA6E8960E}", + "variations": [ + { + "description": "插入label-配置", + "url": "index.html", + "icons": [ + "icon.png", + "icon.png", + "icon.png", + "icon.png" + ], + "EditorsSupport": [ + "word", + "cell", + "slide" + ], + "isViewer": false, + "isVisual": false, + "isModal": true, + "isInsideMode": false, + "isSystem": false, + "initOnSelectionChanged": true, + "hideClose": true, + "initDataType": "text", + "isDisplayedInViewer": true, + "isUpdateOleOnResize": true, + "events": [ + "onClick", + "onTargetPositionChanged" + ] + } + ] +} \ No newline at end of file diff --git a/docker/office/bisheng/icon.png b/docker/office/bisheng/icon.png new file mode 100644 index 000000000..df532d700 Binary files /dev/null and b/docker/office/bisheng/icon.png differ diff --git a/docker/office/bisheng/index.html b/docker/office/bisheng/index.html new file mode 100644 index 000000000..45c5fecb5 --- /dev/null +++ b/docker/office/bisheng/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/bisheng/api/v1/knowledge.py b/src/backend/bisheng/api/v1/knowledge.py index e4bd18b16..706e2f2f2 100644 --- a/src/backend/bisheng/api/v1/knowledge.py +++ b/src/backend/bisheng/api/v1/knowledge.py @@ -131,8 +131,9 @@ async def process_knowledge(*, session.add(db_file) session.commit() session.refresh(db_file) - files.append(db_file) - file_paths.append(filepath) + if not repeat: + files.append(db_file) + file_paths.append(filepath) logger.info(f'fileName={file_name} col={collection_name}') result.append(db_file.copy()) @@ -310,6 +311,7 @@ def delete_knowledge_file(*, # minio minio_client.MinioClient().delete_minio(str(knowledge_file.id)) + minio_client.MinioClient().delete_minio(str(knowledge_file.object_name)) # elastic esvectore_client = decide_vectorstores(collection_name, 'ElasticKeywordsSearch', embeddings) if esvectore_client: @@ -365,11 +367,12 @@ async def addEmbedding(collection_name, model: str, chunk_size: int, separator: # 存储 mysql db_file = session.get(KnowledgeFile, knowledge_file.id) setattr(db_file, 'status', 2) - setattr(db_file, 'object_name', knowledge_file.file_name) - session.add(db_file) - session.flush() # 原文件 object_name_original = f'original/{db_file.id}' + setattr(db_file, 'object_name', object_name_original) + session.add(db_file) + session.flush() + minio_client.MinioClient().upload_minio(object_name_original, path) texts, metadatas = _read_chunk_text(path, knowledge_file.file_name, chunk_size, diff --git a/src/backend/bisheng/cache/utils.py b/src/backend/bisheng/cache/utils.py index bf5e1fa18..4103d5764 100644 --- a/src/backend/bisheng/cache/utils.py +++ b/src/backend/bisheng/cache/utils.py @@ -13,7 +13,7 @@ import requests from appdirs import user_cache_dir from bisheng.settings import settings -from bisheng.utils.minio_client import mino_client, tmp_bucket +from bisheng.utils.minio_client import MinioClient, tmp_bucket CACHE: Dict[str, Any] = {} @@ -177,10 +177,11 @@ def save_uploaded_file(file, folder_name, file_name): # Save the file with the hash as its name if settings.get_knowledge().get('minio'): + minio_client = MinioClient() # 存储oss file_byte = file.read() - mino_client.upload_tmp(file_name, file_byte) - file_path = mino_client.get_share_link(file_name, tmp_bucket) + minio_client.upload_tmp(file_name, file_byte) + file_path = minio_client.get_share_link(file_name, tmp_bucket) else: file_type = md5_name.split('.')[-1] file_path = folder_path / f'{md5_name}.{file_type}' diff --git a/src/backend/bisheng/database/models/message.py b/src/backend/bisheng/database/models/message.py index 7d611f347..9f291e6f2 100644 --- a/src/backend/bisheng/database/models/message.py +++ b/src/backend/bisheng/database/models/message.py @@ -4,7 +4,7 @@ from bisheng.database.models.base import SQLModelSerializable from pydantic import BaseModel -from sqlalchemy import JSON, Column, DateTime, Text, text +from sqlalchemy import JSON, Column, DateTime, String, Text, text from sqlmodel import Field @@ -22,7 +22,7 @@ class MessageBase(SQLModelSerializable): intermediate_steps: Optional[str] = Field(index=False, sa_column=Column(Text), description='过程日志') - files: Optional[str] = Field(index=False, description='上传的文件等') + files: Optional[str] = Field(sa_column=Column(String(length=4096)), description='上传的文件等') # file_access: Optional[bool] = Field(index=False, default=True, description='召回文件是否可以访问') create_time: Optional[datetime] = Field( sa_column=Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP'))) diff --git a/src/backend/bisheng/interface/chains/custom.py b/src/backend/bisheng/interface/chains/custom.py index 2d4b653da..e594a25a6 100644 --- a/src/backend/bisheng/interface/chains/custom.py +++ b/src/backend/bisheng/interface/chains/custom.py @@ -106,9 +106,11 @@ def initialize(cls, llm: BaseLanguageModel, chain_type: str, prompt: BasePromptTemplate = None, + document_prompt: BasePromptTemplate = None, token_max: str = -1): if chain_type == 'stuff': - return load_qa_chain(llm=llm, chain_type=chain_type, prompt=prompt, token_max=token_max) + return load_qa_chain(llm=llm, chain_type=chain_type, prompt=prompt, + token_max=token_max, document_prompt=document_prompt) else: return load_qa_chain(llm=llm, chain_type=chain_type) diff --git a/src/backend/bisheng/main.py b/src/backend/bisheng/main.py index a51d98d25..92b0d3078 100644 --- a/src/backend/bisheng/main.py +++ b/src/backend/bisheng/main.py @@ -60,7 +60,6 @@ def authjwt_exception_handler(request: Request, exc: AuthJWTException): app.include_router(router_rpc) app.on_event('startup')(create_db_and_tables) app.on_event('startup')(setup_llm_caching) - return app diff --git a/src/backend/bisheng/template/frontend_node/chains.py b/src/backend/bisheng/template/frontend_node/chains.py index 3b56e8217..a0b10115f 100644 --- a/src/backend/bisheng/template/frontend_node/chains.py +++ b/src/backend/bisheng/template/frontend_node/chains.py @@ -301,7 +301,14 @@ class CombineDocsChainNode(FrontendNode): name='prompt', display_name='prompt', advanced=False, - info='只对Stuff类型生效') + info='只对Stuff类型生效'), + TemplateField( + field_type='BasePromptTemplate', + required=False, + show=True, + name='document_prompt', + advanced=False, + ) ], ) diff --git a/src/backend/bisheng/utils/minio_client.py b/src/backend/bisheng/utils/minio_client.py index ab193992c..f5a26ad47 100644 --- a/src/backend/bisheng/utils/minio_client.py +++ b/src/backend/bisheng/utils/minio_client.py @@ -95,6 +95,3 @@ def mkdir(self, bucket: str): if self.minio_client: if not self.minio_client.bucket_exists(bucket): self.minio_client.make_bucket(bucket) - - -mino_client = MinioClient() diff --git a/src/bisheng-langchain/bisheng_langchain/chat_models/proxy_llm.py b/src/bisheng-langchain/bisheng_langchain/chat_models/proxy_llm.py index 9a80897af..a973b8aa1 100644 --- a/src/bisheng-langchain/bisheng_langchain/chat_models/proxy_llm.py +++ b/src/bisheng-langchain/bisheng_langchain/chat_models/proxy_llm.py @@ -195,6 +195,8 @@ def _completion_with_retry(**kwargs: Any) -> Any: 'functions': kwargs.get('functions', []) } response = self.client.post(self.elemai_base_url, json=params) + if response.status_code != 200: + raise return response.json() return _completion_with_retry(**kwargs) diff --git a/src/bisheng-langchain/bisheng_langchain/vectorstores/milvus.py b/src/bisheng-langchain/bisheng_langchain/vectorstores/milvus.py new file mode 100644 index 000000000..e69de29bb