ragflow-sdk-myfbee-test 0.27.2.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ragflow_sdk/__init__.py +39 -0
- ragflow_sdk/modules/__init__.py +15 -0
- ragflow_sdk/modules/agent.py +69 -0
- ragflow_sdk/modules/base.py +62 -0
- ragflow_sdk/modules/chat.py +67 -0
- ragflow_sdk/modules/chunk.py +62 -0
- ragflow_sdk/modules/dataset.py +182 -0
- ragflow_sdk/modules/document.py +109 -0
- ragflow_sdk/modules/memory.py +87 -0
- ragflow_sdk/modules/session.py +203 -0
- ragflow_sdk/ragflow.py +398 -0
- ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/METADATA +10 -0
- ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/RECORD +15 -0
- ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/WHEEL +5 -0
- ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/top_level.txt +1 -0
ragflow_sdk/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
# Keep runtime validation when beartype is installed, while allowing slim
|
|
18
|
+
# SDK installs to import without the optional checker.
|
|
19
|
+
try:
|
|
20
|
+
from beartype.claw import beartype_this_package
|
|
21
|
+
|
|
22
|
+
beartype_this_package()
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
import importlib.metadata
|
|
27
|
+
|
|
28
|
+
from .ragflow import RAGFlow
|
|
29
|
+
from .modules.dataset import DataSet
|
|
30
|
+
from .modules.chat import Chat
|
|
31
|
+
from .modules.session import Session
|
|
32
|
+
from .modules.document import Document
|
|
33
|
+
from .modules.chunk import Chunk
|
|
34
|
+
from .modules.agent import Agent
|
|
35
|
+
from .modules.memory import Memory
|
|
36
|
+
|
|
37
|
+
__version__ = importlib.metadata.version("ragflow_sdk")
|
|
38
|
+
|
|
39
|
+
__all__ = ["RAGFlow", "DataSet", "Chat", "Session", "Document", "Chunk", "Agent", "Memory"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
from .base import Base
|
|
18
|
+
from .session import Session
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Agent(Base):
|
|
22
|
+
def __init__(self, rag, res_dict):
|
|
23
|
+
self.id = None
|
|
24
|
+
self.avatar = None
|
|
25
|
+
self.canvas_type = None
|
|
26
|
+
self.description = None
|
|
27
|
+
self.dsl = None
|
|
28
|
+
super().__init__(rag, res_dict)
|
|
29
|
+
|
|
30
|
+
class Dsl(Base):
|
|
31
|
+
def __init__(self, rag, res_dict):
|
|
32
|
+
self.answer = []
|
|
33
|
+
self.components = {"begin": {"downstream": ["Answer:China"], "obj": {"component_name": "Begin", "params": {}}, "upstream": []}}
|
|
34
|
+
self.graph = {
|
|
35
|
+
"edges": [],
|
|
36
|
+
"nodes": [{"data": {"label": "Begin", "name": "begin"}, "id": "begin", "position": {"x": 50, "y": 200}, "sourcePosition": "left", "targetPosition": "right", "type": "beginNode"}],
|
|
37
|
+
}
|
|
38
|
+
self.history = []
|
|
39
|
+
self.messages = []
|
|
40
|
+
self.path = []
|
|
41
|
+
self.reference = []
|
|
42
|
+
super().__init__(rag, res_dict)
|
|
43
|
+
|
|
44
|
+
def create_session(self, **kwargs) -> Session:
|
|
45
|
+
res = self.post(f"/agents/{self.id}/sessions", json=kwargs)
|
|
46
|
+
res = res.json()
|
|
47
|
+
if res.get("code") == 0:
|
|
48
|
+
return Session(self.rag, res.get("data"))
|
|
49
|
+
raise Exception(res.get("message"))
|
|
50
|
+
|
|
51
|
+
def list_sessions(self, page: int = 1, page_size: int = 30, orderby: str = "create_time", desc: bool = True, id: str = None) -> list[Session]:
|
|
52
|
+
res = self.get(f"/agents/{self.id}/sessions", {"page": page, "page_size": page_size, "orderby": orderby, "desc": desc, "id": id})
|
|
53
|
+
res = res.json()
|
|
54
|
+
if res.get("code") == 0:
|
|
55
|
+
result_list = []
|
|
56
|
+
for data in res.get("data"):
|
|
57
|
+
temp_agent = Session(self.rag, data)
|
|
58
|
+
result_list.append(temp_agent)
|
|
59
|
+
return result_list
|
|
60
|
+
raise Exception(res.get("message"))
|
|
61
|
+
|
|
62
|
+
def delete_sessions(self, ids: list[str] | None = None, delete_all: bool = False):
|
|
63
|
+
payload = {"ids": ids}
|
|
64
|
+
if delete_all:
|
|
65
|
+
payload["delete_all"] = True
|
|
66
|
+
res = self.rm(f"/agents/{self.id}/sessions", payload)
|
|
67
|
+
res = res.json()
|
|
68
|
+
if res.get("code") != 0:
|
|
69
|
+
raise Exception(res.get("message"))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Base:
|
|
19
|
+
def __init__(self, rag, res_dict):
|
|
20
|
+
self.rag = rag
|
|
21
|
+
self._update_from_dict(rag, res_dict)
|
|
22
|
+
|
|
23
|
+
def _update_from_dict(self, rag, res_dict):
|
|
24
|
+
for k, v in res_dict.items():
|
|
25
|
+
if isinstance(v, dict):
|
|
26
|
+
self.__dict__[k] = Base(rag, v)
|
|
27
|
+
else:
|
|
28
|
+
self.__dict__[k] = v
|
|
29
|
+
|
|
30
|
+
def to_json(self):
|
|
31
|
+
pr = {}
|
|
32
|
+
for name in dir(self):
|
|
33
|
+
value = getattr(self, name)
|
|
34
|
+
if not name.startswith("__") and not callable(value) and name != "rag":
|
|
35
|
+
if isinstance(value, Base):
|
|
36
|
+
pr[name] = value.to_json()
|
|
37
|
+
else:
|
|
38
|
+
pr[name] = value
|
|
39
|
+
return pr
|
|
40
|
+
|
|
41
|
+
def post(self, path, json=None, stream=False, files=None):
|
|
42
|
+
res = self.rag.post(path, json, stream=stream, files=files)
|
|
43
|
+
return res
|
|
44
|
+
|
|
45
|
+
def get(self, path, params=None):
|
|
46
|
+
res = self.rag.get(path, params)
|
|
47
|
+
return res
|
|
48
|
+
|
|
49
|
+
def rm(self, path, json):
|
|
50
|
+
res = self.rag.delete(path, json)
|
|
51
|
+
return res
|
|
52
|
+
|
|
53
|
+
def put(self, path, json):
|
|
54
|
+
res = self.rag.put(path, json)
|
|
55
|
+
return res
|
|
56
|
+
|
|
57
|
+
def patch(self, path, json):
|
|
58
|
+
res = self.rag.patch(path, json)
|
|
59
|
+
return res
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return str(self.to_json())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .base import Base
|
|
19
|
+
from .session import Session
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Chat(Base):
|
|
23
|
+
def __init__(self, rag, res_dict):
|
|
24
|
+
self.id = ""
|
|
25
|
+
self.name = "assistant"
|
|
26
|
+
self.icon = ""
|
|
27
|
+
self.dataset_ids = []
|
|
28
|
+
self.llm_id = None
|
|
29
|
+
self.llm_setting = {}
|
|
30
|
+
self.prompt_config = {}
|
|
31
|
+
self.similarity_threshold = 0.2
|
|
32
|
+
self.vector_similarity_weight = 0.3
|
|
33
|
+
self.top_n = 6
|
|
34
|
+
self.top_k = 1024
|
|
35
|
+
self.rerank_id = ""
|
|
36
|
+
super().__init__(rag, res_dict)
|
|
37
|
+
|
|
38
|
+
def update(self, update_message: dict):
|
|
39
|
+
if not isinstance(update_message, dict):
|
|
40
|
+
raise Exception("ValueError('`update_message` must be a dict')")
|
|
41
|
+
res = self.patch(f"/chats/{self.id}", update_message)
|
|
42
|
+
res = res.json()
|
|
43
|
+
if res.get("code") != 0:
|
|
44
|
+
raise Exception(res["message"])
|
|
45
|
+
|
|
46
|
+
def create_session(self, name: str = "New session") -> Session:
|
|
47
|
+
res = self.post(f"/chats/{self.id}/sessions", {"name": name})
|
|
48
|
+
res = res.json()
|
|
49
|
+
if res.get("code") == 0:
|
|
50
|
+
return Session(self.rag, res["data"])
|
|
51
|
+
raise Exception(res.get("message"))
|
|
52
|
+
|
|
53
|
+
def list_sessions(self, page: int = 1, page_size: int = 30, orderby: str = "create_time", desc: bool = True, id: str = None, name: str = None, user_id: str = None) -> list[Session]:
|
|
54
|
+
res = self.get(f"/chats/{self.id}/sessions", {"page": page, "page_size": page_size, "orderby": orderby, "desc": desc, "id": id, "name": name, "user_id": user_id})
|
|
55
|
+
res = res.json()
|
|
56
|
+
if res.get("code") == 0:
|
|
57
|
+
result_list = []
|
|
58
|
+
for data in res["data"]:
|
|
59
|
+
result_list.append(Session(self.rag, data))
|
|
60
|
+
return result_list
|
|
61
|
+
raise Exception(res["message"])
|
|
62
|
+
|
|
63
|
+
def delete_sessions(self, ids: list[str] | None = None, delete_all: bool = False):
|
|
64
|
+
res = self.rm(f"/chats/{self.id}/sessions", {"ids": ids, "delete_all": delete_all})
|
|
65
|
+
res = res.json()
|
|
66
|
+
if res.get("code") != 0:
|
|
67
|
+
raise Exception(res.get("message"))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
from .base import Base
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChunkUpdateError(Exception):
|
|
21
|
+
def __init__(self, code=None, message=None, details=None):
|
|
22
|
+
self.code = code
|
|
23
|
+
self.message = message
|
|
24
|
+
self.details = details
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Chunk(Base):
|
|
29
|
+
def __init__(self, rag, res_dict):
|
|
30
|
+
self.id = ""
|
|
31
|
+
self.content = ""
|
|
32
|
+
self.image_id = ""
|
|
33
|
+
self.important_keywords = []
|
|
34
|
+
self.tag_kwd = []
|
|
35
|
+
self.questions = []
|
|
36
|
+
self.create_time = ""
|
|
37
|
+
self.create_timestamp = 0.0
|
|
38
|
+
self.dataset_id = None
|
|
39
|
+
self.document_name = ""
|
|
40
|
+
self.document_keyword = ""
|
|
41
|
+
self.document_id = ""
|
|
42
|
+
self.available = True
|
|
43
|
+
# Additional fields for retrieval results
|
|
44
|
+
self.similarity = 0.0
|
|
45
|
+
self.vector_similarity = 0.0
|
|
46
|
+
self.term_similarity = 0.0
|
|
47
|
+
self.positions = []
|
|
48
|
+
self.doc_type = ""
|
|
49
|
+
for k in list(res_dict.keys()):
|
|
50
|
+
if k not in self.__dict__:
|
|
51
|
+
res_dict.pop(k)
|
|
52
|
+
super().__init__(rag, res_dict)
|
|
53
|
+
|
|
54
|
+
# for backward compatibility
|
|
55
|
+
if not self.document_name:
|
|
56
|
+
self.document_name = self.document_keyword
|
|
57
|
+
|
|
58
|
+
def update(self, update_message: dict):
|
|
59
|
+
res = self.patch(f"/datasets/{self.dataset_id}/documents/{self.document_id}/chunks/{self.id}", update_message)
|
|
60
|
+
res = res.json()
|
|
61
|
+
if res.get("code") != 0:
|
|
62
|
+
raise ChunkUpdateError(code=res.get("code"), message=res.get("message"), details=res.get("details"))
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .base import Base
|
|
19
|
+
from .document import Document
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataSet(Base):
|
|
23
|
+
class ParserConfig(Base):
|
|
24
|
+
def __init__(self, rag, res_dict):
|
|
25
|
+
super().__init__(rag, res_dict)
|
|
26
|
+
|
|
27
|
+
def __init__(self, rag, res_dict):
|
|
28
|
+
self.id = ""
|
|
29
|
+
self.name = ""
|
|
30
|
+
self.avatar = ""
|
|
31
|
+
self.tenant_id = None
|
|
32
|
+
self.description = ""
|
|
33
|
+
self.embedding_model = ""
|
|
34
|
+
self.permission = "me"
|
|
35
|
+
self.document_count = 0
|
|
36
|
+
self.chunk_count = 0
|
|
37
|
+
self.chunk_method = "naive"
|
|
38
|
+
self.parser_config = None
|
|
39
|
+
self.pagerank = 0
|
|
40
|
+
for k in list(res_dict.keys()):
|
|
41
|
+
if k not in self.__dict__:
|
|
42
|
+
res_dict.pop(k)
|
|
43
|
+
super().__init__(rag, res_dict)
|
|
44
|
+
|
|
45
|
+
def update(self, update_message: dict):
|
|
46
|
+
res = self.put(f"/datasets/{self.id}", update_message)
|
|
47
|
+
res = res.json()
|
|
48
|
+
if res.get("code") != 0:
|
|
49
|
+
raise Exception(res["message"])
|
|
50
|
+
|
|
51
|
+
self._update_from_dict(self.rag, res.get("data", {}))
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def upload_documents(self, document_list: list[dict]):
|
|
55
|
+
url = f"/datasets/{self.id}/documents"
|
|
56
|
+
files = [("file", (ele["display_name"], ele["blob"])) for ele in document_list]
|
|
57
|
+
res = self.post(path=url, json=None, files=files)
|
|
58
|
+
res = res.json()
|
|
59
|
+
if res.get("code") == 0:
|
|
60
|
+
doc_list = []
|
|
61
|
+
for doc in res["data"]:
|
|
62
|
+
document = Document(self.rag, doc)
|
|
63
|
+
doc_list.append(document)
|
|
64
|
+
return doc_list
|
|
65
|
+
raise Exception(res.get("message"))
|
|
66
|
+
|
|
67
|
+
def list_documents(
|
|
68
|
+
self,
|
|
69
|
+
id: str | None = None,
|
|
70
|
+
ids: list[str] | None = None,
|
|
71
|
+
name: str | None = None,
|
|
72
|
+
keywords: str | None = None,
|
|
73
|
+
page: int = 1,
|
|
74
|
+
page_size: int = 30,
|
|
75
|
+
orderby: str = "create_time",
|
|
76
|
+
desc: bool = True,
|
|
77
|
+
create_time_from: int = 0,
|
|
78
|
+
create_time_to: int = 0,
|
|
79
|
+
):
|
|
80
|
+
# Validate that id and ids are not used together
|
|
81
|
+
if id and ids:
|
|
82
|
+
raise ValueError("Cannot use both 'id' and 'ids' parameters at the same time.")
|
|
83
|
+
|
|
84
|
+
params = {
|
|
85
|
+
"id": id,
|
|
86
|
+
"name": name,
|
|
87
|
+
"keywords": keywords,
|
|
88
|
+
"page": page,
|
|
89
|
+
"page_size": page_size,
|
|
90
|
+
"orderby": orderby,
|
|
91
|
+
"desc": desc,
|
|
92
|
+
"create_time_from": create_time_from,
|
|
93
|
+
"create_time_to": create_time_to,
|
|
94
|
+
}
|
|
95
|
+
# Handle ids parameter - requests expands list values into multiple query params
|
|
96
|
+
if ids:
|
|
97
|
+
params["ids"] = ids
|
|
98
|
+
res = self.get(f"/datasets/{self.id}/documents", params=params)
|
|
99
|
+
res = res.json()
|
|
100
|
+
documents = []
|
|
101
|
+
if res.get("code") == 0:
|
|
102
|
+
for document in res["data"].get("docs"):
|
|
103
|
+
documents.append(Document(self.rag, document))
|
|
104
|
+
return documents
|
|
105
|
+
raise Exception(res["message"])
|
|
106
|
+
|
|
107
|
+
def delete_documents(self, ids: list[str] | None = None, delete_all: bool = False):
|
|
108
|
+
res = self.rm(f"/datasets/{self.id}/documents", {"ids": ids, "delete_all": delete_all})
|
|
109
|
+
res = res.json()
|
|
110
|
+
if res.get("code") != 0:
|
|
111
|
+
raise Exception(res["message"])
|
|
112
|
+
|
|
113
|
+
def _get_documents_status(self, document_ids):
|
|
114
|
+
import time
|
|
115
|
+
|
|
116
|
+
terminal_states = {"DONE", "FAIL", "CANCEL"}
|
|
117
|
+
interval_sec = 1
|
|
118
|
+
pending = set(document_ids)
|
|
119
|
+
finished = []
|
|
120
|
+
while pending:
|
|
121
|
+
for doc_id in list(pending):
|
|
122
|
+
|
|
123
|
+
def fetch_doc(doc_id: str) -> Document | None:
|
|
124
|
+
try:
|
|
125
|
+
docs = self.list_documents(id=doc_id)
|
|
126
|
+
return docs[0] if docs else None
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
doc = fetch_doc(doc_id)
|
|
131
|
+
if doc is None:
|
|
132
|
+
continue
|
|
133
|
+
if isinstance(doc.run, str) and doc.run.upper() in terminal_states:
|
|
134
|
+
finished.append((doc_id, doc.run, doc.chunk_count, doc.token_count))
|
|
135
|
+
pending.discard(doc_id)
|
|
136
|
+
elif float(doc.progress or 0.0) >= 1.0:
|
|
137
|
+
finished.append((doc_id, "DONE", doc.chunk_count, doc.token_count))
|
|
138
|
+
pending.discard(doc_id)
|
|
139
|
+
if pending:
|
|
140
|
+
time.sleep(interval_sec)
|
|
141
|
+
return finished
|
|
142
|
+
|
|
143
|
+
def async_parse_documents(self, document_ids):
|
|
144
|
+
res = self.post(f"/datasets/{self.id}/chunks", {"document_ids": document_ids})
|
|
145
|
+
res = res.json()
|
|
146
|
+
if res.get("code") != 0:
|
|
147
|
+
raise Exception(res.get("message"))
|
|
148
|
+
|
|
149
|
+
def parse_documents(self, document_ids):
|
|
150
|
+
try:
|
|
151
|
+
self.async_parse_documents(document_ids)
|
|
152
|
+
self._get_documents_status(document_ids)
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
self.async_cancel_parse_documents(document_ids)
|
|
155
|
+
|
|
156
|
+
return self._get_documents_status(document_ids)
|
|
157
|
+
|
|
158
|
+
def async_cancel_parse_documents(self, document_ids):
|
|
159
|
+
res = self.rm(f"/datasets/{self.id}/chunks", {"document_ids": document_ids})
|
|
160
|
+
res = res.json()
|
|
161
|
+
if res.get("code") != 0:
|
|
162
|
+
raise Exception(res.get("message"))
|
|
163
|
+
|
|
164
|
+
def get_auto_metadata(self) -> dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Retrieve auto-metadata configuration for a dataset via SDK.
|
|
167
|
+
"""
|
|
168
|
+
res = self.get(f"/datasets/{self.id}/metadata/config")
|
|
169
|
+
res = res.json()
|
|
170
|
+
if res.get("code") == 0:
|
|
171
|
+
return res["data"]
|
|
172
|
+
raise Exception(res["message"])
|
|
173
|
+
|
|
174
|
+
def update_auto_metadata(self, **config: Any) -> dict[str, Any]:
|
|
175
|
+
"""
|
|
176
|
+
Update auto-metadata configuration for a dataset via SDK.
|
|
177
|
+
"""
|
|
178
|
+
res = self.put(f"/datasets/{self.id}/metadata/config", config)
|
|
179
|
+
res = res.json()
|
|
180
|
+
if res.get("code") == 0:
|
|
181
|
+
return res["data"]
|
|
182
|
+
raise Exception(res["message"])
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from .base import Base
|
|
20
|
+
from .chunk import Chunk
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Document(Base):
|
|
26
|
+
class ParserConfig(Base):
|
|
27
|
+
def __init__(self, rag, res_dict):
|
|
28
|
+
super().__init__(rag, res_dict)
|
|
29
|
+
|
|
30
|
+
def __init__(self, rag, res_dict):
|
|
31
|
+
self.id = ""
|
|
32
|
+
self.name = ""
|
|
33
|
+
self.thumbnail = None
|
|
34
|
+
self.dataset_id = None
|
|
35
|
+
self.chunk_method = "naive"
|
|
36
|
+
self.parser_config = {"pages": [[1, 1000000]]}
|
|
37
|
+
self.source_type = "local"
|
|
38
|
+
self.type = ""
|
|
39
|
+
self.created_by = ""
|
|
40
|
+
self.size = 0
|
|
41
|
+
self.token_count = 0
|
|
42
|
+
self.chunk_count = 0
|
|
43
|
+
self.progress = 0.0
|
|
44
|
+
self.progress_msg = ""
|
|
45
|
+
self.process_begin_at = None
|
|
46
|
+
self.process_duration = 0.0
|
|
47
|
+
self.run = "0"
|
|
48
|
+
self.status = "1"
|
|
49
|
+
self.meta_fields = {}
|
|
50
|
+
for k in list(res_dict.keys()):
|
|
51
|
+
if k not in self.__dict__:
|
|
52
|
+
res_dict.pop(k)
|
|
53
|
+
super().__init__(rag, res_dict)
|
|
54
|
+
|
|
55
|
+
def update(self, update_message: dict):
|
|
56
|
+
if "meta_fields" in update_message:
|
|
57
|
+
if not isinstance(update_message["meta_fields"], dict):
|
|
58
|
+
raise Exception("meta_fields must be a dictionary")
|
|
59
|
+
res = self.patch(f"/datasets/{self.dataset_id}/documents/{self.id}", update_message)
|
|
60
|
+
res = res.json()
|
|
61
|
+
if res.get("code") != 0:
|
|
62
|
+
raise Exception(res["message"])
|
|
63
|
+
|
|
64
|
+
self._update_from_dict(self.rag, res.get("data", {}))
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def download(self):
|
|
68
|
+
res = self.get(f"/datasets/{self.dataset_id}/documents/{self.id}")
|
|
69
|
+
content_disposition = res.headers.get("Content-Disposition", "")
|
|
70
|
+
if content_disposition.lstrip().lower().startswith("attachment"):
|
|
71
|
+
logger.debug("Document.download returning attachment content dataset_id=%s document_id=%s", self.dataset_id, self.id)
|
|
72
|
+
return res.content
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
response = res.json()
|
|
76
|
+
if isinstance(response, dict) and "code" in response and response["code"] != 0:
|
|
77
|
+
raise Exception(response.get("message"))
|
|
78
|
+
except ValueError:
|
|
79
|
+
pass
|
|
80
|
+
logger.debug("Document.download returning non-attachment content dataset_id=%s document_id=%s", self.dataset_id, self.id)
|
|
81
|
+
return res.content
|
|
82
|
+
|
|
83
|
+
def list_chunks(self, page=1, page_size=30, keywords="", id=""):
|
|
84
|
+
data = {"keywords": keywords, "page": page, "page_size": page_size, "id": id}
|
|
85
|
+
res = self.get(f"/datasets/{self.dataset_id}/documents/{self.id}/chunks", data)
|
|
86
|
+
res = res.json()
|
|
87
|
+
if res.get("code") == 0:
|
|
88
|
+
chunks = []
|
|
89
|
+
for data in res["data"].get("chunks"):
|
|
90
|
+
chunk = Chunk(self.rag, data)
|
|
91
|
+
chunks.append(chunk)
|
|
92
|
+
return chunks
|
|
93
|
+
raise Exception(res.get("message"))
|
|
94
|
+
|
|
95
|
+
def add_chunk(self, content: str, important_keywords: list[str] = [], questions: list[str] = [], image_base64: str | None = None, *, tag_kwd: list[str] = []):
|
|
96
|
+
body = {"content": content, "important_keywords": important_keywords, "tag_kwd": tag_kwd, "questions": questions}
|
|
97
|
+
if image_base64 is not None:
|
|
98
|
+
body["image_base64"] = image_base64
|
|
99
|
+
res = self.post(f"/datasets/{self.dataset_id}/documents/{self.id}/chunks", body)
|
|
100
|
+
res = res.json()
|
|
101
|
+
if res.get("code") == 0:
|
|
102
|
+
return Chunk(self.rag, res["data"].get("chunk"))
|
|
103
|
+
raise Exception(res.get("message"))
|
|
104
|
+
|
|
105
|
+
def delete_chunks(self, ids: list[str] | None = None, delete_all: bool = False):
|
|
106
|
+
res = self.rm(f"/datasets/{self.dataset_id}/documents/{self.id}/chunks", {"chunk_ids": ids, "delete_all": delete_all})
|
|
107
|
+
res = res.json()
|
|
108
|
+
if res.get("code") != 0:
|
|
109
|
+
raise Exception(res.get("message"))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
from .base import Base
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Memory(Base):
|
|
21
|
+
def __init__(self, rag, res_dict):
|
|
22
|
+
self.id = ""
|
|
23
|
+
self.name = ""
|
|
24
|
+
self.avatar = None
|
|
25
|
+
self.tenant_id = None
|
|
26
|
+
self.owner_name = ""
|
|
27
|
+
self.memory_type = ["raw"]
|
|
28
|
+
self.storage_type = "table"
|
|
29
|
+
self.embd_id = ""
|
|
30
|
+
self.llm_id = ""
|
|
31
|
+
self.permissions = "me"
|
|
32
|
+
self.description = ""
|
|
33
|
+
self.memory_size = 5 * 1024 * 1024
|
|
34
|
+
self.forgetting_policy = "FIFO"
|
|
35
|
+
self.temperature = 0.5
|
|
36
|
+
self.system_prompt = ""
|
|
37
|
+
self.user_prompt = ""
|
|
38
|
+
for k in list(res_dict.keys()):
|
|
39
|
+
if k not in self.__dict__:
|
|
40
|
+
res_dict.pop(k)
|
|
41
|
+
super().__init__(rag, res_dict)
|
|
42
|
+
|
|
43
|
+
def update(self, update_dict: dict):
|
|
44
|
+
res = self.put(f"/memories/{self.id}", update_dict)
|
|
45
|
+
res = res.json()
|
|
46
|
+
if res.get("code") != 0:
|
|
47
|
+
raise Exception(res["message"])
|
|
48
|
+
self._update_from_dict(self.rag, res.get("data", {}))
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def get_config(self):
|
|
52
|
+
res = self.get(f"/memories/{self.id}/config")
|
|
53
|
+
res = res.json()
|
|
54
|
+
if res.get("code") != 0:
|
|
55
|
+
raise Exception(res["message"])
|
|
56
|
+
self._update_from_dict(self.rag, res.get("data", {}))
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def list_memory_messages(self, agent_id: str | list[str] = None, keywords: str = None, page: int = 1, page_size: int = 50):
|
|
60
|
+
params = {"agent_id": agent_id, "keywords": keywords, "page": page, "page_size": page_size}
|
|
61
|
+
res = self.get(f"/memories/{self.id}", params)
|
|
62
|
+
res = res.json()
|
|
63
|
+
if res.get("code") != 0:
|
|
64
|
+
raise Exception(res["message"])
|
|
65
|
+
return res["data"]
|
|
66
|
+
|
|
67
|
+
def forget_message(self, message_id: int):
|
|
68
|
+
res = self.rm(f"/messages/{self.id}:{message_id}", {})
|
|
69
|
+
res = res.json()
|
|
70
|
+
if res.get("code") != 0:
|
|
71
|
+
raise Exception(res["message"])
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
def update_message_status(self, message_id: int, status: bool):
|
|
75
|
+
update_message = {"status": status}
|
|
76
|
+
res = self.put(f"/messages/{self.id}:{message_id}", update_message)
|
|
77
|
+
res = res.json()
|
|
78
|
+
if res.get("code") != 0:
|
|
79
|
+
raise Exception(res["message"])
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def get_message_content(self, message_id: int) -> dict:
|
|
83
|
+
res = self.get(f"/messages/{self.id}:{message_id}/content")
|
|
84
|
+
res = res.json()
|
|
85
|
+
if res.get("code") != 0:
|
|
86
|
+
raise Exception(res["message"])
|
|
87
|
+
return res["data"]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from .base import Base
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Session(Base):
|
|
26
|
+
def __init__(self, rag, res_dict):
|
|
27
|
+
self.id = None
|
|
28
|
+
self.name = "New session"
|
|
29
|
+
self.messages = [{"role": "assistant", "content": "Hi! I am your assistant, can I help you?"}]
|
|
30
|
+
for key, value in res_dict.items():
|
|
31
|
+
if key == "chat_id" and value is not None:
|
|
32
|
+
self.chat_id = None
|
|
33
|
+
self.__session_type = "chat"
|
|
34
|
+
if key == "agent_id" and value is not None:
|
|
35
|
+
self.agent_id = None
|
|
36
|
+
self.__session_type = "agent"
|
|
37
|
+
super().__init__(rag, res_dict)
|
|
38
|
+
|
|
39
|
+
def ask(
|
|
40
|
+
self,
|
|
41
|
+
question="",
|
|
42
|
+
stream=False,
|
|
43
|
+
inputs=None,
|
|
44
|
+
release=None,
|
|
45
|
+
return_trace=None,
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Ask a question to the session.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
question : str
|
|
54
|
+
The user's question. May be empty when the agent is driven solely by
|
|
55
|
+
Begin component inputs.
|
|
56
|
+
stream : bool
|
|
57
|
+
If ``True``, yields ``Message`` objects as they arrive (SSE streaming).
|
|
58
|
+
If ``False``, yields a single ``Message`` with the final answer.
|
|
59
|
+
inputs : dict, optional
|
|
60
|
+
Values for variables declared on the agent's **Begin** component. Each
|
|
61
|
+
value must be a dict containing at least a ``"value"`` key, and may
|
|
62
|
+
include ``"type"``. Example::
|
|
63
|
+
|
|
64
|
+
session.ask(
|
|
65
|
+
"",
|
|
66
|
+
stream=False,
|
|
67
|
+
inputs={"key1": {"type": "line", "value": "hello"}},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Only meaningful for agent sessions; ignored for chat sessions.
|
|
71
|
+
release : bool, optional
|
|
72
|
+
If ``True``, run against the latest published agent version instead of
|
|
73
|
+
the editable draft. Only meaningful for agent sessions.
|
|
74
|
+
return_trace : bool, optional
|
|
75
|
+
If ``True``, include execution trace information in the response.
|
|
76
|
+
Only meaningful for agent sessions.
|
|
77
|
+
**kwargs
|
|
78
|
+
Additional fields forwarded verbatim to the completion endpoint
|
|
79
|
+
(e.g. ``session_id``, ``files``, ``user_id``, ``custom_header``).
|
|
80
|
+
See the HTTP API reference for the full list.
|
|
81
|
+
"""
|
|
82
|
+
if inputs is not None:
|
|
83
|
+
kwargs["inputs"] = inputs
|
|
84
|
+
if release is not None:
|
|
85
|
+
kwargs["release"] = release
|
|
86
|
+
if return_trace is not None:
|
|
87
|
+
kwargs["return_trace"] = return_trace
|
|
88
|
+
|
|
89
|
+
if inputs is not None or release is not None or return_trace is not None:
|
|
90
|
+
logger.debug(
|
|
91
|
+
"Session.ask explicit-params session_type=%s session_id=%s input_keys=%s release=%s return_trace=%s",
|
|
92
|
+
self.__session_type,
|
|
93
|
+
getattr(self, "id", None),
|
|
94
|
+
list(inputs.keys()) if isinstance(inputs, dict) else None,
|
|
95
|
+
release,
|
|
96
|
+
return_trace,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if self.__session_type == "agent":
|
|
100
|
+
res = self._ask_agent(question, stream, **kwargs)
|
|
101
|
+
elif self.__session_type == "chat":
|
|
102
|
+
res = self._ask_chat(question, stream, **kwargs)
|
|
103
|
+
else:
|
|
104
|
+
raise Exception(f"Unknown session type: {self.__session_type}")
|
|
105
|
+
|
|
106
|
+
if stream:
|
|
107
|
+
for line in res.iter_lines(decode_unicode=True):
|
|
108
|
+
if not line:
|
|
109
|
+
continue # Skip empty lines
|
|
110
|
+
line = line.strip()
|
|
111
|
+
if line.startswith("data:"):
|
|
112
|
+
content = line[len("data:") :].strip()
|
|
113
|
+
if content == "[DONE]":
|
|
114
|
+
break # End of stream
|
|
115
|
+
else:
|
|
116
|
+
content = line
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
json_data = json.loads(content)
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
continue # Skip lines that are not valid JSON
|
|
122
|
+
|
|
123
|
+
event = json_data.get("event", None)
|
|
124
|
+
if event:
|
|
125
|
+
if self.__session_type == "agent" and event not in {"message", "message_end"}:
|
|
126
|
+
continue
|
|
127
|
+
if self.__session_type == "chat" and event != "message":
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if self.__session_type == "chat" and json_data.get("data") is True:
|
|
131
|
+
return
|
|
132
|
+
if self.__session_type == "agent":
|
|
133
|
+
message = self._structure_answer(json_data)
|
|
134
|
+
if event == "message_end":
|
|
135
|
+
message.content = ""
|
|
136
|
+
if message.reference:
|
|
137
|
+
yield message
|
|
138
|
+
continue
|
|
139
|
+
yield message
|
|
140
|
+
else:
|
|
141
|
+
yield self._structure_answer(json_data["data"])
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
json_data = res.json()
|
|
145
|
+
except ValueError:
|
|
146
|
+
raise Exception(f"Invalid response {res}")
|
|
147
|
+
yield self._structure_answer(json_data["data"])
|
|
148
|
+
|
|
149
|
+
def _structure_answer(self, json_data):
|
|
150
|
+
answer = ""
|
|
151
|
+
event = None
|
|
152
|
+
if self.__session_type == "agent":
|
|
153
|
+
event = json_data.get("event")
|
|
154
|
+
json_data = json_data["data"]
|
|
155
|
+
answer = json_data.get("content", "")
|
|
156
|
+
elif self.__session_type == "chat":
|
|
157
|
+
answer = json_data["answer"]
|
|
158
|
+
reference = json_data.get("reference", {})
|
|
159
|
+
temp_dict = {"content": answer, "role": "assistant"}
|
|
160
|
+
if reference and "chunks" in reference:
|
|
161
|
+
chunks = reference["chunks"]
|
|
162
|
+
if isinstance(chunks, dict):
|
|
163
|
+
chunks = list(chunks.values())
|
|
164
|
+
temp_dict["reference"] = chunks
|
|
165
|
+
if self.__session_type == "agent":
|
|
166
|
+
reference_count = len(chunks) if isinstance(chunks, list) else 0
|
|
167
|
+
logger.debug("Session.ask parsed agent references session_id=%s event=%s reference_count=%s", self.id, event, reference_count)
|
|
168
|
+
message = Message(self.rag, temp_dict)
|
|
169
|
+
return message
|
|
170
|
+
|
|
171
|
+
def _ask_chat(self, question: str, stream: bool, **kwargs):
|
|
172
|
+
json_data = {"question": question, "stream": stream, "session_id": self.id}
|
|
173
|
+
json_data.update(kwargs)
|
|
174
|
+
res = self.post(f"/chats/{self.chat_id}/completions", json_data, stream=stream)
|
|
175
|
+
return res
|
|
176
|
+
|
|
177
|
+
def _ask_agent(self, question: str, stream: bool, **kwargs):
|
|
178
|
+
json_data = {
|
|
179
|
+
"agent_id": self.agent_id,
|
|
180
|
+
"query": question,
|
|
181
|
+
"stream": stream,
|
|
182
|
+
"session_id": self.id,
|
|
183
|
+
"openai-compatible": False,
|
|
184
|
+
}
|
|
185
|
+
json_data.update(kwargs)
|
|
186
|
+
res = self.post("/agents/chat/completions", json_data, stream=stream)
|
|
187
|
+
return res
|
|
188
|
+
|
|
189
|
+
def update(self, update_message):
|
|
190
|
+
res = self.patch(f"/chats/{self.chat_id}/sessions/{self.id}", update_message)
|
|
191
|
+
res = res.json()
|
|
192
|
+
if res.get("code") != 0:
|
|
193
|
+
raise Exception(res.get("message"))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Message(Base):
|
|
197
|
+
def __init__(self, rag, res_dict):
|
|
198
|
+
self.content = "Hi! I am your assistant, can I help you?"
|
|
199
|
+
self.reference = None
|
|
200
|
+
self.role = "assistant"
|
|
201
|
+
self.prompt = None
|
|
202
|
+
self.id = None
|
|
203
|
+
super().__init__(rag, res_dict)
|
ragflow_sdk/ragflow.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from typing import Optional, Any
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from .modules.agent import Agent
|
|
21
|
+
from .modules.chat import Chat
|
|
22
|
+
from .modules.chunk import Chunk
|
|
23
|
+
from .modules.dataset import DataSet
|
|
24
|
+
from .modules.memory import Memory
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RAGFlow:
|
|
28
|
+
def __init__(self, api_key, base_url, version="v1"):
|
|
29
|
+
"""
|
|
30
|
+
api_url: http://<host_address>/api/v1
|
|
31
|
+
"""
|
|
32
|
+
self.user_key = api_key
|
|
33
|
+
self.api_url = f"{base_url}/api/{version}"
|
|
34
|
+
self.authorization_header = {"Authorization": "{} {}".format("Bearer", self.user_key)}
|
|
35
|
+
|
|
36
|
+
def post(self, path, json=None, stream=False, files=None):
|
|
37
|
+
res = requests.post(url=self.api_url + path, json=json, headers=self.authorization_header, stream=stream, files=files)
|
|
38
|
+
return res
|
|
39
|
+
|
|
40
|
+
def get(self, path, params=None, json=None):
|
|
41
|
+
res = requests.get(url=self.api_url + path, params=params, headers=self.authorization_header, json=json)
|
|
42
|
+
return res
|
|
43
|
+
|
|
44
|
+
def delete(self, path, json):
|
|
45
|
+
res = requests.delete(url=self.api_url + path, json=json, headers=self.authorization_header)
|
|
46
|
+
return res
|
|
47
|
+
|
|
48
|
+
def put(self, path, json):
|
|
49
|
+
res = requests.put(url=self.api_url + path, json=json, headers=self.authorization_header)
|
|
50
|
+
return res
|
|
51
|
+
|
|
52
|
+
def patch(self, path, json):
|
|
53
|
+
res = requests.patch(url=self.api_url + path, json=json, headers=self.authorization_header)
|
|
54
|
+
return res
|
|
55
|
+
|
|
56
|
+
def create_dataset(
|
|
57
|
+
self,
|
|
58
|
+
name: str,
|
|
59
|
+
avatar: Optional[str] = None,
|
|
60
|
+
description: Optional[str] = None,
|
|
61
|
+
embedding_model: Optional[str] = None,
|
|
62
|
+
permission: str = "me",
|
|
63
|
+
chunk_method: str = "naive",
|
|
64
|
+
parser_config: Optional[DataSet.ParserConfig] = None,
|
|
65
|
+
auto_metadata_config: Optional[dict[str, Any]] = None,
|
|
66
|
+
) -> DataSet:
|
|
67
|
+
payload = {
|
|
68
|
+
"name": name,
|
|
69
|
+
"avatar": avatar,
|
|
70
|
+
"description": description,
|
|
71
|
+
"embedding_model": embedding_model,
|
|
72
|
+
"permission": permission,
|
|
73
|
+
"chunk_method": chunk_method,
|
|
74
|
+
}
|
|
75
|
+
if parser_config is not None:
|
|
76
|
+
payload["parser_config"] = parser_config.to_json()
|
|
77
|
+
if auto_metadata_config is not None:
|
|
78
|
+
payload["auto_metadata_config"] = auto_metadata_config
|
|
79
|
+
|
|
80
|
+
res = self.post("/datasets", payload)
|
|
81
|
+
res = res.json()
|
|
82
|
+
if res.get("code") == 0:
|
|
83
|
+
return DataSet(self, res["data"])
|
|
84
|
+
raise Exception(res["message"])
|
|
85
|
+
|
|
86
|
+
def delete_datasets(self, ids: list[str] | None = None, delete_all: bool = False):
|
|
87
|
+
res = self.delete("/datasets", {"ids": ids, "delete_all": delete_all})
|
|
88
|
+
res = res.json()
|
|
89
|
+
if res.get("code") != 0:
|
|
90
|
+
raise Exception(res["message"])
|
|
91
|
+
|
|
92
|
+
def get_dataset(self, name: str):
|
|
93
|
+
_list = self.list_datasets(name=name)
|
|
94
|
+
if len(_list) > 0:
|
|
95
|
+
return _list[0]
|
|
96
|
+
raise Exception("Dataset %s not found" % name)
|
|
97
|
+
|
|
98
|
+
def list_datasets(
|
|
99
|
+
self, page: int = 1, page_size: int = 30, orderby: str = "create_time", desc: bool = True, id: str | None = None, ids: list[str] | None = None, name: str | None = None
|
|
100
|
+
) -> list[DataSet]:
|
|
101
|
+
if id and ids:
|
|
102
|
+
raise ValueError("Cannot use both 'id' and 'ids' parameters at the same time.")
|
|
103
|
+
|
|
104
|
+
res = self.get(
|
|
105
|
+
"/datasets",
|
|
106
|
+
{
|
|
107
|
+
"page": page,
|
|
108
|
+
"page_size": page_size,
|
|
109
|
+
"orderby": orderby,
|
|
110
|
+
"desc": desc,
|
|
111
|
+
"id": id,
|
|
112
|
+
"ids": ids,
|
|
113
|
+
"name": name,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
res = res.json()
|
|
117
|
+
result_list = []
|
|
118
|
+
if res.get("code") == 0:
|
|
119
|
+
for data in res["data"]:
|
|
120
|
+
result_list.append(DataSet(self, data))
|
|
121
|
+
return result_list
|
|
122
|
+
raise Exception(res["message"])
|
|
123
|
+
|
|
124
|
+
def create_chat(
|
|
125
|
+
self,
|
|
126
|
+
name: str,
|
|
127
|
+
icon: str = "",
|
|
128
|
+
dataset_ids: list[str] | None = None,
|
|
129
|
+
llm_id: str | None = None,
|
|
130
|
+
llm_setting: dict | None = None,
|
|
131
|
+
prompt_config: dict | None = None,
|
|
132
|
+
**kwargs,
|
|
133
|
+
) -> Chat:
|
|
134
|
+
payload = {"name": name, "icon": icon, "dataset_ids": dataset_ids or []}
|
|
135
|
+
if llm_id is not None:
|
|
136
|
+
payload["llm_id"] = llm_id
|
|
137
|
+
if llm_setting is not None:
|
|
138
|
+
payload["llm_setting"] = llm_setting
|
|
139
|
+
if prompt_config is not None:
|
|
140
|
+
payload["prompt_config"] = prompt_config
|
|
141
|
+
payload.update(kwargs)
|
|
142
|
+
res = self.post("/chats", payload)
|
|
143
|
+
res = res.json()
|
|
144
|
+
if res.get("code") == 0:
|
|
145
|
+
return Chat(self, res["data"])
|
|
146
|
+
raise Exception(res["message"])
|
|
147
|
+
|
|
148
|
+
def delete_chats(self, ids: list[str] | None = None, delete_all: bool = False):
|
|
149
|
+
res = self.delete("/chats", {"ids": ids, "delete_all": delete_all})
|
|
150
|
+
res = res.json()
|
|
151
|
+
if res.get("code") != 0:
|
|
152
|
+
raise Exception(res["message"])
|
|
153
|
+
|
|
154
|
+
def get_chat(self, chat_id: str) -> Chat:
|
|
155
|
+
res = self.get(f"/chats/{chat_id}")
|
|
156
|
+
res = res.json()
|
|
157
|
+
if res.get("code") == 0:
|
|
158
|
+
return Chat(self, res["data"])
|
|
159
|
+
raise Exception(res["message"])
|
|
160
|
+
|
|
161
|
+
def list_chats(
|
|
162
|
+
self,
|
|
163
|
+
page: int = 1,
|
|
164
|
+
page_size: int = 30,
|
|
165
|
+
orderby: str = "create_time",
|
|
166
|
+
desc: bool = True,
|
|
167
|
+
id: str | None = None,
|
|
168
|
+
name: str | None = None,
|
|
169
|
+
keywords: str | None = None,
|
|
170
|
+
owner_ids: str | list[str] | None = None,
|
|
171
|
+
) -> list[Chat]:
|
|
172
|
+
res = self.get(
|
|
173
|
+
"/chats",
|
|
174
|
+
{
|
|
175
|
+
"page": page,
|
|
176
|
+
"page_size": page_size,
|
|
177
|
+
"orderby": orderby,
|
|
178
|
+
"desc": desc,
|
|
179
|
+
"id": id,
|
|
180
|
+
"name": name,
|
|
181
|
+
"keywords": keywords,
|
|
182
|
+
"owner_ids": owner_ids,
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
res = res.json()
|
|
186
|
+
result_list = []
|
|
187
|
+
if res.get("code") == 0:
|
|
188
|
+
for data in res["data"]["chats"]:
|
|
189
|
+
result_list.append(Chat(self, data))
|
|
190
|
+
return result_list
|
|
191
|
+
raise Exception(res["message"])
|
|
192
|
+
|
|
193
|
+
def retrieve(
|
|
194
|
+
self,
|
|
195
|
+
dataset_ids,
|
|
196
|
+
document_ids=None,
|
|
197
|
+
question="",
|
|
198
|
+
page=1,
|
|
199
|
+
page_size=30,
|
|
200
|
+
similarity_threshold=0.2,
|
|
201
|
+
vector_similarity_weight=0.3,
|
|
202
|
+
top_k=1024,
|
|
203
|
+
rerank_id: str | None = None,
|
|
204
|
+
keyword: bool = False,
|
|
205
|
+
cross_languages: list[str] | None = None,
|
|
206
|
+
metadata_condition: dict | None = None,
|
|
207
|
+
use_kg: bool = False,
|
|
208
|
+
toc_enhance: bool = False,
|
|
209
|
+
):
|
|
210
|
+
if document_ids is None:
|
|
211
|
+
document_ids = []
|
|
212
|
+
data_json = {
|
|
213
|
+
"page": page,
|
|
214
|
+
"page_size": page_size,
|
|
215
|
+
"similarity_threshold": similarity_threshold,
|
|
216
|
+
"vector_similarity_weight": vector_similarity_weight,
|
|
217
|
+
"top_k": top_k,
|
|
218
|
+
"rerank_id": rerank_id,
|
|
219
|
+
"keyword": keyword,
|
|
220
|
+
"question": question,
|
|
221
|
+
"dataset_ids": dataset_ids,
|
|
222
|
+
"document_ids": document_ids,
|
|
223
|
+
"cross_languages": cross_languages,
|
|
224
|
+
"metadata_condition": metadata_condition,
|
|
225
|
+
"use_kg": use_kg,
|
|
226
|
+
"toc_enhance": toc_enhance,
|
|
227
|
+
}
|
|
228
|
+
# Send a POST request to the backend service (using requests library as an example, actual implementation may vary)
|
|
229
|
+
res = self.post("/retrieval", json=data_json)
|
|
230
|
+
res = res.json()
|
|
231
|
+
if res.get("code") == 0:
|
|
232
|
+
chunks = []
|
|
233
|
+
for chunk_data in res["data"].get("chunks"):
|
|
234
|
+
chunk = Chunk(self, chunk_data)
|
|
235
|
+
chunks.append(chunk)
|
|
236
|
+
return chunks
|
|
237
|
+
raise Exception(res.get("message"))
|
|
238
|
+
|
|
239
|
+
def list_agents(self, page: int = 1, page_size: int = 30, orderby: str = "update_time", desc: bool = True) -> list[Agent]:
|
|
240
|
+
res = self.get(
|
|
241
|
+
"/agents",
|
|
242
|
+
{
|
|
243
|
+
"page": page,
|
|
244
|
+
"page_size": page_size,
|
|
245
|
+
"orderby": orderby,
|
|
246
|
+
"desc": desc,
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
res = res.json()
|
|
250
|
+
result_list = []
|
|
251
|
+
if res.get("code") == 0:
|
|
252
|
+
data = res.get("data") or {}
|
|
253
|
+
data_list = data.get("canvas", [])
|
|
254
|
+
for data in data_list:
|
|
255
|
+
result_list.append(Agent(self, data))
|
|
256
|
+
return result_list
|
|
257
|
+
raise Exception(res["message"])
|
|
258
|
+
|
|
259
|
+
def get_agent(self, agent_id: str) -> Agent:
|
|
260
|
+
res = self.get(f"/agents/{agent_id}")
|
|
261
|
+
res = res.json()
|
|
262
|
+
if res.get("code") == 0:
|
|
263
|
+
return Agent(self, res["data"])
|
|
264
|
+
raise Exception(res["message"])
|
|
265
|
+
|
|
266
|
+
def create_agent(
|
|
267
|
+
self,
|
|
268
|
+
title: str,
|
|
269
|
+
dsl: dict,
|
|
270
|
+
description: str | None = None,
|
|
271
|
+
canvas_type: str | None = None,
|
|
272
|
+
) -> None:
|
|
273
|
+
req = {"title": title, "dsl": dsl}
|
|
274
|
+
|
|
275
|
+
if description is not None:
|
|
276
|
+
req["description"] = description
|
|
277
|
+
|
|
278
|
+
if canvas_type is not None:
|
|
279
|
+
req["canvas_type"] = canvas_type
|
|
280
|
+
|
|
281
|
+
res = self.post("/agents", req)
|
|
282
|
+
res = res.json()
|
|
283
|
+
|
|
284
|
+
if res.get("code") != 0:
|
|
285
|
+
raise Exception(res["message"])
|
|
286
|
+
|
|
287
|
+
def update_agent(
|
|
288
|
+
self,
|
|
289
|
+
agent_id: str,
|
|
290
|
+
title: str | None = None,
|
|
291
|
+
description: str | None = None,
|
|
292
|
+
dsl: dict | None = None,
|
|
293
|
+
canvas_type: str | None = None,
|
|
294
|
+
) -> None:
|
|
295
|
+
req = {}
|
|
296
|
+
|
|
297
|
+
if title is not None:
|
|
298
|
+
req["title"] = title
|
|
299
|
+
|
|
300
|
+
if description is not None:
|
|
301
|
+
req["description"] = description
|
|
302
|
+
|
|
303
|
+
if dsl is not None:
|
|
304
|
+
req["dsl"] = dsl
|
|
305
|
+
|
|
306
|
+
if canvas_type is not None:
|
|
307
|
+
req["canvas_type"] = canvas_type
|
|
308
|
+
|
|
309
|
+
res = self.put(f"/agents/{agent_id}", req)
|
|
310
|
+
res = res.json()
|
|
311
|
+
|
|
312
|
+
if res.get("code") != 0:
|
|
313
|
+
raise Exception(res["message"])
|
|
314
|
+
|
|
315
|
+
def delete_agent(self, agent_id: str) -> None:
|
|
316
|
+
res = self.delete(f"/agents/{agent_id}", {})
|
|
317
|
+
res = res.json()
|
|
318
|
+
|
|
319
|
+
if res.get("code") != 0:
|
|
320
|
+
raise Exception(res["message"])
|
|
321
|
+
|
|
322
|
+
def create_memory(self, name: str, memory_type: list[str], embd_id: str, llm_id: str):
|
|
323
|
+
payload = {"name": name, "memory_type": memory_type, "embd_id": embd_id, "llm_id": llm_id}
|
|
324
|
+
res = self.post("/memories", payload)
|
|
325
|
+
res = res.json()
|
|
326
|
+
if res.get("code") != 0:
|
|
327
|
+
raise Exception(res["message"])
|
|
328
|
+
return Memory(self, res["data"])
|
|
329
|
+
|
|
330
|
+
def list_memory(self, page: int = 1, page_size: int = 50, tenant_id: str | list[str] = None, memory_type: str | list[str] = None, storage_type: str = None, keywords: str = None) -> dict:
|
|
331
|
+
res = self.get(
|
|
332
|
+
"/memories",
|
|
333
|
+
{
|
|
334
|
+
"page": page,
|
|
335
|
+
"page_size": page_size,
|
|
336
|
+
"tenant_id": tenant_id,
|
|
337
|
+
"memory_type": memory_type,
|
|
338
|
+
"storage_type": storage_type,
|
|
339
|
+
"keywords": keywords,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
res = res.json()
|
|
343
|
+
if res.get("code") != 0:
|
|
344
|
+
raise Exception(res["message"])
|
|
345
|
+
result_list = []
|
|
346
|
+
for data in res["data"]["memory_list"]:
|
|
347
|
+
result_list.append(Memory(self, data))
|
|
348
|
+
return {"code": res.get("code", 0), "message": res.get("message"), "memory_list": result_list, "total_count": res["data"]["total_count"]}
|
|
349
|
+
|
|
350
|
+
def delete_memory(self, memory_id: str):
|
|
351
|
+
res = self.delete(f"/memories/{memory_id}", {})
|
|
352
|
+
res = res.json()
|
|
353
|
+
if res.get("code") != 0:
|
|
354
|
+
raise Exception(res["message"])
|
|
355
|
+
|
|
356
|
+
def add_message(self, memory_id: list[str], agent_id: str, session_id: str, user_input: str, agent_response: str, user_id: str = "") -> str:
|
|
357
|
+
"""Append messages to memories; ``user_id`` is forwarded only for API-key auth (external subject)."""
|
|
358
|
+
payload = {"memory_id": memory_id, "agent_id": agent_id, "session_id": session_id, "user_input": user_input, "agent_response": agent_response, "user_id": user_id}
|
|
359
|
+
res = self.post("/messages", payload)
|
|
360
|
+
res = res.json()
|
|
361
|
+
if res.get("code") != 0:
|
|
362
|
+
raise Exception(res["message"])
|
|
363
|
+
return res["message"]
|
|
364
|
+
|
|
365
|
+
def search_message(
|
|
366
|
+
self,
|
|
367
|
+
query: str,
|
|
368
|
+
memory_id: list[str],
|
|
369
|
+
agent_id: str = None,
|
|
370
|
+
session_id: str = None,
|
|
371
|
+
user_id: str = None,
|
|
372
|
+
similarity_threshold: float = 0.2,
|
|
373
|
+
keywords_similarity_weight: float = 0.7,
|
|
374
|
+
top_n: int = 10,
|
|
375
|
+
) -> list[dict]:
|
|
376
|
+
params = {
|
|
377
|
+
"query": query,
|
|
378
|
+
"memory_id": memory_id,
|
|
379
|
+
"agent_id": agent_id,
|
|
380
|
+
"session_id": session_id,
|
|
381
|
+
"user_id": user_id,
|
|
382
|
+
"similarity_threshold": similarity_threshold,
|
|
383
|
+
"keywords_similarity_weight": keywords_similarity_weight,
|
|
384
|
+
"top_n": top_n,
|
|
385
|
+
}
|
|
386
|
+
res = self.get("/messages/search", params)
|
|
387
|
+
res = res.json()
|
|
388
|
+
if res.get("code") != 0:
|
|
389
|
+
raise Exception(res["message"])
|
|
390
|
+
return res["data"]
|
|
391
|
+
|
|
392
|
+
def get_recent_messages(self, memory_id: list[str], agent_id: str = None, session_id: str = None, limit: int = 10) -> list[dict]:
|
|
393
|
+
params = {"memory_id": memory_id, "agent_id": agent_id, "session_id": session_id, "limit": limit}
|
|
394
|
+
res = self.get("/messages", params)
|
|
395
|
+
res = res.json()
|
|
396
|
+
if res.get("code") != 0:
|
|
397
|
+
raise Exception(res["message"])
|
|
398
|
+
return res["data"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ragflow-sdk-myfbee-test
|
|
3
|
+
Version: 0.27.2.dev0
|
|
4
|
+
Summary: Python client sdk of [RAGFlow](https://github.com/infiniflow/ragflow). RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine based on deep document understanding.
|
|
5
|
+
Author-email: Zhichang Yu <yuzhichang@gmail.com>
|
|
6
|
+
License: Apache License, Version 2.0
|
|
7
|
+
Requires-Python: <3.14,>=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests<3.0.0,>=2.30.0
|
|
10
|
+
Requires-Dist: beartype<1.0.0,>=0.20.0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ragflow_sdk/__init__.py,sha256=ql_QJdp4vDpQ82flx5mdaQVX-Jn1ExmN_lYfPKGuaS4,1319
|
|
2
|
+
ragflow_sdk/ragflow.py,sha256=lVbpO1JHr8KNB9aDpkdfq967tbuXkIG-N7x4dPJo0oQ,13954
|
|
3
|
+
ragflow_sdk/modules/__init__.py,sha256=UQpEQcTBGyFrQsFf8jsVbreWSQA39-TPtgjjtoOpyQY,622
|
|
4
|
+
ragflow_sdk/modules/agent.py,sha256=ocygkPNRm44GKVozpSCe6eOvkXhMao6axpFMmZPd92E,2793
|
|
5
|
+
ragflow_sdk/modules/base.py,sha256=uBGd7yUFqfobmdhsWmP-nqNFmYwvcW_juT1Gu3-Qw-4,1918
|
|
6
|
+
ragflow_sdk/modules/chat.py,sha256=v7hljH8DVBNmhJjKw7F9y-lhtu-CzZkTGuCiMpXaeak,2656
|
|
7
|
+
ragflow_sdk/modules/chunk.py,sha256=HAj7uRkaVTOow9Ozc_BlbLH-2p1zel-ucBsZs1T2VXc,2137
|
|
8
|
+
ragflow_sdk/modules/dataset.py,sha256=3JN_HWEu7h-IK9lUdSLkIWEw9ZyHgbUo86BugxlroUI,6503
|
|
9
|
+
ragflow_sdk/modules/document.py,sha256=tVk1bCuoOEOAPKHD_ULx0smZ04A52pmY2OGSF_6WAkU,4412
|
|
10
|
+
ragflow_sdk/modules/memory.py,sha256=6Gp140KJPd8mB0hdkIf0rCc_RjRqoA7S6d4glYu4pcE,3114
|
|
11
|
+
ragflow_sdk/modules/session.py,sha256=pRpIFuIX6TuF-EZcaYLzZLFDA1QmVkIClavXhER_EUY,7898
|
|
12
|
+
ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/METADATA,sha256=tdjOvgs9mY_sJahROw_EIA3nn2DJ-31t6dm4P3Vz0c0,495
|
|
13
|
+
ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/top_level.txt,sha256=ep9RG2ggpS-qOwUXbtEbjfVyi6mVgQ407otRgO-xNdA,12
|
|
15
|
+
ragflow_sdk_myfbee_test-0.27.2.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ragflow_sdk
|