mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-10-11 21:39:54 +00:00
Refactor: reformat all code for lefthook using ruff and gofmt (#16585)
This commit is contained in:
+33
-33
@@ -25,14 +25,14 @@ import requests
|
||||
|
||||
class HttpClient:
|
||||
def __init__(
|
||||
self,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 9381,
|
||||
api_version: str = "v1",
|
||||
api_key: Optional[str] = None,
|
||||
connect_timeout: float = 5.0,
|
||||
read_timeout: float = 60.0,
|
||||
verify_ssl: bool = False,
|
||||
self,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 9381,
|
||||
api_version: str = "v1",
|
||||
api_key: Optional[str] = None,
|
||||
connect_timeout: float = 5.0,
|
||||
read_timeout: float = 60.0,
|
||||
verify_ssl: bool = False,
|
||||
) -> None:
|
||||
self.host = host
|
||||
self.port = port
|
||||
@@ -71,19 +71,19 @@ class HttpClient:
|
||||
return headers
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
use_api_base: bool = True,
|
||||
auth_kind: Optional[str] = "api",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
json_body: Optional[Dict[str, Any]] = None,
|
||||
data: Any = None,
|
||||
files: Any = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
stream: bool = False,
|
||||
iterations: int = 1,
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
use_api_base: bool = True,
|
||||
auth_kind: Optional[str] = "api",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
json_body: Optional[Dict[str, Any]] = None,
|
||||
data: Any = None,
|
||||
files: Any = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
stream: bool = False,
|
||||
iterations: int = 1,
|
||||
) -> requests.Response | dict:
|
||||
url = self.build_url(path, use_api_base=use_api_base)
|
||||
merged_headers = self._headers(auth_kind, headers)
|
||||
@@ -144,18 +144,18 @@ class HttpClient:
|
||||
# )
|
||||
|
||||
def request_json(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
use_api_base: bool = True,
|
||||
auth_kind: Optional[str] = "api",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
json_body: Optional[Dict[str, Any]] = None,
|
||||
data: Any = None,
|
||||
files: Any = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
stream: bool = False,
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
use_api_base: bool = True,
|
||||
auth_kind: Optional[str] = "api",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
json_body: Optional[Dict[str, Any]] = None,
|
||||
data: Any = None,
|
||||
files: Any = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
stream: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
response = self.request(
|
||||
method,
|
||||
|
||||
+19
-21
@@ -336,8 +336,8 @@ reset_default_asr: RESET DEFAULT ASR ";"
|
||||
reset_default_tts: RESET DEFAULT TTS ";"
|
||||
|
||||
list_user_datasets: LIST DATASETS ";"
|
||||
create_user_dataset_with_parser: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PARSER quoted_string ";"
|
||||
create_user_dataset_with_pipeline: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PIPELINE quoted_string ";"
|
||||
create_user_dataset_with_parser: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PARSER quoted_string ";"
|
||||
create_user_dataset_with_pipeline: CREATE DATASET quoted_string WITH EMBEDDING quoted_string PIPELINE quoted_string ";"
|
||||
drop_user_dataset: DROP DATASET quoted_string ";"
|
||||
list_user_dataset_files: LIST FILES OF DATASET quoted_string ";"
|
||||
list_user_dataset_documents: LIST DOCUMENTS OF DATASET quoted_string ";"
|
||||
@@ -640,15 +640,13 @@ class RAGFlowCLITransformer(Transformer):
|
||||
dataset_name = items[2].children[0].strip("'\"")
|
||||
embedding = items[5].children[0].strip("'\"")
|
||||
parser_type = items[7].children[0].strip("'\"")
|
||||
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding,
|
||||
"parser_type": parser_type}
|
||||
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding, "parser_type": parser_type}
|
||||
|
||||
def create_user_dataset_with_pipeline(self, items):
|
||||
dataset_name = items[2].children[0].strip("'\"")
|
||||
embedding = items[5].children[0].strip("'\"")
|
||||
pipeline = items[7].children[0].strip("'\"")
|
||||
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding,
|
||||
"pipeline": pipeline}
|
||||
return {"type": "create_user_dataset", "dataset_name": dataset_name, "embedding": embedding, "pipeline": pipeline}
|
||||
|
||||
def drop_user_dataset(self, items):
|
||||
dataset_name = items[2].children[0].strip("'\"")
|
||||
@@ -666,7 +664,7 @@ class RAGFlowCLITransformer(Transformer):
|
||||
dataset_names = []
|
||||
dataset_names.append(items[4].children[0].strip("'\""))
|
||||
for i in range(5, len(items)):
|
||||
if items[i] and hasattr(items[i], 'children') and items[i].children:
|
||||
if items[i] and hasattr(items[i], "children") and items[i].children:
|
||||
dataset_names.append(items[i].children[0].strip("'\""))
|
||||
return {"type": "list_user_datasets_metadata", "dataset_names": dataset_names}
|
||||
|
||||
@@ -675,7 +673,7 @@ class RAGFlowCLITransformer(Transformer):
|
||||
doc_ids = []
|
||||
if len(items) > 6 and items[6] == "DOCUMENTS":
|
||||
for i in range(7, len(items)):
|
||||
if items[i] and hasattr(items[i], 'children') and items[i].children:
|
||||
if items[i] and hasattr(items[i], "children") and items[i].children:
|
||||
doc_id = items[i].children[0].strip("'\"")
|
||||
doc_ids.append(doc_id)
|
||||
return {"type": "list_user_documents_metadata_summary", "dataset_name": dataset_name, "document_ids": doc_ids}
|
||||
@@ -698,17 +696,17 @@ class RAGFlowCLITransformer(Transformer):
|
||||
dataset_name = None
|
||||
vector_size = None
|
||||
for i, item in enumerate(items):
|
||||
if hasattr(item, 'data') and item.data == 'quoted_string':
|
||||
if hasattr(item, "data") and item.data == "quoted_string":
|
||||
dataset_name = item.children[0].strip("'\"")
|
||||
if hasattr(item, 'type') and item.type == 'NUMBER':
|
||||
if i > 0 and items[i-1].type == 'SIZE' and items[i-2].type == 'VECTOR':
|
||||
if hasattr(item, "type") and item.type == "NUMBER":
|
||||
if i > 0 and items[i - 1].type == "SIZE" and items[i - 2].type == "VECTOR":
|
||||
vector_size = int(item)
|
||||
return {"type": "create_dataset_table", "dataset_name": dataset_name, "vector_size": vector_size}
|
||||
|
||||
def drop_dataset_table(self, items):
|
||||
dataset_name = None
|
||||
for item in items:
|
||||
if hasattr(item, 'data') and item.data == 'quoted_string':
|
||||
if hasattr(item, "data") and item.data == "quoted_string":
|
||||
dataset_name = item.children[0].strip("'\"")
|
||||
return {"type": "drop_dataset_table", "dataset_name": dataset_name}
|
||||
|
||||
@@ -792,7 +790,7 @@ class RAGFlowCLITransformer(Transformer):
|
||||
|
||||
def update_chunk(self, items):
|
||||
def get_quoted_value(item):
|
||||
if hasattr(item, 'children') and item.children:
|
||||
if hasattr(item, "children") and item.children:
|
||||
return item.children[0].strip("'\"")
|
||||
return str(item).strip("'\"")
|
||||
|
||||
@@ -813,16 +811,16 @@ class RAGFlowCLITransformer(Transformer):
|
||||
for i in range(2, len(items)):
|
||||
item = items[i]
|
||||
# Check for FROM token to stop
|
||||
if hasattr(item, 'type') and item.type == 'FROM':
|
||||
if hasattr(item, "type") and item.type == "FROM":
|
||||
break
|
||||
if hasattr(item, 'children') and item.children:
|
||||
if hasattr(item, "children") and item.children:
|
||||
tag = item.children[0].strip("'\"")
|
||||
tags.append(tag)
|
||||
# Find dataset_name: quoted_string after DATASET
|
||||
dataset_name = None
|
||||
for i, item in enumerate(items):
|
||||
# Check if item is a DATASET token
|
||||
if hasattr(item, 'type') and item.type == 'DATASET':
|
||||
if hasattr(item, "type") and item.type == "DATASET":
|
||||
# Next item should be quoted_string
|
||||
dataset_name = items[i + 1].children[0].strip("'\"")
|
||||
break
|
||||
@@ -835,10 +833,10 @@ class RAGFlowCLITransformer(Transformer):
|
||||
|
||||
# Check if it's "REMOVE ALL CHUNKS"
|
||||
for item in items:
|
||||
if hasattr(item, 'type') and item.type == 'ALL':
|
||||
if hasattr(item, "type") and item.type == "ALL":
|
||||
# Find doc_id
|
||||
for j, inner_item in enumerate(items):
|
||||
if hasattr(inner_item, 'type') and inner_item.type == 'DOCUMENT':
|
||||
if hasattr(inner_item, "type") and inner_item.type == "DOCUMENT":
|
||||
doc_id = items[j + 1].children[0].strip("'\"")
|
||||
return {"type": "remove_chunks", "doc_id": doc_id, "delete_all": True}
|
||||
|
||||
@@ -846,12 +844,12 @@ class RAGFlowCLITransformer(Transformer):
|
||||
chunk_ids = []
|
||||
doc_id = None
|
||||
for i, item in enumerate(items):
|
||||
if hasattr(item, 'type') and item.type == 'DOCUMENT':
|
||||
if hasattr(item, "type") and item.type == "DOCUMENT":
|
||||
doc_id = items[i + 1].children[0].strip("'\"")
|
||||
elif hasattr(item, 'children') and item.children:
|
||||
elif hasattr(item, "children") and item.children:
|
||||
val = item.children[0].strip("'\"")
|
||||
# Skip if it's "FROM" or "DOCUMENT"
|
||||
if val.upper() in ['FROM', 'DOCUMENT']:
|
||||
if val.upper() in ["FROM", "DOCUMENT"]:
|
||||
continue
|
||||
chunk_ids.append(val)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from user import login_user
|
||||
|
||||
warnings.filterwarnings("ignore", category=getpass.GetPassWarning)
|
||||
|
||||
|
||||
def encrypt(input_string):
|
||||
pub = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB\n-----END PUBLIC KEY-----"
|
||||
pub_key = RSA.importKey(pub)
|
||||
@@ -49,9 +50,6 @@ def encode_to_base64(input_string):
|
||||
return base64_encoded.decode("utf-8")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class RAGFlowCLI(Cmd):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -240,9 +238,9 @@ class RAGFlowCLI(Cmd):
|
||||
print(r"""
|
||||
____ ___ ______________ ________ ____
|
||||
/ __ \/ | / ____/ ____/ /___ _ __ / ____/ / / _/
|
||||
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / / / / / /
|
||||
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / /___/ /____/ /
|
||||
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ \____/_____/___/
|
||||
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / / / / / /
|
||||
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / /___/ /____/ /
|
||||
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ \____/_____/___/
|
||||
""")
|
||||
self.cmdloop()
|
||||
|
||||
@@ -254,15 +252,13 @@ class RAGFlowCLI(Cmd):
|
||||
result = self.parse_command(command)
|
||||
self.execute_command(result)
|
||||
|
||||
|
||||
def parse_connection_args(self, args: List[str]) -> Dict[str, Any]:
|
||||
parser = argparse.ArgumentParser(description="RAGFlow CLI Client", add_help=False)
|
||||
parser.add_argument("-h", "--host", default="127.0.0.1", help="Admin or RAGFlow service host")
|
||||
parser.add_argument("-p", "--port", type=int, default=9381, help="Admin or RAGFlow service port")
|
||||
parser.add_argument("-w", "--password", default="admin", type=str, help="Superuser password")
|
||||
parser.add_argument("-t", "--type", default="admin", type=str, help="CLI mode, admin or user")
|
||||
parser.add_argument("-u", "--username", default=None,
|
||||
help="Username (email). In admin mode defaults to [email protected], in user mode required.")
|
||||
parser.add_argument("-u", "--username", default=None, help="Username (email). In admin mode defaults to [email protected], in user mode required.")
|
||||
parser.add_argument("command", nargs="?", help="Single command")
|
||||
try:
|
||||
parsed_args, remaining_args = parser.parse_known_args(args)
|
||||
@@ -274,7 +270,7 @@ class RAGFlowCLI(Cmd):
|
||||
|
||||
if remaining_args:
|
||||
if remaining_args[0] == "command":
|
||||
command_str = ' '.join(remaining_args[1:]) + ';'
|
||||
command_str = " ".join(remaining_args[1:]) + ";"
|
||||
auth = True
|
||||
if remaining_args[1] == "register":
|
||||
auth = False
|
||||
@@ -282,28 +278,14 @@ class RAGFlowCLI(Cmd):
|
||||
if username is None:
|
||||
print("Error: username (-u) is required in user mode")
|
||||
return {"error": "Username required"}
|
||||
return {
|
||||
"host": parsed_args.host,
|
||||
"port": parsed_args.port,
|
||||
"password": parsed_args.password,
|
||||
"type": parsed_args.type,
|
||||
"username": username,
|
||||
"command": command_str,
|
||||
"auth": auth
|
||||
}
|
||||
return {"host": parsed_args.host, "port": parsed_args.port, "password": parsed_args.password, "type": parsed_args.type, "username": username, "command": command_str, "auth": auth}
|
||||
else:
|
||||
return {"error": "Invalid command"}
|
||||
else:
|
||||
auth = True
|
||||
if username is None:
|
||||
auth = False
|
||||
return {
|
||||
"host": parsed_args.host,
|
||||
"port": parsed_args.port,
|
||||
"type": parsed_args.type,
|
||||
"username": username,
|
||||
"auth": auth
|
||||
}
|
||||
return {"host": parsed_args.host, "port": parsed_args.port, "type": parsed_args.type, "username": username, "auth": auth}
|
||||
except SystemExit:
|
||||
return {"error": "Invalid connection arguments"}
|
||||
|
||||
@@ -321,6 +303,7 @@ class RAGFlowCLI(Cmd):
|
||||
# print(f"Parsed command: {command_dict}")
|
||||
run_command(self.ragflow_client, command_dict)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
cli = RAGFlowCLI()
|
||||
|
||||
+91
-229
@@ -71,6 +71,7 @@ class RAGFlowClient:
|
||||
user_password: str = command.get("password")
|
||||
if not user_password:
|
||||
import getpass
|
||||
|
||||
user_password = getpass.getpass("Password: ")
|
||||
try:
|
||||
token = login_user(self.http_client, self.server_type, email, user_password)
|
||||
@@ -86,8 +87,7 @@ class RAGFlowClient:
|
||||
def ping_server(self, command):
|
||||
iterations = command.get("iterations", 1)
|
||||
if iterations > 1:
|
||||
response = self.http_client.request("GET", "/system/ping", use_api_base=True, auth_kind="web",
|
||||
iterations=iterations)
|
||||
response = self.http_client.request("GET", "/system/ping", use_api_base=True, auth_kind="web", iterations=iterations)
|
||||
return response
|
||||
else:
|
||||
response = self.http_client.request("GET", "/system/ping", use_api_base=True, auth_kind="web")
|
||||
@@ -106,8 +106,7 @@ class RAGFlowClient:
|
||||
enc_password = encrypt_password(password)
|
||||
print(f"Register user: {nickname}, email: {username}, password: ******")
|
||||
payload = {"email": username, "nickname": nickname, "password": enc_password}
|
||||
response = self.http_client.request(method="POST", path="/users",
|
||||
json_body=payload, use_api_base=True, auth_kind="web")
|
||||
response = self.http_client.request(method="POST", path="/users", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json["code"] == 0:
|
||||
@@ -135,8 +134,7 @@ class RAGFlowClient:
|
||||
|
||||
service_id: int = command["number"]
|
||||
|
||||
response = self.http_client.request("GET", f"/admin/services/{service_id}", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", f"/admin/services/{service_id}", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
res_data = res_json["data"]
|
||||
@@ -226,9 +224,7 @@ class RAGFlowClient:
|
||||
password_tree: Tree = command["password"]
|
||||
password: str = password_tree.children[0].strip("'\"")
|
||||
print(f"Alter user: {user_name}, password: ******")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name}/password",
|
||||
json_body={"new_password": encrypt_password(password)}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name}/password", json_body={"new_password": encrypt_password(password)}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print(res_json["message"])
|
||||
@@ -247,9 +243,7 @@ class RAGFlowClient:
|
||||
print(f"Create user: {user_name}, password: ******, role: {role}")
|
||||
# enpass1 = encrypt(password)
|
||||
enc_password = encrypt_password(password)
|
||||
response = self.http_client.request(method="POST", path="/admin/users",
|
||||
json_body={"username": user_name, "password": enc_password, "role": role},
|
||||
use_api_base=True, auth_kind="admin")
|
||||
response = self.http_client.request(method="POST", path="/admin/users", json_body={"username": user_name, "password": enc_password, "role": role}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -266,9 +260,7 @@ class RAGFlowClient:
|
||||
activate_status: str = activate_tree.children[0].strip("'\"")
|
||||
if activate_status.lower() in ["on", "off"]:
|
||||
print(f"Alter user {user_name} activate status, turn {activate_status.lower()}.")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name}/activate",
|
||||
json_body={"activate_status": activate_status}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name}/activate", json_body={"activate_status": activate_status}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print(res_json["message"])
|
||||
@@ -283,14 +275,12 @@ class RAGFlowClient:
|
||||
|
||||
user_name_tree: Tree = command["user_name"]
|
||||
user_name: str = user_name_tree.children[0].strip("'\"")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name}/admin", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name}/admin", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print(res_json["message"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to grant {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to grant {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def revoke_admin(self, command):
|
||||
if self.server_type != "admin":
|
||||
@@ -298,14 +288,12 @@ class RAGFlowClient:
|
||||
|
||||
user_name_tree: Tree = command["user_name"]
|
||||
user_name: str = user_name_tree.children[0].strip("'\"")
|
||||
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/admin", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/admin", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print(res_json["message"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to revoke {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to revoke {user_name} admin authorization, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def create_role(self, command):
|
||||
if self.server_type != "admin":
|
||||
@@ -319,10 +307,7 @@ class RAGFlowClient:
|
||||
desc_str = desc_tree.children[0].strip("'\"")
|
||||
|
||||
print(f"create role name: {role_name}, description: {desc_str}")
|
||||
response = self.http_client.request("POST", "/admin/roles",
|
||||
json_body={"role_name": role_name, "description": desc_str},
|
||||
use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("POST", "/admin/roles", json_body={"role_name": role_name, "description": desc_str}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -336,9 +321,7 @@ class RAGFlowClient:
|
||||
role_name_tree: Tree = command["role_name"]
|
||||
role_name: str = role_name_tree.children[0].strip("'\"")
|
||||
print(f"drop role name: {role_name}")
|
||||
response = self.http_client.request("DELETE", f"/admin/roles/{role_name}",
|
||||
use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("DELETE", f"/admin/roles/{role_name}", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -355,24 +338,18 @@ class RAGFlowClient:
|
||||
desc_str: str = desc_tree.children[0].strip("'\"")
|
||||
|
||||
print(f"alter role name: {role_name}, description: {desc_str}")
|
||||
response = self.http_client.request("PUT", f"/admin/roles/{role_name}",
|
||||
json_body={"description": desc_str},
|
||||
use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("PUT", f"/admin/roles/{role_name}", json_body={"description": desc_str}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to update role {role_name} with description: {desc_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to update role {role_name} with description: {desc_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def list_roles(self, command):
|
||||
if self.server_type != "admin":
|
||||
print("This command is only allowed in ADMIN mode")
|
||||
|
||||
response = self.http_client.request("GET", "/admin/roles",
|
||||
use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", "/admin/roles", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -386,9 +363,7 @@ class RAGFlowClient:
|
||||
role_name_tree: Tree = command["role_name"]
|
||||
role_name: str = role_name_tree.children[0].strip("'\"")
|
||||
print(f"show role: {role_name}")
|
||||
response = self.http_client.request("GET", f"/admin/roles/{role_name}/permission",
|
||||
use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", f"/admin/roles/{role_name}/permission", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -409,15 +384,12 @@ class RAGFlowClient:
|
||||
action_str: str = action_tree.children[0].strip("'\"")
|
||||
actions.append(action_str)
|
||||
print(f"grant role_name: {role_name_str}, resource: {resource_str}, actions: {actions}")
|
||||
response = self.http_client.request("POST", f"/admin/roles/{role_name_str}/permission",
|
||||
json_body={"actions": actions, "resource": resource_str}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("POST", f"/admin/roles/{role_name_str}/permission", json_body={"actions": actions, "resource": resource_str}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to grant role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to grant role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def revoke_permission(self, command):
|
||||
if self.server_type != "admin":
|
||||
@@ -433,15 +405,12 @@ class RAGFlowClient:
|
||||
action_str: str = action_tree.children[0].strip("'\"")
|
||||
actions.append(action_str)
|
||||
print(f"revoke role_name: {role_name_str}, resource: {resource_str}, actions: {actions}")
|
||||
response = self.http_client.request("DELETE", f"/admin/roles/{role_name_str}/permission",
|
||||
json_body={"actions": actions, "resource": resource_str}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("DELETE", f"/admin/roles/{role_name_str}/permission", json_body={"actions": actions, "resource": resource_str}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to revoke role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to revoke role {role_name_str} with {actions} on {resource_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def alter_user_role(self, command):
|
||||
if self.server_type != "admin":
|
||||
@@ -452,15 +421,12 @@ class RAGFlowClient:
|
||||
user_name_tree: Tree = command["user_name"]
|
||||
user_name_str: str = user_name_tree.children[0].strip("'\"")
|
||||
print(f"alter_user_role user_name: {user_name_str}, role_name: {role_name_str}")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name_str}/role",
|
||||
json_body={"role_name": role_name_str}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("PUT", f"/admin/users/{user_name_str}/role", json_body={"role_name": role_name_str}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to alter user: {user_name_str} to role {role_name_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to alter user: {user_name_str} to role {role_name_str}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def show_user_permission(self, command):
|
||||
if self.server_type != "admin":
|
||||
@@ -469,14 +435,12 @@ class RAGFlowClient:
|
||||
user_name_tree: Tree = command["user_name"]
|
||||
user_name_str: str = user_name_tree.children[0].strip("'\"")
|
||||
print(f"show_user_permission user_name: {user_name_str}")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name_str}/permission", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name_str}/permission", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to show user: {user_name_str} permission, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to show user: {user_name_str} permission, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def generate_key(self, command: dict[str, Any]) -> None:
|
||||
if self.server_type != "admin":
|
||||
@@ -485,14 +449,12 @@ class RAGFlowClient:
|
||||
username_tree: Tree = command["user_name"]
|
||||
user_name: str = username_tree.children[0].strip("'\"")
|
||||
print(f"Generating API key for user: {user_name}")
|
||||
response = self.http_client.request("POST", f"/admin/users/{user_name}/keys", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("POST", f"/admin/users/{user_name}/keys", use_api_base=True, auth_kind="admin")
|
||||
res_json: dict[str, Any] = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
else:
|
||||
print(
|
||||
f"Failed to generate key for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Failed to generate key for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def list_keys(self, command: dict[str, Any]) -> None:
|
||||
if self.server_type != "admin":
|
||||
@@ -501,8 +463,7 @@ class RAGFlowClient:
|
||||
username_tree: Tree = command["user_name"]
|
||||
user_name: str = username_tree.children[0].strip("'\"")
|
||||
print(f"Listing API keys for user: {user_name}")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name}/keys", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name}/keys", use_api_base=True, auth_kind="admin")
|
||||
res_json: dict[str, Any] = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -520,8 +481,7 @@ class RAGFlowClient:
|
||||
print(f"Dropping API key for user: {user_name}")
|
||||
# URL encode the key to handle special characters
|
||||
encoded_key: str = urllib.parse.quote(key, safe="")
|
||||
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/keys/{encoded_key}", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("DELETE", f"/admin/users/{user_name}/keys/{encoded_key}", use_api_base=True, auth_kind="admin")
|
||||
res_json: dict[str, Any] = response.json()
|
||||
if response.status_code == 200:
|
||||
print(res_json["message"])
|
||||
@@ -534,23 +494,19 @@ class RAGFlowClient:
|
||||
|
||||
var_name = _strip_tree_value(command["var_name"])
|
||||
var_value = _strip_tree_value(command["var_value"])
|
||||
response = self.http_client.request("PUT", "/admin/variables",
|
||||
json_body={"var_name": var_name, "var_value": var_value}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("PUT", "/admin/variables", json_body={"var_name": var_name, "var_value": var_value}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print(res_json["message"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to set variable {var_name} to {var_value}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to set variable {var_name} to {var_value}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def show_variable(self, command):
|
||||
if self.server_type != "admin":
|
||||
print("This command is only allowed in ADMIN mode")
|
||||
|
||||
var_name = _strip_tree_value(command["var_name"])
|
||||
response = self.http_client.request(method="GET", path="/admin/variables", json_body={"var_name": var_name},
|
||||
use_api_base=True, auth_kind="admin")
|
||||
response = self.http_client.request(method="GET", path="/admin/variables", json_body={"var_name": var_name}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -604,8 +560,7 @@ class RAGFlowClient:
|
||||
if self.server_type != "admin":
|
||||
print("This command is only allowed in ADMIN mode")
|
||||
license = command["license"]
|
||||
response = self.http_client.request("POST", "/admin/license", json_body={"license": license}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("POST", "/admin/license", json_body={"license": license}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print("Set license successfully")
|
||||
@@ -617,9 +572,7 @@ class RAGFlowClient:
|
||||
print("This command is only allowed in ADMIN mode")
|
||||
value1 = command["value1"]
|
||||
value2 = command["value2"]
|
||||
response = self.http_client.request("POST", "/admin/license/config",
|
||||
json_body={"value1": value1, "value2": value2}, use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("POST", "/admin/license/config", json_body={"value1": value1, "value2": value2}, use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
print("Set license successfully")
|
||||
@@ -690,8 +643,7 @@ class RAGFlowClient:
|
||||
user_name: str = username_tree.children[0].strip("'\"")
|
||||
print(f"Listing all datasets of user: {user_name}")
|
||||
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name}/datasets", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name}/datasets", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
table_data = res_json["data"]
|
||||
@@ -708,8 +660,7 @@ class RAGFlowClient:
|
||||
username_tree: Tree = command["user_name"]
|
||||
user_name: str = username_tree.children[0].strip("'\"")
|
||||
print(f"Listing all agents of user: {user_name}")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name}/agents", use_api_base=True,
|
||||
auth_kind="admin")
|
||||
response = self.http_client.request("GET", f"/admin/users/{user_name}/agents", use_api_base=True, auth_kind="admin")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
table_data = res_json["data"]
|
||||
@@ -733,8 +684,7 @@ class RAGFlowClient:
|
||||
|
||||
# Step 1: Add provider
|
||||
provider_payload = {"provider_name": provider_name}
|
||||
provider_response = self.http_client.request("PUT", "/providers", json_body=provider_payload,
|
||||
use_api_base=True, auth_kind="web")
|
||||
provider_response = self.http_client.request("PUT", "/providers", json_body=provider_payload, use_api_base=True, auth_kind="web")
|
||||
provider_res = provider_response.json()
|
||||
if provider_response.status_code == 200 and provider_res.get("code") == 0:
|
||||
print(f"Success to add provider {provider_name}")
|
||||
@@ -747,15 +697,8 @@ class RAGFlowClient:
|
||||
return
|
||||
|
||||
# Step 2: Add instance
|
||||
instance_payload = {
|
||||
"instance_name": "default",
|
||||
"api_key": api_key,
|
||||
"region": "default",
|
||||
"base_url": ""
|
||||
}
|
||||
instance_response = self.http_client.request("POST", f"/providers/{provider_name}/instances",
|
||||
json_body=instance_payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
instance_payload = {"instance_name": "default", "api_key": api_key, "region": "default", "base_url": ""}
|
||||
instance_response = self.http_client.request("POST", f"/providers/{provider_name}/instances", json_body=instance_payload, use_api_base=True, auth_kind="web")
|
||||
instance_res = instance_response.json()
|
||||
if instance_response.status_code == 200 and instance_res.get("code") == 0:
|
||||
print(f"Success to add instance for provider {provider_name}")
|
||||
@@ -771,8 +714,7 @@ class RAGFlowClient:
|
||||
print("This command is only allowed in USER mode")
|
||||
return
|
||||
provider_name: str = command["provider_name"]
|
||||
response = self.http_client.request("DELETE", f"/providers/{provider_name}", use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("DELETE", f"/providers/{provider_name}", use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print(f"Success to drop model provider {provider_name}")
|
||||
@@ -810,8 +752,7 @@ class RAGFlowClient:
|
||||
"model_type": model_type,
|
||||
"model_name": model_name,
|
||||
}
|
||||
response = self.http_client.request("PATCH", "/models/default", json_body=payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("PATCH", "/models/default", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print(f"Success to set default {model_type} to {model_id}")
|
||||
@@ -830,8 +771,7 @@ class RAGFlowClient:
|
||||
return
|
||||
|
||||
payload = {"model_type": model_type}
|
||||
response = self.http_client.request("PATCH", "/models/default", json_body=payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("PATCH", "/models/default", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print(f"Success to reset default {model_type}")
|
||||
@@ -861,8 +801,7 @@ class RAGFlowClient:
|
||||
|
||||
iterations = command.get("iterations", 1)
|
||||
if iterations > 1:
|
||||
response = self.http_client.request("GET", "/datasets", use_api_base=True, auth_kind="web",
|
||||
iterations=iterations)
|
||||
response = self.http_client.request("GET", "/datasets", use_api_base=True, auth_kind="web", iterations=iterations)
|
||||
return response
|
||||
else:
|
||||
response = self.http_client.request("GET", "/datasets", use_api_base=True, auth_kind="web")
|
||||
@@ -876,16 +815,12 @@ class RAGFlowClient:
|
||||
def create_user_dataset(self, command):
|
||||
if self.server_type != "user":
|
||||
print("This command is only allowed in USER mode")
|
||||
payload = {
|
||||
"name": command["dataset_name"],
|
||||
"embedding_model": command["embedding"]
|
||||
}
|
||||
payload = {"name": command["dataset_name"], "embedding_model": command["embedding"]}
|
||||
if "parser_id" in command:
|
||||
payload["chunk_method"] = command["parser"]
|
||||
if "pipeline" in command:
|
||||
payload["pipeline_id"] = command["pipeline"]
|
||||
response = self.http_client.request("POST", "/datasets", json_body=payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("POST", "/datasets", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
self._print_table_simple(res_json["data"])
|
||||
@@ -981,8 +916,7 @@ class RAGFlowClient:
|
||||
|
||||
dataset_ids = [dataset_id for _, dataset_id in valid_datasets]
|
||||
kb_ids_param = ",".join(dataset_ids)
|
||||
response = self.http_client.request("GET", f"/kb/get_meta?kb_ids={kb_ids_param}",
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("GET", f"/kb/get_meta?kb_ids={kb_ids_param}", use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code != 200:
|
||||
print(f"Fail to get metadata, code: {res_json.get('code')}, message: {res_json.get('message')}")
|
||||
@@ -996,11 +930,7 @@ class RAGFlowClient:
|
||||
table_data = []
|
||||
for field_name, values_dict in meta.items():
|
||||
for value, docs in values_dict.items():
|
||||
table_data.append({
|
||||
"field": field_name,
|
||||
"value": value,
|
||||
"doc_ids": ", ".join(docs)
|
||||
})
|
||||
table_data.append({"field": field_name, "value": value, "doc_ids": ", ".join(docs)})
|
||||
self._print_table_simple(table_data)
|
||||
|
||||
def list_user_documents_metadata_summary(self, command_dict):
|
||||
@@ -1018,8 +948,7 @@ class RAGFlowClient:
|
||||
payload = {"kb_id": kb_id}
|
||||
if doc_ids:
|
||||
payload["doc_ids"] = doc_ids
|
||||
response = self.http_client.request("POST", "/document/metadata/summary", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", "/document/metadata/summary", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
summary = res_json.get("data", {}).get("summary", {})
|
||||
@@ -1086,16 +1015,11 @@ class RAGFlowClient:
|
||||
"quote": True,
|
||||
"keyword": False,
|
||||
"tts": False,
|
||||
"system": "You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base.\n\n **Essential Rules:**\n - Your answer must be derived **solely** from this knowledge base: `{knowledge}`.\n - **When information is available**: Summarize the content to give a detailed answer.\n - **When information is unavailable**: Your response must contain this exact sentence: \"The answer you are looking for is not found in the knowledge base!\"\n - **Always consider** the entire conversation history.",
|
||||
"system": 'You are an intelligent assistant. Your primary function is to answer questions based strictly on the provided knowledge base.\n\n **Essential Rules:**\n - Your answer must be derived **solely** from this knowledge base: `{knowledge}`.\n - **When information is available**: Summarize the content to give a detailed answer.\n - **When information is unavailable**: Your response must contain this exact sentence: "The answer you are looking for is not found in the knowledge base!"\n - **Always consider** the entire conversation history.',
|
||||
"refine_multiturn": False,
|
||||
"use_kg": False,
|
||||
"reasoning": False,
|
||||
"parameters": [
|
||||
{
|
||||
"key": "knowledge",
|
||||
"optional": False
|
||||
}
|
||||
],
|
||||
"parameters": [{"key": "knowledge", "optional": False}],
|
||||
"toc_enhance": False,
|
||||
},
|
||||
"similarity_threshold": 0.2,
|
||||
@@ -1136,8 +1060,7 @@ class RAGFlowClient:
|
||||
# Build payload
|
||||
payload = {"kb_id": dataset_id, "vector_size": vector_size}
|
||||
# Call API
|
||||
response = self.http_client.request("POST", "/kb/doc_engine_table", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", "/kb/doc_engine_table", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print(f"Success to create table for dataset: {dataset_name}")
|
||||
@@ -1155,8 +1078,7 @@ class RAGFlowClient:
|
||||
return
|
||||
# Call API to delete table
|
||||
payload = {"kb_id": dataset_id}
|
||||
response = self.http_client.request("DELETE", "/kb/doc_engine_table", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("DELETE", "/kb/doc_engine_table", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print(f"Success to drop table for dataset: {dataset_name}")
|
||||
@@ -1168,8 +1090,7 @@ class RAGFlowClient:
|
||||
print("This command is only allowed in USER mode")
|
||||
return
|
||||
# Call API to create metadata table
|
||||
response = self.http_client.request("POST", "/tenant/doc_engine_metadata_table",
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", "/tenant/doc_engine_metadata_table", use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print("Success to create metadata table")
|
||||
@@ -1181,8 +1102,7 @@ class RAGFlowClient:
|
||||
print("This command is only allowed in USER mode")
|
||||
return
|
||||
# Call API to delete metadata table
|
||||
response = self.http_client.request("DELETE", "/tenant/doc_engine_metadata_table",
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("DELETE", "/tenant/doc_engine_metadata_table", use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json.get("code") == 0:
|
||||
print("Success to drop metadata table")
|
||||
@@ -1225,8 +1145,7 @@ class RAGFlowClient:
|
||||
|
||||
def _list_chat_sessions(self, dialog_id):
|
||||
"""List all sessions (conversations) for a given dialog."""
|
||||
response = self.http_client.request("GET", f"/chats/{dialog_id}/conversations", use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("GET", f"/chats/{dialog_id}/conversations", use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json["code"] == 0:
|
||||
return res_json["data"]
|
||||
@@ -1242,14 +1161,12 @@ class RAGFlowClient:
|
||||
if dialog_id is None:
|
||||
return
|
||||
payload = {"name": "New conversation"}
|
||||
response = self.http_client.request("POST", f"/chats/{dialog_id}/conversations", json_body=payload,
|
||||
use_api_base=True, auth_kind="web")
|
||||
response = self.http_client.request("POST", f"/chats/{dialog_id}/conversations", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json["code"] == 0:
|
||||
print(f"Success to create chat session for chat: {chat_name}")
|
||||
else:
|
||||
print(
|
||||
f"Fail to create chat session for chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to create chat session for chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def drop_chat_session(self, command):
|
||||
if self.server_type != "user":
|
||||
@@ -1270,14 +1187,12 @@ class RAGFlowClient:
|
||||
print(f"Chat session '{session_id}' not found in chat '{chat_name}'")
|
||||
return
|
||||
payload = {"ids": to_drop_session_ids}
|
||||
response = self.http_client.request("DELETE", f"/chats/{dialog_id}/conversations", json_body=payload,
|
||||
use_api_base=True, auth_kind="web")
|
||||
response = self.http_client.request("DELETE", f"/chats/{dialog_id}/conversations", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json["code"] == 0:
|
||||
print(f"Success to drop chat session '{session_id}' from chat: {chat_name}")
|
||||
else:
|
||||
print(
|
||||
f"Fail to drop chat session '{session_id}' from chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to drop chat session '{session_id}' from chat {chat_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def list_chat_sessions(self, command):
|
||||
if self.server_type != "user":
|
||||
@@ -1305,13 +1220,9 @@ class RAGFlowClient:
|
||||
|
||||
# Prepare payload for completion API
|
||||
# Note: stream parameter is not sent, server defaults to stream=True
|
||||
payload = {
|
||||
"session_id": session_id,
|
||||
"messages": [{"role": "user", "content": message}]
|
||||
}
|
||||
payload = {"session_id": session_id, "messages": [{"role": "user", "content": message}]}
|
||||
|
||||
response = self.http_client.request("POST", "/chat/completions", json_body=payload,
|
||||
use_api_base=True, auth_kind="web", stream=True)
|
||||
response = self.http_client.request("POST", "/chat/completions", json_body=payload, use_api_base=True, auth_kind="web", stream=True)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Fail to chat on session, status code: {response.status_code}")
|
||||
@@ -1322,17 +1233,16 @@ class RAGFlowClient:
|
||||
for line in response.iter_lines():
|
||||
if not line:
|
||||
continue
|
||||
line_str = line.decode('utf-8')
|
||||
if not line_str.startswith('data:'):
|
||||
line_str = line.decode("utf-8")
|
||||
if not line_str.startswith("data:"):
|
||||
continue
|
||||
data_str = line_str[5:].strip()
|
||||
if data_str == '[DONE]':
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
data_json = json.loads(data_str)
|
||||
if data_json.get("code") != 0:
|
||||
print(
|
||||
f"\nFail to chat on session, code: {data_json.get('code')}, message: {data_json.get('message', '')}")
|
||||
print(f"\nFail to chat on session, code: {data_json.get('code')}, message: {data_json.get('message', '')}")
|
||||
return
|
||||
# Check if it's the final message
|
||||
if data_json.get("data") is True:
|
||||
@@ -1416,14 +1326,12 @@ class RAGFlowClient:
|
||||
print(f"Documents {document_names} not found in {dataset_name}")
|
||||
|
||||
payload = {"doc_ids": document_ids, "run": 1}
|
||||
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json["code"] == 0:
|
||||
print(f"Success to parse {to_parse_doc_names} of {dataset_name}")
|
||||
else:
|
||||
print(
|
||||
f"Fail to parse documents {res_json["data"]["docs"]}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to parse documents {res_json['data']['docs']}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def parse_dataset(self, command_dict):
|
||||
if self.server_type != "user":
|
||||
@@ -1442,8 +1350,7 @@ class RAGFlowClient:
|
||||
document_ids.append(doc["id"])
|
||||
|
||||
payload = {"doc_ids": document_ids, "run": 1}
|
||||
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("POST", "/documents/ingest", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200 and res_json["code"] == 0:
|
||||
pass
|
||||
@@ -1483,15 +1390,7 @@ class RAGFlowClient:
|
||||
encoder = MultipartEncoder(fields=fields)
|
||||
headers = {"Content-Type": encoder.content_type}
|
||||
response = self.http_client.request(
|
||||
"POST",
|
||||
f"/datasets/{dataset_id}/documents?return_raw_files=true",
|
||||
headers=headers,
|
||||
data=encoder,
|
||||
json_body=None,
|
||||
params=None,
|
||||
stream=False,
|
||||
auth_kind="web",
|
||||
use_api_base=True
|
||||
"POST", f"/datasets/{dataset_id}/documents?return_raw_files=true", headers=headers, data=encoder, json_body=None, params=None, stream=False, auth_kind="web", use_api_base=True
|
||||
)
|
||||
res = response.json()
|
||||
if res.get("code") == 0:
|
||||
@@ -1526,22 +1425,18 @@ class RAGFlowClient:
|
||||
}
|
||||
iterations = command_dict.get("iterations", 1)
|
||||
if iterations > 1:
|
||||
response = self.http_client.request("POST", "/retrieval", json_body=payload, use_api_base=True,
|
||||
auth_kind="web", iterations=iterations)
|
||||
response = self.http_client.request("POST", "/retrieval", json_body=payload, use_api_base=True, auth_kind="web", iterations=iterations)
|
||||
return response
|
||||
else:
|
||||
response = self.http_client.request("POST", "/retrieval", json_body=payload, use_api_base=True,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("POST", "/retrieval", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json["code"] == 0:
|
||||
self._print_table_simple(res_json["data"]["chunks"])
|
||||
else:
|
||||
print(
|
||||
f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
else:
|
||||
print(
|
||||
f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to search datasets: {dataset_names}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
|
||||
def get_chunk(self, command_dict):
|
||||
if self.server_type != "user":
|
||||
@@ -1549,8 +1444,7 @@ class RAGFlowClient:
|
||||
return
|
||||
|
||||
chunk_id = command_dict["chunk_id"]
|
||||
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json["code"] == 0:
|
||||
@@ -1568,8 +1462,7 @@ class RAGFlowClient:
|
||||
|
||||
file_path = command_dict["file_path"]
|
||||
payload = {"file_path": file_path}
|
||||
response = self.http_client.request("POST", "/kb/insert_from_file", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", "/kb/insert_from_file", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json["code"] == 0:
|
||||
@@ -1589,8 +1482,7 @@ class RAGFlowClient:
|
||||
|
||||
file_path = command_dict["file_path"]
|
||||
payload = {"file_path": file_path}
|
||||
response = self.http_client.request("POST", "/tenant/insert_metadata_from_file", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", "/tenant/insert_metadata_from_file", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json["code"] == 0:
|
||||
@@ -1617,8 +1509,7 @@ class RAGFlowClient:
|
||||
return
|
||||
|
||||
# Get doc_id from chunk_id via GET /chunk/get
|
||||
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("GET", f"/chunk/get?chunk_id={chunk_id}", use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code != 200:
|
||||
print(f"Fail to get chunk info, code: {res_json.get('code')}, message: {res_json.get('message')}")
|
||||
@@ -1655,14 +1546,8 @@ class RAGFlowClient:
|
||||
else:
|
||||
print(f"Fail to update chunk, HTTP {response.status_code}")
|
||||
|
||||
def _get_documents_by_ids(self, ids:list[str]):
|
||||
response = self.http_client.request(
|
||||
"POST",
|
||||
"/document/infos",
|
||||
json_body={"doc_ids": ids},
|
||||
use_api_base=False,
|
||||
auth_kind="web"
|
||||
)
|
||||
def _get_documents_by_ids(self, ids: list[str]):
|
||||
response = self.http_client.request("POST", "/document/infos", json_body={"doc_ids": ids}, use_api_base=False, auth_kind="web")
|
||||
|
||||
if response.status_code != 200:
|
||||
return f"Fail to get document info, HTTP {response.status_code}", None
|
||||
@@ -1687,6 +1572,7 @@ class RAGFlowClient:
|
||||
|
||||
# Parse JSON string to dict
|
||||
import json
|
||||
|
||||
try:
|
||||
meta_fields = json.loads(meta_json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -1713,13 +1599,7 @@ class RAGFlowClient:
|
||||
"meta_fields": meta_fields,
|
||||
}
|
||||
|
||||
response = self.http_client.request(
|
||||
"PATCH",
|
||||
f"/datasets/{dataset_id}/documents/{doc_id}",
|
||||
json_body=payload,
|
||||
use_api_base=True,
|
||||
auth_kind="web"
|
||||
)
|
||||
response = self.http_client.request("PATCH", f"/datasets/{dataset_id}/documents/{doc_id}", json_body=payload, use_api_base=True, auth_kind="web")
|
||||
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
@@ -1747,8 +1627,7 @@ class RAGFlowClient:
|
||||
"tags": tags,
|
||||
}
|
||||
|
||||
response = self.http_client.request("POST", f"/kb/{dataset_id}/rm_tags", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", f"/kb/{dataset_id}/rm_tags", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json.get("code") == 0:
|
||||
@@ -1771,8 +1650,7 @@ class RAGFlowClient:
|
||||
elif command_dict.get("chunk_ids"):
|
||||
payload["chunk_ids"] = command_dict["chunk_ids"]
|
||||
|
||||
response = self.http_client.request("POST", "/chunk/rm", json_body=payload,
|
||||
use_api_base=False, auth_kind="web")
|
||||
response = self.http_client.request("POST", "/chunk/rm", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json.get("code") == 0:
|
||||
@@ -1803,15 +1681,14 @@ class RAGFlowClient:
|
||||
if "available_int" in command_dict:
|
||||
payload["available_int"] = command_dict["available_int"]
|
||||
|
||||
response = self.http_client.request("POST", "/chunk/list", json_body=payload, use_api_base=False,
|
||||
auth_kind="web")
|
||||
response = self.http_client.request("POST", "/chunk/list", json_body=payload, use_api_base=False, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code == 200:
|
||||
if res_json["code"] == 0:
|
||||
chunks = res_json["data"]["chunks"]
|
||||
if chunks:
|
||||
for i, chunk in enumerate(chunks):
|
||||
print(f"\n--- Chunk {i+1} ---")
|
||||
print(f"\n--- Chunk {i + 1} ---")
|
||||
for key, value in chunk.items():
|
||||
print(f" {key}: {value}")
|
||||
else:
|
||||
@@ -1845,7 +1722,7 @@ class RAGFlowClient:
|
||||
all_done = True
|
||||
for doc in docs:
|
||||
if doc.get("run") != "DONE":
|
||||
print(f"Document {doc["name"]} is not done, status: {doc.get("run")}")
|
||||
print(f"Document {doc['name']} is not done, status: {doc.get('run')}")
|
||||
all_done = False
|
||||
break
|
||||
if all_done:
|
||||
@@ -1856,16 +1733,10 @@ class RAGFlowClient:
|
||||
|
||||
def _list_documents(self, dataset_name: str, dataset_id: str):
|
||||
# Use the new RESTful API: GET /api/v1/datasets/<dataset_id>/documents
|
||||
response = self.http_client.request(
|
||||
"GET",
|
||||
f"/datasets/{dataset_id}/documents",
|
||||
use_api_base=True,
|
||||
auth_kind="web"
|
||||
)
|
||||
response = self.http_client.request("GET", f"/datasets/{dataset_id}/documents", use_api_base=True, auth_kind="web")
|
||||
res_json = response.json()
|
||||
if response.status_code != 200:
|
||||
print(
|
||||
f"Fail to list files from dataset {dataset_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
print(f"Fail to list files from dataset {dataset_name}, code: {res_json['code']}, message: {res_json['message']}")
|
||||
return None
|
||||
return res_json["data"]["docs"]
|
||||
|
||||
@@ -2254,22 +2125,14 @@ def run_benchmark(client: RAGFlowClient, command_dict: dict):
|
||||
total_duration = result["duration"]
|
||||
qps = iterations / total_duration if total_duration > 0 else None
|
||||
print(f"command: {command}, Concurrency: {concurrency}, iterations: {iterations}")
|
||||
print(
|
||||
f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {iterations}, SUCCESS: {success_count}, FAILURE: {iterations - success_count}")
|
||||
print(f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {iterations}, SUCCESS: {success_count}, FAILURE: {iterations - success_count}")
|
||||
pass
|
||||
else:
|
||||
results: List[Optional[dict]] = [None] * concurrency
|
||||
mp_context = mp.get_context("spawn")
|
||||
start_time = time.perf_counter()
|
||||
with ProcessPoolExecutor(max_workers=concurrency, mp_context=mp_context) as executor:
|
||||
future_map = {
|
||||
executor.submit(
|
||||
run_command,
|
||||
client,
|
||||
command
|
||||
): idx
|
||||
for idx in range(concurrency)
|
||||
}
|
||||
future_map = {executor.submit(run_command, client, command): idx for idx in range(concurrency)}
|
||||
for future in as_completed(future_map):
|
||||
idx = future_map[future]
|
||||
results[idx] = future.result()
|
||||
@@ -2291,7 +2154,6 @@ def run_benchmark(client: RAGFlowClient, command_dict: dict):
|
||||
total_command_count = iterations * concurrency
|
||||
qps = total_command_count / total_duration if total_duration > 0 else None
|
||||
print(f"command: {command}, Concurrency: {concurrency} , iterations: {iterations}")
|
||||
print(
|
||||
f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {total_command_count}, SUCCESS: {success_count}, FAILURE: {total_command_count - success_count}")
|
||||
print(f"total duration: {total_duration:.4f}s, QPS: {qps}, COMMAND_COUNT: {total_command_count}, SUCCESS: {success_count}, FAILURE: {total_command_count - success_count}")
|
||||
|
||||
pass
|
||||
|
||||
@@ -29,6 +29,7 @@ def encrypt_password(password_plain: str) -> str:
|
||||
import base64
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
|
||||
|
||||
def crypt(line):
|
||||
"""
|
||||
decrypt(crypt(input_string)) == base64(input_string), which frontend and ragflow_cli use.
|
||||
@@ -36,13 +37,11 @@ def encrypt_password(password_plain: str) -> str:
|
||||
pub = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB\n-----END PUBLIC KEY-----"
|
||||
rsa_key = RSA.importKey(pub)
|
||||
cipher = Cipher_pkcs1_v1_5.new(rsa_key)
|
||||
password_base64 = base64.b64encode(line.encode('utf-8')).decode("utf-8")
|
||||
password_base64 = base64.b64encode(line.encode("utf-8")).decode("utf-8")
|
||||
encrypted_password = cipher.encrypt(password_base64.encode())
|
||||
return base64.b64encode(encrypted_password).decode('utf-8')
|
||||
return base64.b64encode(encrypted_password).decode("utf-8")
|
||||
except Exception as exc:
|
||||
raise AuthException(
|
||||
"Password encryption unavailable; install pycryptodomex (uv sync --python 3.13 --group test)."
|
||||
) from exc
|
||||
raise AuthException("Password encryption unavailable; install pycryptodomex (uv sync --python 3.13 --group test).") from exc
|
||||
return crypt(password_plain)
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#
|
||||
|
||||
import time
|
||||
|
||||
start_ts = time.time()
|
||||
|
||||
import os
|
||||
@@ -38,26 +39,24 @@ from common.versions import get_ragflow_version
|
||||
|
||||
stop_event = threading.Event()
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
faulthandler.enable()
|
||||
init_root_logger("admin_service")
|
||||
logging.info(r"""
|
||||
____ ___ ______________ ___ __ _
|
||||
/ __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
|
||||
____ ___ ______________ ___ __ _
|
||||
/ __ \/ | / ____/ ____/ /___ _ __ / | ____/ /___ ___ (_)___
|
||||
/ /_/ / /| |/ / __/ /_ / / __ \ | /| / / / /| |/ __ / __ `__ \/ / __ \
|
||||
/ _, _/ ___ / /_/ / __/ / / /_/ / |/ |/ / / ___ / /_/ / / / / / / / / / /
|
||||
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
|
||||
/_/ |_/_/ |_\____/_/ /_/\____/|__/|__/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
|
||||
""")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.config["SESSION_PERMANENT"] = False
|
||||
app.config["SESSION_TYPE"] = "filesystem"
|
||||
app.config["MAX_CONTENT_LENGTH"] = int(
|
||||
os.environ.get("MAX_CONTENT_LENGTH", 1024 * 1024 * 1024)
|
||||
)
|
||||
app.config["MAX_CONTENT_LENGTH"] = int(os.environ.get("MAX_CONTENT_LENGTH", 1024 * 1024 * 1024))
|
||||
Session(app)
|
||||
logging.info(f'RAGFlow admin version: {get_ragflow_version()}')
|
||||
logging.info(f"RAGFlow admin version: {get_ragflow_version()}")
|
||||
show_configs()
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
|
||||
+9
-28
@@ -70,9 +70,7 @@ def setup_auth(login_manager):
|
||||
logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars")
|
||||
return None
|
||||
|
||||
user = UserService.query(
|
||||
access_token=access_token, status=StatusEnum.VALID.value
|
||||
)
|
||||
user = UserService.query(access_token=access_token, status=StatusEnum.VALID.value)
|
||||
if user:
|
||||
if not user[0].access_token or not user[0].access_token.strip():
|
||||
logging.warning(f"User {user[0].email} has empty access_token in database")
|
||||
@@ -126,19 +124,13 @@ def add_tenant_for_admin(user_info: dict, role: str):
|
||||
"img2txt_id": settings.IMAGE2TEXT_MDL,
|
||||
"rerank_id": settings.RERANK_MDL,
|
||||
}
|
||||
usr_tenant = {
|
||||
"tenant_id": user_info["id"],
|
||||
"user_id": user_info["id"],
|
||||
"invited_by": user_info["id"],
|
||||
"role": role
|
||||
}
|
||||
usr_tenant = {"tenant_id": user_info["id"], "user_id": user_info["id"], "invited_by": user_info["id"], "role": role}
|
||||
|
||||
# tenant_llm = get_init_tenant_llm(user_info["id"])
|
||||
TenantService.insert(**tenant)
|
||||
UserTenantService.insert(**usr_tenant)
|
||||
# TenantLLMService.insert_many(tenant_llm)
|
||||
logging.info(
|
||||
f"Added tenant for email: {user_info['email']}, A default tenant has been set; changing the default models after login is strongly recommended.")
|
||||
logging.info(f"Added tenant for email: {user_info['email']}, A default tenant has been set; changing the default models after login is strongly recommended.")
|
||||
|
||||
|
||||
def check_admin_auth(func):
|
||||
@@ -212,28 +204,17 @@ def login_verify(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
auth = request.authorization
|
||||
if not auth or 'username' not in auth.parameters or 'password' not in auth.parameters:
|
||||
return jsonify({
|
||||
"code": 401,
|
||||
"message": "Authentication required",
|
||||
"data": None
|
||||
}), 200
|
||||
if not auth or "username" not in auth.parameters or "password" not in auth.parameters:
|
||||
return jsonify({"code": 401, "message": "Authentication required", "data": None}), 200
|
||||
|
||||
username = auth.parameters['username']
|
||||
password = auth.parameters['password']
|
||||
username = auth.parameters["username"]
|
||||
password = auth.parameters["password"]
|
||||
try:
|
||||
if not check_admin(username, password):
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": "Access denied",
|
||||
"data": None
|
||||
}), 200
|
||||
return jsonify({"code": 500, "message": "Access denied", "data": None}), 200
|
||||
except Exception:
|
||||
logging.exception("An error occurred during admin login verification.")
|
||||
return jsonify({
|
||||
"code": 500,
|
||||
"message": "An internal server error occurred."
|
||||
}), 200
|
||||
return jsonify({"code": 500, "message": "An internal server error occurred."}), 200
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
|
||||
+120
-125
@@ -34,8 +34,7 @@ class BaseConfig(BaseModel):
|
||||
detail_func_name: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {'id': self.id, 'name': self.name, 'host': self.host, 'port': self.port,
|
||||
'service_type': self.service_type}
|
||||
return {"id": self.id, "name": self.name, "host": self.host, "port": self.port, "service_type": self.service_type}
|
||||
|
||||
|
||||
class ServiceConfigs:
|
||||
@@ -63,11 +62,11 @@ class MetaConfig(BaseConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['meta_type'] = self.meta_type
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["meta_type"] = self.meta_type
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -77,21 +76,20 @@ class MySQLConfig(MetaConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['username'] = self.username
|
||||
extra_dict['password'] = self.password
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["username"] = self.username
|
||||
extra_dict["password"] = self.password
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
class PostgresConfig(MetaConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
return result
|
||||
|
||||
|
||||
@@ -100,11 +98,11 @@ class RetrievalConfig(BaseConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['retrieval_type'] = self.retrieval_type
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["retrieval_type"] = self.retrieval_type
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -113,11 +111,11 @@ class InfinityConfig(RetrievalConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['db_name'] = self.db_name
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["db_name"] = self.db_name
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -127,12 +125,12 @@ class ElasticsearchConfig(RetrievalConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['username'] = self.username
|
||||
extra_dict['password'] = self.password
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["username"] = self.username
|
||||
extra_dict["password"] = self.password
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -141,11 +139,11 @@ class MessageQueueConfig(BaseConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['mq_type'] = self.mq_type
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["mq_type"] = self.mq_type
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -155,30 +153,28 @@ class RedisConfig(MessageQueueConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['database'] = self.database
|
||||
extra_dict['password'] = self.password
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["database"] = self.database
|
||||
extra_dict["password"] = self.password
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
class RabbitMQConfig(MessageQueueConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
return result
|
||||
|
||||
|
||||
class RAGFlowServerConfig(BaseConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
return result
|
||||
|
||||
|
||||
@@ -187,9 +183,9 @@ class TaskExecutorConfig(BaseConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
result['extra']['message_queue_type'] = self.message_queue_type
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
result["extra"]["message_queue_type"] = self.message_queue_type
|
||||
return result
|
||||
|
||||
|
||||
@@ -198,11 +194,11 @@ class FileStoreConfig(BaseConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['store_type'] = self.store_type
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["store_type"] = self.store_type
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -212,12 +208,12 @@ class MinioConfig(FileStoreConfig):
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result = super().to_dict()
|
||||
if 'extra' not in result:
|
||||
result['extra'] = dict()
|
||||
extra_dict = result['extra'].copy()
|
||||
extra_dict['user'] = self.user
|
||||
extra_dict['password'] = self.password
|
||||
result['extra'] = extra_dict
|
||||
if "extra" not in result:
|
||||
result["extra"] = dict()
|
||||
extra_dict = result["extra"].copy()
|
||||
extra_dict["user"] = self.user
|
||||
extra_dict["password"] = self.password
|
||||
result["extra"] = extra_dict
|
||||
return result
|
||||
|
||||
|
||||
@@ -229,106 +225,105 @@ def load_configurations(config_path: str) -> list[BaseConfig]:
|
||||
for k, v in raw_configs.items():
|
||||
match k:
|
||||
case "ragflow":
|
||||
name: str = f'ragflow_{ragflow_count}'
|
||||
host: str = v['host']
|
||||
http_port: int = v['http_port']
|
||||
config = RAGFlowServerConfig(id=id_count, name=name, host=host, port=http_port,
|
||||
service_type="ragflow_server",
|
||||
detail_func_name="check_ragflow_server_alive")
|
||||
name: str = f"ragflow_{ragflow_count}"
|
||||
host: str = v["host"]
|
||||
http_port: int = v["http_port"]
|
||||
config = RAGFlowServerConfig(id=id_count, name=name, host=host, port=http_port, service_type="ragflow_server", detail_func_name="check_ragflow_server_alive")
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "es":
|
||||
name: str = 'elasticsearch'
|
||||
url = v['hosts']
|
||||
name: str = "elasticsearch"
|
||||
url = v["hosts"]
|
||||
parsed = urlparse(url)
|
||||
host: str = parsed.hostname
|
||||
port: int = parsed.port
|
||||
username: str = v.get('username')
|
||||
password: str = v.get('password')
|
||||
config = ElasticsearchConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval",
|
||||
retrieval_type="elasticsearch",
|
||||
username=username, password=password,
|
||||
detail_func_name="get_es_cluster_stats")
|
||||
username: str = v.get("username")
|
||||
password: str = v.get("password")
|
||||
config = ElasticsearchConfig(
|
||||
id=id_count,
|
||||
name=name,
|
||||
host=host,
|
||||
port=port,
|
||||
service_type="retrieval",
|
||||
retrieval_type="elasticsearch",
|
||||
username=username,
|
||||
password=password,
|
||||
detail_func_name="get_es_cluster_stats",
|
||||
)
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
|
||||
case "infinity":
|
||||
name: str = 'infinity'
|
||||
url = v['uri']
|
||||
parts = url.split(':', 1)
|
||||
name: str = "infinity"
|
||||
url = v["uri"]
|
||||
parts = url.split(":", 1)
|
||||
host = parts[0]
|
||||
port = int(parts[1])
|
||||
database: str = v.get('db_name', 'default_db')
|
||||
config = InfinityConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval",
|
||||
retrieval_type="infinity",
|
||||
db_name=database, detail_func_name="get_infinity_status")
|
||||
database: str = v.get("db_name", "default_db")
|
||||
config = InfinityConfig(id=id_count, name=name, host=host, port=port, service_type="retrieval", retrieval_type="infinity", db_name=database, detail_func_name="get_infinity_status")
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "minio_0":
|
||||
name: str = 'minio_0'
|
||||
url = v['host']
|
||||
parts = url.split(':', 1)
|
||||
name: str = "minio_0"
|
||||
url = v["host"]
|
||||
parts = url.split(":", 1)
|
||||
host = parts[0]
|
||||
port = int(parts[1])
|
||||
user = v.get('user')
|
||||
password = v.get('password')
|
||||
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password,
|
||||
service_type="file_store",
|
||||
store_type="minio", detail_func_name="check_minio_alive")
|
||||
user = v.get("user")
|
||||
password = v.get("password")
|
||||
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password, service_type="file_store", store_type="minio", detail_func_name="check_minio_alive")
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "minio":
|
||||
name: str = 'minio'
|
||||
url = v['host']
|
||||
parts = url.split(':', 1)
|
||||
name: str = "minio"
|
||||
url = v["host"]
|
||||
parts = url.split(":", 1)
|
||||
host = parts[0]
|
||||
port = int(parts[1])
|
||||
user = v.get('user')
|
||||
password = v.get('password')
|
||||
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password,
|
||||
service_type="file_store",
|
||||
store_type="minio", detail_func_name="check_minio_alive")
|
||||
user = v.get("user")
|
||||
password = v.get("password")
|
||||
config = MinioConfig(id=id_count, name=name, host=host, port=port, user=user, password=password, service_type="file_store", store_type="minio", detail_func_name="check_minio_alive")
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "redis":
|
||||
name: str = 'redis'
|
||||
url = v['host']
|
||||
parts = url.split(':', 1)
|
||||
name: str = "redis"
|
||||
url = v["host"]
|
||||
parts = url.split(":", 1)
|
||||
host = parts[0]
|
||||
port = int(parts[1])
|
||||
password = v.get('password')
|
||||
db: int = v.get('db')
|
||||
config = RedisConfig(id=id_count, name=name, host=host, port=port, password=password, database=db,
|
||||
service_type="message_queue", mq_type="redis", detail_func_name="get_redis_info")
|
||||
password = v.get("password")
|
||||
db: int = v.get("db")
|
||||
config = RedisConfig(id=id_count, name=name, host=host, port=port, password=password, database=db, service_type="message_queue", mq_type="redis", detail_func_name="get_redis_info")
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "mysql":
|
||||
name: str = 'mysql'
|
||||
host: str = v.get('host')
|
||||
port: int = v.get('port')
|
||||
username = v.get('user')
|
||||
password = v.get('password')
|
||||
config = MySQLConfig(id=id_count, name=name, host=host, port=port, username=username, password=password,
|
||||
service_type="meta_data", meta_type="mysql", detail_func_name="get_mysql_status")
|
||||
name: str = "mysql"
|
||||
host: str = v.get("host")
|
||||
port: int = v.get("port")
|
||||
username = v.get("user")
|
||||
password = v.get("password")
|
||||
config = MySQLConfig(
|
||||
id=id_count, name=name, host=host, port=port, username=username, password=password, service_type="meta_data", meta_type="mysql", detail_func_name="get_mysql_status"
|
||||
)
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "admin":
|
||||
pass
|
||||
case "task_executor":
|
||||
name: str = 'task_executor'
|
||||
host: str = v.get('host', '')
|
||||
port: int = v.get('port', 0)
|
||||
message_queue_type: str = v.get('message_queue_type')
|
||||
config = TaskExecutorConfig(id=id_count, name=name, host=host, port=port, message_queue_type=message_queue_type,
|
||||
service_type="task_executor", detail_func_name="check_task_executor_alive")
|
||||
name: str = "task_executor"
|
||||
host: str = v.get("host", "")
|
||||
port: int = v.get("port", 0)
|
||||
message_queue_type: str = v.get("message_queue_type")
|
||||
config = TaskExecutorConfig(
|
||||
id=id_count, name=name, host=host, port=port, message_queue_type=message_queue_type, service_type="task_executor", detail_func_name="check_task_executor_alive"
|
||||
)
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case "rabbitmq":
|
||||
name: str = 'rabbitmq'
|
||||
host: str = v.get('host')
|
||||
port: int = v.get('port')
|
||||
config = RabbitMQConfig(id=id_count, name=name, host=host, port=port,
|
||||
service_type="message_queue", mq_type="rabbitmq", detail_func_name="check_rabbitmq_alive")
|
||||
name: str = "rabbitmq"
|
||||
host: str = v.get("host")
|
||||
port: int = v.get("port")
|
||||
config = RabbitMQConfig(id=id_count, name=name, host=host, port=port, service_type="message_queue", mq_type="rabbitmq", detail_func_name="check_rabbitmq_alive")
|
||||
configurations.append(config)
|
||||
id_count += 1
|
||||
case _:
|
||||
|
||||
@@ -4,14 +4,17 @@ class AdminException(Exception):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
class UserNotFoundError(AdminException):
|
||||
def __init__(self, username):
|
||||
super().__init__(f"User '{username}' not found", 404)
|
||||
|
||||
|
||||
class UserAlreadyExistsError(AdminException):
|
||||
def __init__(self, username):
|
||||
super().__init__(f"User '{username}' already exists", 409)
|
||||
|
||||
|
||||
class CannotDeleteAdminError(AdminException):
|
||||
def __init__(self):
|
||||
super().__init__("Cannot delete admin account", 403)
|
||||
super().__init__("Cannot delete admin account", 403)
|
||||
|
||||
@@ -17,16 +17,8 @@ from flask import jsonify
|
||||
|
||||
|
||||
def success_response(data=None, message="Success", code=0):
|
||||
return jsonify({
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data
|
||||
}), 200
|
||||
return jsonify({"code": code, "message": message, "data": data}), 200
|
||||
|
||||
|
||||
def error_response(message="Error", code=-1, data=None):
|
||||
return jsonify({
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data
|
||||
}), 400
|
||||
return jsonify({"code": code, "message": message, "data": data}), 400
|
||||
|
||||
@@ -489,10 +489,7 @@ class SandboxMgr:
|
||||
"""List all available sandbox providers."""
|
||||
result = []
|
||||
for provider_id, metadata in SandboxMgr.PROVIDER_REGISTRY.items():
|
||||
result.append({
|
||||
"id": provider_id,
|
||||
**metadata
|
||||
})
|
||||
result.append({"id": provider_id, **metadata})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@@ -635,6 +632,7 @@ class SandboxMgr:
|
||||
config_json = json.dumps(config)
|
||||
SettingsMgr.update_by_name(f"sandbox.{provider_type}", config_json)
|
||||
from agent.sandbox.client import reload_provider
|
||||
|
||||
reload_provider()
|
||||
|
||||
return {"provider_type": provider_type, "config": config}
|
||||
@@ -727,11 +725,7 @@ def main() -> dict:
|
||||
# Build detailed result message
|
||||
success = execution_result.exit_code == 0 and "TEST_PASSED" in execution_result.stdout
|
||||
|
||||
message_parts = [
|
||||
f"Test {success and 'PASSED' or 'FAILED'}",
|
||||
f"Exit code: {execution_result.exit_code}",
|
||||
f"Execution time: {execution_result.execution_time:.2f}s"
|
||||
]
|
||||
message_parts = [f"Test {success and 'PASSED' or 'FAILED'}", f"Exit code: {execution_result.exit_code}", f"Execution time: {execution_result.execution_time:.2f}s"]
|
||||
|
||||
if execution_result.stdout.strip():
|
||||
stdout_preview = execution_result.stdout.strip()[:200]
|
||||
@@ -751,12 +745,13 @@ def main() -> dict:
|
||||
"execution_time": execution_result.execution_time,
|
||||
"stdout": execution_result.stdout,
|
||||
"stderr": execution_result.stderr,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
except AdminException:
|
||||
raise
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
error_details = traceback.format_exc()
|
||||
raise AdminException(f"Connection test failed: {str(e)}\\n\\nStack trace:\\n{error_details}")
|
||||
|
||||
@@ -161,6 +161,7 @@ class Graph:
|
||||
|
||||
def close(self):
|
||||
from common.mcp_tool_call_conn import MCPToolCallSession
|
||||
|
||||
seen = set()
|
||||
for cpn in self.components.values():
|
||||
obj = cpn.get("obj")
|
||||
|
||||
@@ -22,8 +22,9 @@ from typing import Dict, Type
|
||||
_package_path = os.path.dirname(__file__)
|
||||
__all_classes: Dict[str, Type] = {}
|
||||
|
||||
|
||||
def _import_submodules() -> None:
|
||||
for filename in os.listdir(_package_path): # noqa: F821
|
||||
for filename in os.listdir(_package_path): # noqa: F821
|
||||
if filename.startswith("__") or not filename.endswith(".py") or filename.startswith("base"):
|
||||
continue
|
||||
module_name = filename[:-3]
|
||||
@@ -34,13 +35,14 @@ def _import_submodules() -> None:
|
||||
except ImportError as e:
|
||||
print(f"Warning: Failed to import module {module_name}: {str(e)}")
|
||||
|
||||
|
||||
def _extract_classes_from_module(module: ModuleType) -> None:
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (inspect.isclass(obj) and
|
||||
obj.__module__ == module.__name__ and not name.startswith("_")):
|
||||
if inspect.isclass(obj) and obj.__module__ == module.__name__ and not name.startswith("_"):
|
||||
__all_classes[name] = obj
|
||||
globals()[name] = obj
|
||||
|
||||
|
||||
_import_submodules()
|
||||
|
||||
__all__ = list(__all_classes.keys()) + ["__all_classes"]
|
||||
|
||||
+23
-52
@@ -28,7 +28,6 @@ from agent import settings
|
||||
from common.connection_utils import timeout
|
||||
|
||||
|
||||
|
||||
from common.misc_utils import thread_pool_exec
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -101,6 +100,7 @@ class ComponentParamBase(ABC):
|
||||
return None
|
||||
logging.warning("ComponentParamBase.__str__: JSON fallback via str() for type=%s", type(obj).__name__)
|
||||
return str(obj)
|
||||
|
||||
return json.dumps(self.as_dict(), ensure_ascii=False, default=_serialize_default)
|
||||
|
||||
def as_dict(self):
|
||||
@@ -135,15 +135,11 @@ class ComponentParamBase(ABC):
|
||||
update_from_raw_conf = conf.get(_IS_RAW_CONF, True)
|
||||
if update_from_raw_conf:
|
||||
deprecated_params_set = self._get_or_init_deprecated_params_set()
|
||||
feeded_deprecated_params_set = (
|
||||
self._get_or_init_feeded_deprecated_params_set()
|
||||
)
|
||||
feeded_deprecated_params_set = self._get_or_init_feeded_deprecated_params_set()
|
||||
user_feeded_params_set = self._get_or_init_user_feeded_params_set()
|
||||
setattr(self, _IS_RAW_CONF, False)
|
||||
else:
|
||||
feeded_deprecated_params_set = (
|
||||
self._get_or_init_feeded_deprecated_params_set(conf)
|
||||
)
|
||||
feeded_deprecated_params_set = self._get_or_init_feeded_deprecated_params_set(conf)
|
||||
user_feeded_params_set = self._get_or_init_user_feeded_params_set(conf)
|
||||
|
||||
def _recursive_update_param(param, config, depth, prefix):
|
||||
@@ -179,15 +175,11 @@ class ComponentParamBase(ABC):
|
||||
|
||||
else:
|
||||
# recursive set obj attr
|
||||
sub_params = _recursive_update_param(
|
||||
attr, config_value, depth + 1, prefix=f"{prefix}{config_key}."
|
||||
)
|
||||
sub_params = _recursive_update_param(attr, config_value, depth + 1, prefix=f"{prefix}{config_key}.")
|
||||
setattr(param, config_key, sub_params)
|
||||
|
||||
if not allow_redundant and redundant_attrs:
|
||||
raise ValueError(
|
||||
f"cpn `{getattr(self, '_name', type(self))}` has redundant parameters: `{[redundant_attrs]}`"
|
||||
)
|
||||
raise ValueError(f"cpn `{getattr(self, '_name', type(self))}` has redundant parameters: `{[redundant_attrs]}`")
|
||||
|
||||
return param
|
||||
|
||||
@@ -218,9 +210,7 @@ class ComponentParamBase(ABC):
|
||||
param_validation_path_prefix = home_dir + "/param_validation/"
|
||||
|
||||
param_name = type(self).__name__
|
||||
param_validation_path = "/".join(
|
||||
[param_validation_path_prefix, param_name + ".json"]
|
||||
)
|
||||
param_validation_path = "/".join([param_validation_path_prefix, param_name + ".json"])
|
||||
|
||||
validation_json = None
|
||||
|
||||
@@ -253,11 +243,7 @@ class ComponentParamBase(ABC):
|
||||
break
|
||||
|
||||
if not value_legal:
|
||||
raise ValueError(
|
||||
"Please check runtime conf, {} = {} does not match user-parameter restriction".format(
|
||||
variable, value
|
||||
)
|
||||
)
|
||||
raise ValueError("Please check runtime conf, {} = {} does not match user-parameter restriction".format(variable, value))
|
||||
|
||||
elif variable in validation_json:
|
||||
self._validate_param(attr, validation_json)
|
||||
@@ -335,11 +321,7 @@ class ComponentParamBase(ABC):
|
||||
def _range(value, ranges):
|
||||
in_range = False
|
||||
for left_limit, right_limit in ranges:
|
||||
if (
|
||||
left_limit - settings.FLOAT_ZERO
|
||||
<= value
|
||||
<= right_limit + settings.FLOAT_ZERO
|
||||
):
|
||||
if left_limit - settings.FLOAT_ZERO <= value <= right_limit + settings.FLOAT_ZERO:
|
||||
in_range = True
|
||||
break
|
||||
|
||||
@@ -355,16 +337,11 @@ class ComponentParamBase(ABC):
|
||||
|
||||
def _warn_deprecated_param(self, param_name, description):
|
||||
if self._deprecated_params_set.get(param_name):
|
||||
logging.warning(
|
||||
f"{description} {param_name} is deprecated and ignored in this version."
|
||||
)
|
||||
logging.warning(f"{description} {param_name} is deprecated and ignored in this version.")
|
||||
|
||||
def _warn_to_deprecate_param(self, param_name, description, new_param):
|
||||
if self._deprecated_params_set.get(param_name):
|
||||
logging.warning(
|
||||
f"{description} {param_name} will be deprecated in future release; "
|
||||
f"please use {new_param} instead."
|
||||
)
|
||||
logging.warning(f"{description} {param_name} will be deprecated in future release; please use {new_param} instead.")
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -385,9 +362,7 @@ class ComponentBase(ABC):
|
||||
return """{{
|
||||
"component_name": "{}",
|
||||
"params": {}
|
||||
}}""".format(self.component_name,
|
||||
self._param
|
||||
)
|
||||
}}""".format(self.component_name, self._param)
|
||||
|
||||
def __init__(self, canvas, id, param: ComponentParamBase):
|
||||
from agent.canvas import Graph # Local import to avoid cyclic dependency
|
||||
@@ -403,7 +378,7 @@ class ComponentBase(ABC):
|
||||
|
||||
def check_if_canceled(self, message: str = "") -> bool:
|
||||
if self.is_canceled():
|
||||
task_id = getattr(self._canvas, 'task_id', 'unknown')
|
||||
task_id = getattr(self._canvas, "task_id", "unknown")
|
||||
log_message = f"Task {task_id} has been canceled"
|
||||
if message:
|
||||
log_message += f" during {message}"
|
||||
@@ -491,7 +466,9 @@ class ComponentBase(ABC):
|
||||
input_elements = self.get_input_elements()
|
||||
_logger.debug(
|
||||
"[Base] Component '%s' (%s) resolving inputs. Input element keys: %s",
|
||||
self._id, self.component_name, list(input_elements.keys()),
|
||||
self._id,
|
||||
self.component_name,
|
||||
list(input_elements.keys()),
|
||||
)
|
||||
for var, o in input_elements.items():
|
||||
v = self.get_param(var)
|
||||
@@ -504,7 +481,7 @@ class ComponentBase(ABC):
|
||||
_logger.debug("[Base] var '%s': resolved ref '%s' -> %s", var, v, json.dumps(resolved, ensure_ascii=False, default=str)[:200])
|
||||
elif isinstance(v, str) and re.search(self.variable_ref_patt, v):
|
||||
elements = self.get_input_elements_from_text(v)
|
||||
kv = {k: e.get('value', '') for k, e in elements.items()}
|
||||
kv = {k: e.get("value", "") for k, e in elements.items()}
|
||||
self.set_input_value(var, self.string_format(v, kv))
|
||||
_logger.debug("[Base] var '%s': resolved text refs '%s' -> %s", var, v, json.dumps(kv, ensure_ascii=False, default=str)[:200])
|
||||
else:
|
||||
@@ -545,7 +522,7 @@ class ComponentBase(ABC):
|
||||
"name": (self._canvas.get_component_name(cpn_id) + f"@{var_nm}") if cpn_id else exp,
|
||||
"value": self._canvas.get_variable_value(exp),
|
||||
"_retrieval": self._canvas.get_variable_value(f"{cpn_id}@_references") if cpn_id else None,
|
||||
"_cpn_id": cpn_id
|
||||
"_cpn_id": cpn_id,
|
||||
}
|
||||
for r in re.finditer(self.iteration_alias_patt, txt, flags=re.IGNORECASE | re.DOTALL):
|
||||
exp = r.group(1)
|
||||
@@ -559,7 +536,7 @@ class ComponentBase(ABC):
|
||||
"name": (self._canvas.get_component_name(cpn_id) + f"@{var_nm}"),
|
||||
"value": self._canvas.get_variable_value(ref),
|
||||
"_retrieval": self._canvas.get_variable_value(f"{cpn_id}@_references"),
|
||||
"_cpn_id": cpn_id
|
||||
"_cpn_id": cpn_id,
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -601,33 +578,27 @@ class ComponentBase(ABC):
|
||||
return self._canvas.get_component(pid)["obj"]
|
||||
|
||||
def get_upstream(self) -> List[str]:
|
||||
cpn_nms = self._canvas.get_component(self._id)['upstream']
|
||||
cpn_nms = self._canvas.get_component(self._id)["upstream"]
|
||||
return cpn_nms
|
||||
|
||||
def get_downstream(self) -> List[str]:
|
||||
cpn_nms = self._canvas.get_component(self._id)['downstream']
|
||||
cpn_nms = self._canvas.get_component(self._id)["downstream"]
|
||||
return cpn_nms
|
||||
|
||||
@staticmethod
|
||||
def string_format(content: str, kv: dict[str, str]) -> str:
|
||||
for n, v in kv.items():
|
||||
|
||||
def repl(_match, val=v):
|
||||
return str(val) if val is not None else ""
|
||||
|
||||
content = re.sub(
|
||||
r"\{%s\}" % re.escape(n),
|
||||
repl,
|
||||
content
|
||||
)
|
||||
content = re.sub(r"\{%s\}" % re.escape(n), repl, content)
|
||||
return content
|
||||
|
||||
def exception_handler(self):
|
||||
if not self._param.exception_method:
|
||||
return None
|
||||
return {
|
||||
"goto": self._param.exception_goto,
|
||||
"default_value": self._param.exception_default_value
|
||||
}
|
||||
return {"goto": self._param.exception_goto, "default_value": self._param.exception_default_value}
|
||||
|
||||
def get_exception_default_value(self):
|
||||
if self._param.exception_method != "comment":
|
||||
|
||||
@@ -17,17 +17,17 @@ from agent.component.fillup import UserFillUpParam, UserFillUp
|
||||
|
||||
|
||||
class BeginParam(UserFillUpParam):
|
||||
|
||||
"""
|
||||
Define the Begin component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mode = "conversational"
|
||||
self.prologue = "Hi! I'm your smart assistant. What can I do for you?"
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.mode, "The 'mode' should be either `conversational` or `task`", ["conversational", "task","Webhook"])
|
||||
self.check_valid_value(self.mode, "The 'mode' should be either `conversational` or `task`", ["conversational", "task", "Webhook"])
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return getattr(self, "inputs")
|
||||
|
||||
@@ -371,9 +371,7 @@ class Browser(ComponentBase, ABC):
|
||||
sort_keys=True,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
raw_canvas_id = (
|
||||
f"dsl_{hashlib.sha1(graph_text.encode('utf-8')).hexdigest()[:12]}"
|
||||
)
|
||||
raw_canvas_id = f"dsl_{hashlib.sha1(graph_text.encode('utf-8')).hexdigest()[:12]}"
|
||||
canvas_id = self._safe_path_segment(raw_canvas_id)
|
||||
node_id = self._safe_path_segment(self._id)
|
||||
return os.path.join(root, tenant, canvas_id, node_id)
|
||||
@@ -488,10 +486,7 @@ class Browser(ComponentBase, ABC):
|
||||
# Keep browser-use watchdog fallback in sync with our resolved path.
|
||||
os.environ["BROWSER_USE_BROWSER_BINARY_PATH"] = executable_path
|
||||
else:
|
||||
logging.warning(
|
||||
"Browser no local browser executable found. "
|
||||
"Set BROWSER_USE_EXECUTABLE_PATH or preinstall chromium in image to avoid runtime playwright install."
|
||||
)
|
||||
logging.warning("Browser no local browser executable found. Set BROWSER_USE_EXECUTABLE_PATH or preinstall chromium in image to avoid runtime playwright install.")
|
||||
if profile_dir:
|
||||
browser_kwargs["user_data_dir"] = profile_dir
|
||||
# browser-use expects profile_directory to be a profile name
|
||||
@@ -682,21 +677,13 @@ class Browser(ComponentBase, ABC):
|
||||
try:
|
||||
self._prepare_input_values()
|
||||
user_prompt = self._resolve_text(kwargs.get("prompts", self._param.prompts))
|
||||
with tempfile.TemporaryDirectory(prefix="browser_use_upload_") as upload_dir, tempfile.TemporaryDirectory(
|
||||
prefix="browser_use_download_"
|
||||
) as download_dir:
|
||||
with tempfile.TemporaryDirectory(prefix="browser_use_upload_") as upload_dir, tempfile.TemporaryDirectory(prefix="browser_use_download_") as download_dir:
|
||||
uploaded_files = self._prepare_upload_files(upload_dir)
|
||||
|
||||
upload_lines = [
|
||||
f"- file_id={item['file_id']}, name={item['name']}, local_path={item['local_path']}"
|
||||
for item in uploaded_files
|
||||
]
|
||||
upload_lines = [f"- file_id={item['file_id']}, name={item['name']}, local_path={item['local_path']}" for item in uploaded_files]
|
||||
task_text = user_prompt
|
||||
if upload_lines:
|
||||
task_text += (
|
||||
"\n\nYou can upload files from these local paths when operating web pages:\n"
|
||||
+ "\n".join(upload_lines)
|
||||
)
|
||||
task_text += "\n\nYou can upload files from these local paths when operating web pages:\n" + "\n".join(upload_lines)
|
||||
|
||||
upload_local_paths = [item.get("local_path", "") for item in uploaded_files if item.get("local_path")]
|
||||
if persist_session:
|
||||
@@ -707,11 +694,7 @@ class Browser(ComponentBase, ABC):
|
||||
profile_dir = tempfile.mkdtemp(prefix="browser_use_profile_")
|
||||
except OSError:
|
||||
profile_dir = None
|
||||
history = asyncio.run(
|
||||
self._run_browser_use_async(
|
||||
task_text, download_dir, upload_local_paths, profile_dir
|
||||
)
|
||||
)
|
||||
history = asyncio.run(self._run_browser_use_async(task_text, download_dir, upload_local_paths, profile_dir))
|
||||
target_dir_id = FileService.get_root_folder(self._canvas.get_tenant_id())["id"]
|
||||
downloaded_files = self._save_downloads(download_dir, target_dir_id)
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ from rag.llm.chat_model import ERROR_PREFIX
|
||||
|
||||
|
||||
class CategorizeParam(LLMParam):
|
||||
|
||||
"""
|
||||
Define the categorize component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.category_description = {}
|
||||
@@ -50,12 +50,7 @@ class CategorizeParam(LLMParam):
|
||||
raise ValueError(f"[Categorize] 'To' of category {k} can not be empty!")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"type": "line",
|
||||
"name": "Query"
|
||||
}
|
||||
}
|
||||
return {"query": {"type": "line", "name": "Query"}}
|
||||
|
||||
def update_prompt(self):
|
||||
cate_lines = []
|
||||
@@ -63,13 +58,12 @@ class CategorizeParam(LLMParam):
|
||||
for line in desc.get("examples", []):
|
||||
if not line:
|
||||
continue
|
||||
cate_lines.append("USER: \"" + re.sub(r"\n", " ", line, flags=re.DOTALL) + "\" → "+c)
|
||||
cate_lines.append('USER: "' + re.sub(r"\n", " ", line, flags=re.DOTALL) + '" → ' + c)
|
||||
|
||||
descriptions = []
|
||||
for c, desc in self.category_description.items():
|
||||
if desc.get("description"):
|
||||
descriptions.append(
|
||||
"\n------\nCategory: {}\nDescription: {}".format(c, desc["description"]))
|
||||
descriptions.append("\n------\nCategory: {}\nDescription: {}".format(c, desc["description"]))
|
||||
|
||||
self.sys_prompt = """
|
||||
You are an advanced classification system that categorizes user questions into specific types. Analyze the input question and classify it into ONE of the following categories:
|
||||
@@ -84,10 +78,7 @@ Here's description of each category:
|
||||
- Return only the category name without explanations
|
||||
- Use "Other" only when no other category fits
|
||||
|
||||
""".format(
|
||||
"\n - ".join(list(self.category_description.keys())),
|
||||
"\n".join(descriptions)
|
||||
)
|
||||
""".format("\n - ".join(list(self.category_description.keys())), "\n".join(descriptions))
|
||||
|
||||
if cate_lines:
|
||||
self.sys_prompt += """
|
||||
@@ -106,7 +97,7 @@ class Categorize(LLM, ABC):
|
||||
logging.warning(f"[Categorize] input element not detected for query key: {query_key}")
|
||||
return elements
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
async def _invoke_async(self, **kwargs):
|
||||
if self.check_if_canceled("Categorize processing"):
|
||||
return
|
||||
@@ -130,7 +121,7 @@ class Categorize(LLM, ABC):
|
||||
user_prompt = """
|
||||
---- Real Data ----
|
||||
{} →
|
||||
""".format(" | ".join(["{}: \"{}\"".format(c["role"].upper(), re.sub(r"\n", "", c["content"], flags=re.DOTALL)) for c in msg]))
|
||||
""".format(" | ".join(['{}: "{}"'.format(c["role"].upper(), re.sub(r"\n", "", c["content"], flags=re.DOTALL)) for c in msg]))
|
||||
|
||||
if self.check_if_canceled("Categorize processing"):
|
||||
return
|
||||
@@ -158,7 +149,7 @@ class Categorize(LLM, ABC):
|
||||
self.set_output("category_name", max_category)
|
||||
self.set_output("_next", cpn_ids)
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
return asyncio.run(self._invoke_async(**kwargs))
|
||||
|
||||
|
||||
@@ -19,55 +19,47 @@ import os
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
from api.utils.api_utils import timeout
|
||||
|
||||
|
||||
class DataOperationsParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Data Operations component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.query = []
|
||||
self.operations = "literal_eval"
|
||||
self.select_keys = []
|
||||
self.filter_values=[]
|
||||
self.updates=[]
|
||||
self.remove_keys=[]
|
||||
self.rename_keys=[]
|
||||
self.outputs = {
|
||||
"result": {
|
||||
"value": [],
|
||||
"type": "Array of Object"
|
||||
}
|
||||
}
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.operations, "Support operations", ["select_keys", "literal_eval","combine","filter_values","append_or_update","remove_keys","rename_keys"])
|
||||
|
||||
|
||||
self.filter_values = []
|
||||
self.updates = []
|
||||
self.remove_keys = []
|
||||
self.rename_keys = []
|
||||
self.outputs = {"result": {"value": [], "type": "Array of Object"}}
|
||||
|
||||
class DataOperations(ComponentBase,ABC):
|
||||
def check(self):
|
||||
self.check_valid_value(self.operations, "Support operations", ["select_keys", "literal_eval", "combine", "filter_values", "append_or_update", "remove_keys", "rename_keys"])
|
||||
|
||||
|
||||
class DataOperations(ComponentBase, ABC):
|
||||
component_name = "DataOperations"
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
k: {"name": o.get("name", ""), "type": "line"}
|
||||
for input_item in (self._param.query or [])
|
||||
for k, o in self.get_input_elements_from_text(input_item).items()
|
||||
}
|
||||
return {k: {"name": o.get("name", ""), "type": "line"} for input_item in (self._param.query or []) for k, o in self.get_input_elements_from_text(input_item).items()}
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
self.input_objects=[]
|
||||
self.input_objects = []
|
||||
inputs = getattr(self._param, "query", None)
|
||||
if not isinstance(inputs, (list, tuple)):
|
||||
inputs = [inputs]
|
||||
for input_ref in inputs:
|
||||
input_object=self._canvas.get_variable_value(input_ref)
|
||||
input_object = self._canvas.get_variable_value(input_ref)
|
||||
self.set_input_value(input_ref, input_object)
|
||||
if input_object is None:
|
||||
continue
|
||||
if isinstance(input_object,dict):
|
||||
if isinstance(input_object, dict):
|
||||
self.input_objects.append(input_object)
|
||||
elif isinstance(input_object,list):
|
||||
elif isinstance(input_object, list):
|
||||
self.input_objects.extend(x for x in input_object if isinstance(x, dict))
|
||||
else:
|
||||
continue
|
||||
@@ -85,13 +77,12 @@ class DataOperations(ComponentBase,ABC):
|
||||
self._remove_keys()
|
||||
else:
|
||||
self._rename_keys()
|
||||
|
||||
|
||||
def _select_keys(self):
|
||||
filter_criteria: list[str] = self._param.select_keys
|
||||
results = [{key: value for key, value in data_dict.items() if key in filter_criteria} for data_dict in self.input_objects]
|
||||
self.set_output("result", results)
|
||||
|
||||
|
||||
def _recursive_eval(self, data):
|
||||
if isinstance(data, dict):
|
||||
return {k: self._recursive_eval(v) for k, v in data.items()}
|
||||
@@ -99,23 +90,19 @@ class DataOperations(ComponentBase,ABC):
|
||||
return [self._recursive_eval(item) for item in data]
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
if (
|
||||
data.strip().startswith(("{", "[", "(", "'", '"'))
|
||||
or data.strip().lower() in ("true", "false", "none")
|
||||
or data.strip().replace(".", "").isdigit()
|
||||
):
|
||||
if data.strip().startswith(("{", "[", "(", "'", '"')) or data.strip().lower() in ("true", "false", "none") or data.strip().replace(".", "").isdigit():
|
||||
return ast.literal_eval(data)
|
||||
except (ValueError, SyntaxError, TypeError, MemoryError):
|
||||
return data
|
||||
else:
|
||||
return data
|
||||
return data
|
||||
|
||||
|
||||
def _literal_eval(self):
|
||||
self.set_output("result", self._recursive_eval(self.input_objects))
|
||||
|
||||
def _combine(self):
|
||||
result={}
|
||||
result = {}
|
||||
for obj in self.input_objects:
|
||||
for key, value in obj.items():
|
||||
if key not in result:
|
||||
@@ -126,15 +113,13 @@ class DataOperations(ComponentBase,ABC):
|
||||
else:
|
||||
result[key].append(value)
|
||||
else:
|
||||
result[key] = (
|
||||
[result[key], value] if not isinstance(value, list) else [result[key], *value]
|
||||
)
|
||||
result[key] = [result[key], value] if not isinstance(value, list) else [result[key], *value]
|
||||
self.set_output("result", result)
|
||||
|
||||
def norm(self,v):
|
||||
|
||||
def norm(self, v):
|
||||
s = "" if v is None else str(v)
|
||||
return s
|
||||
|
||||
|
||||
def match_rule(self, obj, rule):
|
||||
key = rule.get("key")
|
||||
op = (rule.get("operator") or "equals").lower()
|
||||
@@ -155,10 +140,10 @@ class DataOperations(ComponentBase,ABC):
|
||||
if op == "end with":
|
||||
return v.endswith(target)
|
||||
return False
|
||||
|
||||
|
||||
def _filter_values(self):
|
||||
results=[]
|
||||
rules = (getattr(self._param, "filter_values", None) or [])
|
||||
results = []
|
||||
rules = getattr(self._param, "filter_values", None) or []
|
||||
for obj in self.input_objects:
|
||||
if not rules:
|
||||
results.append(obj)
|
||||
@@ -166,11 +151,10 @@ class DataOperations(ComponentBase,ABC):
|
||||
if all(self.match_rule(obj, r) for r in rules):
|
||||
results.append(obj)
|
||||
self.set_output("result", results)
|
||||
|
||||
|
||||
|
||||
def _append_or_update(self):
|
||||
results=[]
|
||||
updates = getattr(self._param, "updates", []) or []
|
||||
results = []
|
||||
updates = getattr(self._param, "updates", []) or []
|
||||
for obj in self.input_objects:
|
||||
new_obj = dict(obj)
|
||||
for item in updates:
|
||||
@@ -187,7 +171,7 @@ class DataOperations(ComponentBase,ABC):
|
||||
results = []
|
||||
remove_keys = getattr(self._param, "remove_keys", []) or []
|
||||
|
||||
for obj in (self.input_objects or []):
|
||||
for obj in self.input_objects or []:
|
||||
new_obj = dict(obj)
|
||||
for k in remove_keys:
|
||||
if not isinstance(k, str):
|
||||
@@ -200,7 +184,7 @@ class DataOperations(ComponentBase,ABC):
|
||||
results = []
|
||||
rename_pairs = getattr(self._param, "rename_keys", []) or []
|
||||
|
||||
for obj in (self.input_objects or []):
|
||||
for obj in self.input_objects or []:
|
||||
new_obj = dict(obj)
|
||||
for pair in rename_pairs:
|
||||
if not isinstance(pair, dict):
|
||||
|
||||
@@ -163,6 +163,7 @@ class DocGenerator(Message, ABC):
|
||||
logging.info("Starting document generation, content length: %s chars", len(content))
|
||||
|
||||
if content:
|
||||
|
||||
def _replace_variable(match_obj: re.Match[str]) -> str:
|
||||
match = match_obj.group(1)
|
||||
try:
|
||||
|
||||
@@ -39,71 +39,52 @@ class ExcelProcessorParam(ComponentParamBase):
|
||||
"""
|
||||
Define the ExcelProcessor component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Input configuration
|
||||
self.input_files = [] # Variable references to uploaded files
|
||||
self.operation = "read" # read, merge, transform, output
|
||||
|
||||
|
||||
# Processing options
|
||||
self.sheet_selection = "all" # all, first, or comma-separated sheet names
|
||||
self.merge_strategy = "concat" # concat, join
|
||||
self.join_on = "" # Column name for join operations
|
||||
|
||||
|
||||
# Transform options (for LLM-guided transformations)
|
||||
self.transform_instructions = ""
|
||||
self.transform_data = "" # Variable reference to transformation data
|
||||
|
||||
|
||||
# Output options
|
||||
self.output_format = "xlsx" # xlsx, csv
|
||||
self.output_filename = "output"
|
||||
|
||||
|
||||
# Component outputs
|
||||
self.outputs = {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"value": {}
|
||||
},
|
||||
"summary": {
|
||||
"type": "str",
|
||||
"value": ""
|
||||
},
|
||||
"markdown": {
|
||||
"type": "str",
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
|
||||
self.outputs = {"data": {"type": "object", "value": {}}, "summary": {"type": "str", "value": ""}, "markdown": {"type": "str", "value": ""}}
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(
|
||||
self.operation,
|
||||
"[ExcelProcessor] Operation",
|
||||
["read", "merge", "transform", "output"]
|
||||
)
|
||||
self.check_valid_value(
|
||||
self.output_format,
|
||||
"[ExcelProcessor] Output format",
|
||||
["xlsx", "csv"]
|
||||
)
|
||||
self.check_valid_value(self.operation, "[ExcelProcessor] Operation", ["read", "merge", "transform", "output"])
|
||||
self.check_valid_value(self.output_format, "[ExcelProcessor] Output format", ["xlsx", "csv"])
|
||||
return True
|
||||
|
||||
|
||||
class ExcelProcessor(ComponentBase, ABC):
|
||||
"""
|
||||
Excel processing component for RAGFlow agents.
|
||||
|
||||
|
||||
Operations:
|
||||
- read: Parse Excel files into structured data
|
||||
- merge: Combine multiple Excel files
|
||||
- transform: Apply data transformations based on instructions
|
||||
- output: Generate Excel file output
|
||||
"""
|
||||
|
||||
component_name = "ExcelProcessor"
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
"""Define input form for the component."""
|
||||
res = {}
|
||||
for ref in (self._param.input_files or []):
|
||||
for ref in self._param.input_files or []:
|
||||
for k, o in self.get_input_elements_from_text(ref).items():
|
||||
res[k] = {"name": o.get("name", ""), "type": "file"}
|
||||
if self._param.transform_data:
|
||||
@@ -111,13 +92,13 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
res[k] = {"name": o.get("name", ""), "type": "object"}
|
||||
return res
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("ExcelProcessor processing"):
|
||||
return
|
||||
|
||||
operation = self._param.operation.lower()
|
||||
|
||||
|
||||
if operation == "read":
|
||||
self._read_excels()
|
||||
elif operation == "merge":
|
||||
@@ -137,7 +118,7 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
value = self._canvas.get_variable_value(file_ref)
|
||||
if value is None:
|
||||
return None, None
|
||||
|
||||
|
||||
# Handle different value formats
|
||||
if isinstance(value, dict):
|
||||
# File reference from Begin/UserFillUp component
|
||||
@@ -154,12 +135,13 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
# Could be base64 encoded or a path
|
||||
if value.startswith("data:"):
|
||||
import base64
|
||||
|
||||
# Extract base64 content
|
||||
_, encoded = value.split(",", 1)
|
||||
return base64.b64decode(encoded), "uploaded.xlsx"
|
||||
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _get_file_content_from_list(self, item) -> tuple[bytes, str]:
|
||||
"""Extract file content from a list item."""
|
||||
if isinstance(item, dict):
|
||||
@@ -170,15 +152,15 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
"""Parse Excel content into a dictionary of DataFrames (one per sheet)."""
|
||||
try:
|
||||
excel_file = BytesIO(content)
|
||||
|
||||
|
||||
if filename.lower().endswith(".csv"):
|
||||
df = pd.read_csv(excel_file)
|
||||
return {"Sheet1": df}
|
||||
else:
|
||||
# Read all sheets
|
||||
xlsx = pd.ExcelFile(excel_file, engine='openpyxl')
|
||||
xlsx = pd.ExcelFile(excel_file, engine="openpyxl")
|
||||
sheet_selection = self._param.sheet_selection
|
||||
|
||||
|
||||
if sheet_selection == "all":
|
||||
sheets_to_read = xlsx.sheet_names
|
||||
elif sheet_selection == "first":
|
||||
@@ -187,12 +169,12 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
# Comma-separated sheet names
|
||||
requested = [s.strip() for s in sheet_selection.split(",")]
|
||||
sheets_to_read = [s for s in requested if s in xlsx.sheet_names]
|
||||
|
||||
|
||||
dfs = {}
|
||||
for sheet in sheets_to_read:
|
||||
dfs[sheet] = pd.read_excel(xlsx, sheet_name=sheet)
|
||||
return dfs
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error parsing Excel file {filename}: {e}")
|
||||
return {}
|
||||
@@ -202,36 +184,36 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
all_data = {}
|
||||
summaries = []
|
||||
markdown_parts = []
|
||||
|
||||
for file_ref in (self._param.input_files or []):
|
||||
|
||||
for file_ref in self._param.input_files or []:
|
||||
if self.check_if_canceled("ExcelProcessor reading"):
|
||||
return
|
||||
|
||||
|
||||
# Get variable value
|
||||
value = self._canvas.get_variable_value(file_ref)
|
||||
self.set_input_value(file_ref, str(value)[:200] if value else "")
|
||||
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
|
||||
# Handle file content
|
||||
content, filename = self._get_file_content(file_ref)
|
||||
if content is None:
|
||||
continue
|
||||
|
||||
|
||||
# Parse Excel
|
||||
dfs = self._parse_excel_to_dataframes(content, filename)
|
||||
|
||||
|
||||
for sheet_name, df in dfs.items():
|
||||
key = f"{filename}_{sheet_name}" if len(dfs) > 1 else filename
|
||||
all_data[key] = df.to_dict(orient="records")
|
||||
|
||||
|
||||
# Build summary
|
||||
summaries.append(f"**{key}**: {len(df)} rows, {len(df.columns)} columns ({', '.join(df.columns.tolist()[:5])}{'...' if len(df.columns) > 5 else ''})")
|
||||
|
||||
|
||||
# Build markdown table
|
||||
markdown_parts.append(f"### {key}\n\n{df.head(10).to_markdown(index=False)}\n")
|
||||
|
||||
|
||||
# Set outputs
|
||||
self.set_output("data", all_data)
|
||||
self.set_output("summary", "\n".join(summaries) if summaries else "No Excel files found")
|
||||
@@ -240,29 +222,29 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
def _merge_excels(self):
|
||||
"""Merge multiple Excel files/sheets into one."""
|
||||
all_dfs = []
|
||||
|
||||
for file_ref in (self._param.input_files or []):
|
||||
|
||||
for file_ref in self._param.input_files or []:
|
||||
if self.check_if_canceled("ExcelProcessor merging"):
|
||||
return
|
||||
|
||||
|
||||
value = self._canvas.get_variable_value(file_ref)
|
||||
self.set_input_value(file_ref, str(value)[:200] if value else "")
|
||||
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
|
||||
content, filename = self._get_file_content(file_ref)
|
||||
if content is None:
|
||||
continue
|
||||
|
||||
|
||||
dfs = self._parse_excel_to_dataframes(content, filename)
|
||||
all_dfs.extend(dfs.values())
|
||||
|
||||
|
||||
if not all_dfs:
|
||||
self.set_output("data", {})
|
||||
self.set_output("summary", "No data to merge")
|
||||
return
|
||||
|
||||
|
||||
# Merge strategy
|
||||
if self._param.merge_strategy == "concat":
|
||||
merged_df = pd.concat(all_dfs, ignore_index=True)
|
||||
@@ -273,7 +255,7 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
merged_df = merged_df.merge(df, on=self._param.join_on, how="outer")
|
||||
else:
|
||||
merged_df = pd.concat(all_dfs, ignore_index=True)
|
||||
|
||||
|
||||
self.set_output("data", {"merged": merged_df.to_dict(orient="records")})
|
||||
self.set_output("summary", f"Merged {len(all_dfs)} sources into {len(merged_df)} rows, {len(merged_df.columns)} columns")
|
||||
self.set_output("markdown", merged_df.head(20).to_markdown(index=False))
|
||||
@@ -285,14 +267,14 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
if not transform_ref:
|
||||
self.set_output("summary", "No transform data reference provided")
|
||||
return
|
||||
|
||||
|
||||
data = self._canvas.get_variable_value(transform_ref)
|
||||
self.set_input_value(transform_ref, str(data)[:300] if data else "")
|
||||
|
||||
|
||||
if data is None:
|
||||
self.set_output("summary", "Transform data is empty")
|
||||
return
|
||||
|
||||
|
||||
# Convert to DataFrame
|
||||
if isinstance(data, dict):
|
||||
# Could be {"sheet": [rows]} format
|
||||
@@ -315,7 +297,7 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
else:
|
||||
self.set_output("data", {"raw": str(data)})
|
||||
self.set_output("markdown", str(data))
|
||||
|
||||
|
||||
self.set_output("summary", "Transformed data ready for processing")
|
||||
|
||||
def _output_excel(self):
|
||||
@@ -325,14 +307,14 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
if not transform_ref:
|
||||
self.set_output("summary", "No data reference for output")
|
||||
return
|
||||
|
||||
|
||||
data = self._canvas.get_variable_value(transform_ref)
|
||||
self.set_input_value(transform_ref, str(data)[:300] if data else "")
|
||||
|
||||
|
||||
if data is None:
|
||||
self.set_output("summary", "No data to output")
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# Prepare DataFrames
|
||||
if isinstance(data, dict):
|
||||
@@ -346,10 +328,10 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
else:
|
||||
self.set_output("summary", "Invalid data format for Excel output")
|
||||
return
|
||||
|
||||
|
||||
# Generate output
|
||||
doc_id = get_uuid()
|
||||
|
||||
|
||||
if self._param.output_format == "csv":
|
||||
# For CSV, only output first sheet
|
||||
first_df = list(dfs.values())[0]
|
||||
@@ -358,7 +340,7 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
else:
|
||||
# Excel output
|
||||
excel_io = BytesIO()
|
||||
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
|
||||
with pd.ExcelWriter(excel_io, engine="openpyxl") as writer:
|
||||
for sheet_name, df in dfs.items():
|
||||
# Sanitize sheet name (max 31 chars, no special chars)
|
||||
safe_name = sheet_name[:31].replace("/", "_").replace("\\", "_")
|
||||
@@ -366,23 +348,19 @@ class ExcelProcessor(ComponentBase, ABC):
|
||||
excel_io.seek(0)
|
||||
binary_content = excel_io.read()
|
||||
filename = f"{self._param.output_filename}.xlsx"
|
||||
|
||||
|
||||
# Store file
|
||||
settings.STORAGE_IMPL.put(self._canvas._tenant_id, doc_id, binary_content)
|
||||
|
||||
|
||||
# Set attachment output
|
||||
self.set_output("attachment", {
|
||||
"doc_id": doc_id,
|
||||
"format": self._param.output_format,
|
||||
"file_name": filename
|
||||
})
|
||||
|
||||
self.set_output("attachment", {"doc_id": doc_id, "format": self._param.output_format, "file_name": filename})
|
||||
|
||||
total_rows = sum(len(df) for df in dfs.values())
|
||||
self.set_output("summary", f"Generated {filename} with {len(dfs)} sheet(s), {total_rows} total rows")
|
||||
self.set_output("data", {k: v.to_dict(orient="records") for k, v in dfs.items()})
|
||||
|
||||
|
||||
logging.info(f"ExcelProcessor: Generated {filename} as {doc_id}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"ExcelProcessor output error: {e}")
|
||||
self.set_output("summary", f"Error generating output: {str(e)}")
|
||||
|
||||
@@ -29,4 +29,4 @@ class ExitLoop(ComponentBase, ABC):
|
||||
pass
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
@@ -25,7 +25,6 @@ _INITIAL_USER_INPUT_CONSUMED_KEY = "sys.__initial_user_input_consumed__"
|
||||
|
||||
|
||||
class UserFillUpParam(ComponentParamBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.enable_tips = True
|
||||
@@ -55,11 +54,7 @@ class UserFillUp(ComponentBase):
|
||||
return {}
|
||||
|
||||
if isinstance(query, dict):
|
||||
matched = {
|
||||
key: value if isinstance(value, dict) else {"value": value}
|
||||
for key, value in query.items()
|
||||
if key in fields
|
||||
}
|
||||
matched = {key: value if isinstance(value, dict) else {"value": value} for key, value in query.items() if key in fields}
|
||||
if matched:
|
||||
self._canvas.globals[_INITIAL_USER_INPUT_CONSUMED_KEY] = True
|
||||
return matched
|
||||
@@ -108,7 +103,7 @@ class UserFillUp(ComponentBase):
|
||||
ans = v
|
||||
if not ans:
|
||||
ans = ""
|
||||
content = re.sub(r"\{%s\}"%k, ans, content)
|
||||
content = re.sub(r"\{%s\}" % k, ans, content)
|
||||
|
||||
self.set_output("tips", content)
|
||||
layout_recognize = self._param.layout_recognize or None
|
||||
|
||||
@@ -24,6 +24,7 @@ class VariableModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
"""
|
||||
|
||||
|
||||
class IterationParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Iteration component parameters.
|
||||
@@ -32,15 +33,10 @@ class IterationParam(ComponentParamBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.items_ref = ""
|
||||
self.variable={}
|
||||
self.variable = {}
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"items": {
|
||||
"type": "json",
|
||||
"name": "Items"
|
||||
}
|
||||
}
|
||||
return {"items": {"type": "json", "name": "Items"}}
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
@@ -62,10 +58,7 @@ class Iteration(ComponentBase, ABC):
|
||||
|
||||
arr = self._canvas.get_variable_value(self._param.items_ref)
|
||||
if not isinstance(arr, list):
|
||||
self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is "+str(type(arr)))
|
||||
self.set_output("_ERROR", self._param.items_ref + " must be an array, but its type is " + str(type(arr)))
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Need to process {} items.".format(len(self._canvas.get_variable_value(self._param.items_ref)))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class IterationItemParam(ComponentParamBase):
|
||||
"""
|
||||
Define the IterationItem component parameters.
|
||||
"""
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
@@ -40,7 +41,7 @@ class IterationItem(ComponentBase, ABC):
|
||||
arr = self._canvas.get_variable_value(parent._param.items_ref)
|
||||
if not isinstance(arr, list):
|
||||
self._idx = -1
|
||||
raise Exception(parent._param.items_ref + " must be an array, but its type is "+str(type(arr)))
|
||||
raise Exception(parent._param.items_ref + " must be an array, but its type is " + str(type(arr)))
|
||||
|
||||
if self._idx > 0:
|
||||
if self.check_if_canceled("IterationItem processing"):
|
||||
|
||||
@@ -3,10 +3,12 @@ import os
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
from api.utils.api_utils import timeout
|
||||
|
||||
|
||||
class ListOperationsParam(ComponentParamBase):
|
||||
"""
|
||||
Define the List Operations component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.query = ""
|
||||
@@ -20,24 +22,8 @@ class ListOperationsParam(ComponentParamBase):
|
||||
# first field). Mirrors internal/agent/component/list_operations.go
|
||||
# parseSortByFieldList + opSort's SortBy path.
|
||||
self.sort_by = ""
|
||||
self.filter = {
|
||||
"operator": "=",
|
||||
"value": ""
|
||||
}
|
||||
self.outputs = {
|
||||
"result": {
|
||||
"value": [],
|
||||
"type": "Array of ?"
|
||||
},
|
||||
"first": {
|
||||
"value": "",
|
||||
"type": "?"
|
||||
},
|
||||
"last": {
|
||||
"value": "",
|
||||
"type": "?"
|
||||
}
|
||||
}
|
||||
self.filter = {"operator": "=", "value": ""}
|
||||
self.outputs = {"result": {"value": [], "type": "Array of ?"}, "first": {"value": "", "type": "?"}, "last": {"value": "", "type": "?"}}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_operation_name(operation):
|
||||
@@ -45,7 +31,7 @@ class ListOperationsParam(ComponentParamBase):
|
||||
if op.lower() == "topn":
|
||||
return "head"
|
||||
return op or "nth"
|
||||
|
||||
|
||||
def check(self):
|
||||
self.check_empty(self.query, "query")
|
||||
self.operations = self._normalize_operation_name(self.operations)
|
||||
@@ -57,14 +43,14 @@ class ListOperationsParam(ComponentParamBase):
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {}
|
||||
|
||||
|
||||
class ListOperations(ComponentBase,ABC):
|
||||
|
||||
class ListOperations(ComponentBase, ABC):
|
||||
component_name = "ListOperations"
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
self.input_objects=[]
|
||||
self.input_objects = []
|
||||
inputs = getattr(self._param, "query", None)
|
||||
self.inputs = self._canvas.get_variable_value(inputs)
|
||||
if not isinstance(self.inputs, list):
|
||||
@@ -83,7 +69,6 @@ class ListOperations(ComponentBase,ABC):
|
||||
elif self._param.operations == "drop_duplicates":
|
||||
self._drop_duplicates()
|
||||
|
||||
|
||||
def _coerce_n(self):
|
||||
try:
|
||||
return int(getattr(self._param, "n", 0))
|
||||
@@ -99,12 +84,10 @@ class ListOperations(ComponentBase,ABC):
|
||||
def _set_outputs(self, outputs):
|
||||
self._param.outputs["result"]["value"] = outputs
|
||||
self._param.outputs["first"]["value"] = outputs[0] if outputs else None
|
||||
self._param.outputs["last"]["value"] = outputs[-1] if outputs else None
|
||||
self._param.outputs["last"]["value"] = outputs[-1] if outputs else None
|
||||
|
||||
def _raise_strict_range_error(self, operation, n):
|
||||
raise ValueError(
|
||||
f"{operation} requires n to be within the valid range in strict mode, got {n}."
|
||||
)
|
||||
raise ValueError(f"{operation} requires n to be within the valid range in strict mode, got {n}.")
|
||||
|
||||
def _nth(self):
|
||||
n = self._coerce_n()
|
||||
@@ -160,9 +143,9 @@ class ListOperations(ComponentBase,ABC):
|
||||
self._set_outputs(outputs)
|
||||
|
||||
def _filter(self):
|
||||
self._set_outputs([i for i in self.inputs if self._eval(self._norm(i),self._param.filter["operator"],self._param.filter["value"])])
|
||||
self._set_outputs([i for i in self.inputs if self._eval(self._norm(i), self._param.filter["operator"], self._param.filter["value"])])
|
||||
|
||||
def _norm(self,v):
|
||||
def _norm(self, v):
|
||||
s = "" if v is None else str(v)
|
||||
return s
|
||||
|
||||
@@ -222,7 +205,7 @@ class ListOperations(ComponentBase,ABC):
|
||||
outs.append(item)
|
||||
self._set_outputs(outs)
|
||||
|
||||
def _hashable(self,x):
|
||||
def _hashable(self, x):
|
||||
if isinstance(x, dict):
|
||||
return tuple(sorted((k, self._hashable(v)) for k, v in x.items()))
|
||||
if isinstance(x, (list, tuple)):
|
||||
|
||||
+7
-13
@@ -25,16 +25,11 @@ class LoopParam(ComponentParamBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.loop_variables = []
|
||||
self.loop_termination_condition=[]
|
||||
self.loop_termination_condition = []
|
||||
self.maximum_loop_count = 0
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"items": {
|
||||
"type": "json",
|
||||
"name": "Items"
|
||||
}
|
||||
}
|
||||
return {"items": {"type": "json", "name": "Items"}}
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
@@ -83,10 +78,10 @@ class Loop(ComponentBase, ABC):
|
||||
for item in self._param.loop_variables:
|
||||
if self._is_incomplete_loop_variable(item):
|
||||
raise ValueError("Loop Variable is not complete.")
|
||||
if item["input_mode"]=="variable":
|
||||
self.set_output(item["variable"],self._canvas.get_variable_value(item["value"]))
|
||||
elif item["input_mode"]=="constant":
|
||||
self.set_output(item["variable"],item["value"])
|
||||
if item["input_mode"] == "variable":
|
||||
self.set_output(item["variable"], self._canvas.get_variable_value(item["value"]))
|
||||
elif item["input_mode"] == "constant":
|
||||
self.set_output(item["variable"], item["value"])
|
||||
else:
|
||||
if item["type"] == "number":
|
||||
self.set_output(item["variable"], 0)
|
||||
@@ -101,6 +96,5 @@ class Loop(ComponentBase, ABC):
|
||||
else:
|
||||
self.set_output(item["variable"], "")
|
||||
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Loop from canvas."
|
||||
return "Loop from canvas."
|
||||
|
||||
@@ -21,9 +21,11 @@ class LoopItemParam(ComponentParamBase):
|
||||
"""
|
||||
Define the LoopItem component parameters.
|
||||
"""
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
|
||||
class LoopItem(ComponentBase, ABC):
|
||||
component_name = "LoopItem"
|
||||
|
||||
@@ -31,7 +33,6 @@ class LoopItem(ComponentBase, ABC):
|
||||
super().__init__(canvas, id, param)
|
||||
self._idx = 0
|
||||
|
||||
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("LoopItem processing"):
|
||||
return
|
||||
@@ -45,7 +46,7 @@ class LoopItem(ComponentBase, ABC):
|
||||
return
|
||||
self._idx += 1
|
||||
|
||||
def evaluate_condition(self,var, operator, value):
|
||||
def evaluate_condition(self, var, operator, value):
|
||||
if isinstance(var, str):
|
||||
if operator == "contains":
|
||||
return value in var
|
||||
@@ -140,11 +141,7 @@ class LoopItem(ComponentBase, ABC):
|
||||
else:
|
||||
raise ValueError("Invalid input mode.")
|
||||
conditions.append(self.evaluate_condition(var, operator, value))
|
||||
should_end = (
|
||||
all(conditions) if logical_operator == "and"
|
||||
else any(conditions) if logical_operator == "or"
|
||||
else None
|
||||
)
|
||||
should_end = all(conditions) if logical_operator == "and" else any(conditions) if logical_operator == "or" else None
|
||||
if should_end is None:
|
||||
raise ValueError("Invalid logical operator,should be 'and' or 'or'.")
|
||||
|
||||
@@ -164,4 +161,4 @@ class LoopItem(ComponentBase, ABC):
|
||||
return False
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Next turn..."
|
||||
return "Next turn..."
|
||||
|
||||
+59
-80
@@ -14,8 +14,10 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
import nest_asyncio
|
||||
|
||||
nest_asyncio.apply()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -45,20 +47,14 @@ class MessageParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Message component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.content = []
|
||||
self.stream = True
|
||||
self.output_format = None # default output format
|
||||
self.auto_play = False
|
||||
self.outputs = {
|
||||
"content": {
|
||||
"type": "str"
|
||||
},
|
||||
"downloads": {
|
||||
"type": "list"
|
||||
}
|
||||
}
|
||||
self.outputs = {"content": {"type": "str"}, "downloads": {"type": "list"}}
|
||||
|
||||
def check(self):
|
||||
self.check_empty(self.content, "[Message] Content")
|
||||
@@ -71,9 +67,7 @@ class Message(ComponentBase):
|
||||
|
||||
@staticmethod
|
||||
def _is_download_info(value: Any) -> bool:
|
||||
return isinstance(value, dict) and all(
|
||||
key in value for key in ("doc_id", "filename", "mime_type")
|
||||
)
|
||||
return isinstance(value, dict) and all(key in value for key in ("doc_id", "filename", "mime_type"))
|
||||
|
||||
@staticmethod
|
||||
def _download_info_includes_content(value: Any) -> bool:
|
||||
@@ -157,7 +151,7 @@ class Message(ComponentBase):
|
||||
delimiter: str = None,
|
||||
downloads: list[dict[str, Any]] | None = None,
|
||||
) -> tuple[str, dict[str, str | list | Any]]:
|
||||
for k,v in self.get_input_elements_from_text(script).items():
|
||||
for k, v in self.get_input_elements_from_text(script).items():
|
||||
if k in kwargs:
|
||||
continue
|
||||
v = v["value"]
|
||||
@@ -191,7 +185,7 @@ class Message(ComponentBase):
|
||||
buf += t
|
||||
return buf
|
||||
|
||||
async def _stream(self, rand_cnt:str):
|
||||
async def _stream(self, rand_cnt: str):
|
||||
s = 0
|
||||
all_content = ""
|
||||
cache = {}
|
||||
@@ -200,8 +194,8 @@ class Message(ComponentBase):
|
||||
if self.check_if_canceled("Message streaming"):
|
||||
return
|
||||
|
||||
all_content += rand_cnt[s: r.start()]
|
||||
yield rand_cnt[s: r.start()]
|
||||
all_content += rand_cnt[s : r.start()]
|
||||
yield rand_cnt[s : r.start()]
|
||||
s = r.end()
|
||||
exp = r.group(1)
|
||||
if exp in cache:
|
||||
@@ -235,9 +229,7 @@ class Message(ComponentBase):
|
||||
continue
|
||||
elif inspect.isawaitable(v):
|
||||
v = await v
|
||||
v = self._stringify_message_value(
|
||||
v, downloads=downloads, fallback_to_str=True
|
||||
)
|
||||
v = self._stringify_message_value(v, downloads=downloads, fallback_to_str=True)
|
||||
yield v
|
||||
self.set_input_value(exp, v)
|
||||
all_content += v
|
||||
@@ -247,21 +239,19 @@ class Message(ComponentBase):
|
||||
if self.check_if_canceled("Message streaming"):
|
||||
return
|
||||
|
||||
all_content += rand_cnt[s: ]
|
||||
yield rand_cnt[s: ]
|
||||
all_content += rand_cnt[s:]
|
||||
yield rand_cnt[s:]
|
||||
|
||||
self.set_output("downloads", downloads)
|
||||
self.set_output("content", all_content)
|
||||
self._convert_content(all_content)
|
||||
await self._save_to_memory(all_content)
|
||||
|
||||
def _is_jinjia2(self, content:str) -> bool:
|
||||
patt = [
|
||||
r"\{%.*%\}", "{{", "}}"
|
||||
]
|
||||
def _is_jinjia2(self, content: str) -> bool:
|
||||
patt = [r"\{%.*%\}", "{{", "}}"]
|
||||
return any([re.search(p, content) for p in patt])
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("Message processing"):
|
||||
return
|
||||
@@ -303,19 +293,19 @@ class Message(ComponentBase):
|
||||
def _parse_markdown_table_lines(self, table_lines: list):
|
||||
"""
|
||||
Parse a list of Markdown table lines into a pandas DataFrame.
|
||||
|
||||
|
||||
Args:
|
||||
table_lines: List of strings, each representing a row in the Markdown table
|
||||
(excluding separator lines like |---|---|)
|
||||
|
||||
|
||||
Returns:
|
||||
pandas DataFrame with the table data, or None if parsing fails
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
|
||||
if not table_lines:
|
||||
return None
|
||||
|
||||
|
||||
rows = []
|
||||
headers = None
|
||||
|
||||
@@ -350,29 +340,29 @@ class Message(ComponentBase):
|
||||
return cell
|
||||
|
||||
return cell
|
||||
|
||||
|
||||
for line in table_lines:
|
||||
# Split by | and clean up
|
||||
cells = [cell.strip() for cell in line.split('|')]
|
||||
cells = [cell.strip() for cell in line.split("|")]
|
||||
# Remove empty first and last elements from split (caused by leading/trailing |)
|
||||
cells = [c for c in cells if c]
|
||||
|
||||
|
||||
if headers is None:
|
||||
headers = cells
|
||||
else:
|
||||
cells = [_coerce_excel_cell_type(c) for c in cells]
|
||||
rows.append(cells)
|
||||
|
||||
|
||||
if headers and rows:
|
||||
# Ensure all rows have same number of columns as headers
|
||||
normalized_rows = []
|
||||
for row in rows:
|
||||
while len(row) < len(headers):
|
||||
row.append('')
|
||||
normalized_rows.append(row[:len(headers)])
|
||||
|
||||
row.append("")
|
||||
normalized_rows.append(row[: len(headers)])
|
||||
|
||||
return pd.DataFrame(normalized_rows, columns=headers)
|
||||
|
||||
|
||||
return None
|
||||
|
||||
def _convert_content(self, content):
|
||||
@@ -380,6 +370,7 @@ class Message(ComponentBase):
|
||||
return
|
||||
|
||||
import pypandoc
|
||||
|
||||
doc_id = get_uuid()
|
||||
|
||||
if self._param.output_format.lower() not in {"markdown", "html", "pdf", "docx", "xlsx"}:
|
||||
@@ -408,49 +399,47 @@ class Message(ComponentBase):
|
||||
|
||||
# Debug: log the content being parsed
|
||||
logging.info(f"XLSX Parser: Content length={len(content) if content else 0}, first 500 chars: {content[:500] if content else 'None'}")
|
||||
|
||||
|
||||
# Try to parse ALL Markdown tables from the content
|
||||
# Each table will be written to a separate sheet
|
||||
tables = [] # List of (sheet_name, dataframe)
|
||||
|
||||
|
||||
if isinstance(content, str):
|
||||
lines = content.strip().split('\n')
|
||||
lines = content.strip().split("\n")
|
||||
logging.info(f"XLSX Parser: Total lines={len(lines)}, lines starting with '|': {sum(1 for line in lines if line.strip().startswith('|'))}")
|
||||
current_table_lines = []
|
||||
current_table_title = None
|
||||
pending_title = None
|
||||
in_table = False
|
||||
table_count = 0
|
||||
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
|
||||
|
||||
# Check for potential table title (lines before a table)
|
||||
# Look for patterns like "Table 1:", "## Table", or markdown headers
|
||||
if not in_table and stripped and not stripped.startswith('|'):
|
||||
if not in_table and stripped and not stripped.startswith("|"):
|
||||
# Check if this could be a table title
|
||||
lower_stripped = stripped.lower()
|
||||
if (lower_stripped.startswith('table') or
|
||||
stripped.startswith('#') or
|
||||
':' in stripped):
|
||||
pending_title = stripped.lstrip('#').strip()
|
||||
|
||||
if stripped.startswith('|') and '|' in stripped[1:]:
|
||||
if lower_stripped.startswith("table") or stripped.startswith("#") or ":" in stripped:
|
||||
pending_title = stripped.lstrip("#").strip()
|
||||
|
||||
if stripped.startswith("|") and "|" in stripped[1:]:
|
||||
# Check if this is a separator line (|---|---|)
|
||||
cleaned = stripped.replace(' ', '').replace('|', '').replace('-', '').replace(':', '')
|
||||
if cleaned == '':
|
||||
cleaned = stripped.replace(" ", "").replace("|", "").replace("-", "").replace(":", "")
|
||||
if cleaned == "":
|
||||
continue # Skip separator line
|
||||
|
||||
|
||||
if not in_table:
|
||||
# Starting a new table
|
||||
in_table = True
|
||||
current_table_lines = []
|
||||
current_table_title = pending_title
|
||||
pending_title = None
|
||||
|
||||
|
||||
current_table_lines.append(stripped)
|
||||
|
||||
elif in_table and not stripped.startswith('|'):
|
||||
|
||||
elif in_table and not stripped.startswith("|"):
|
||||
# End of current table - save it
|
||||
if current_table_lines:
|
||||
df = self._parse_markdown_table_lines(current_table_lines)
|
||||
@@ -460,24 +449,22 @@ class Message(ComponentBase):
|
||||
if current_table_title:
|
||||
# Clean and truncate title for sheet name
|
||||
sheet_name = current_table_title[:31]
|
||||
sheet_name = sheet_name.replace('/', '_').replace('\\', '_').replace('*', '').replace('?', '').replace('[', '').replace(']', '').replace(':', '')
|
||||
sheet_name = sheet_name.replace("/", "_").replace("\\", "_").replace("*", "").replace("?", "").replace("[", "").replace("]", "").replace(":", "")
|
||||
else:
|
||||
sheet_name = f"Table_{table_count}"
|
||||
tables.append((sheet_name, df))
|
||||
|
||||
|
||||
# Reset for next table
|
||||
in_table = False
|
||||
current_table_lines = []
|
||||
current_table_title = None
|
||||
|
||||
|
||||
# Check if this line could be a title for the next table
|
||||
if stripped:
|
||||
lower_stripped = stripped.lower()
|
||||
if (lower_stripped.startswith('table') or
|
||||
stripped.startswith('#') or
|
||||
':' in stripped):
|
||||
pending_title = stripped.lstrip('#').strip()
|
||||
|
||||
if lower_stripped.startswith("table") or stripped.startswith("#") or ":" in stripped:
|
||||
pending_title = stripped.lstrip("#").strip()
|
||||
|
||||
# Don't forget the last table if content ends with a table
|
||||
if in_table and current_table_lines:
|
||||
df = self._parse_markdown_table_lines(current_table_lines)
|
||||
@@ -485,11 +472,11 @@ class Message(ComponentBase):
|
||||
table_count += 1
|
||||
if current_table_title:
|
||||
sheet_name = current_table_title[:31]
|
||||
sheet_name = sheet_name.replace('/', '_').replace('\\', '_').replace('*', '').replace('?', '').replace('[', '').replace(']', '').replace(':', '')
|
||||
sheet_name = sheet_name.replace("/", "_").replace("\\", "_").replace("*", "").replace("?", "").replace("[", "").replace("]", "").replace(":", "")
|
||||
else:
|
||||
sheet_name = f"Table_{table_count}"
|
||||
tables.append((sheet_name, df))
|
||||
|
||||
|
||||
# Fallback: if no tables found, create single sheet with content
|
||||
if not tables:
|
||||
df = pd.DataFrame({"Content": [content if content else ""]})
|
||||
@@ -497,7 +484,7 @@ class Message(ComponentBase):
|
||||
|
||||
# Write all tables to Excel, each in a separate sheet
|
||||
excel_io = BytesIO()
|
||||
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
|
||||
with pd.ExcelWriter(excel_io, engine="openpyxl") as writer:
|
||||
used_names = set()
|
||||
for sheet_name, df in tables:
|
||||
# Ensure unique sheet names
|
||||
@@ -505,14 +492,14 @@ class Message(ComponentBase):
|
||||
counter = 1
|
||||
while sheet_name in used_names:
|
||||
suffix = f"_{counter}"
|
||||
sheet_name = original_name[:31-len(suffix)] + suffix
|
||||
sheet_name = original_name[: 31 - len(suffix)] + suffix
|
||||
counter += 1
|
||||
used_names.add(sheet_name)
|
||||
df.to_excel(writer, sheet_name=sheet_name, index=False)
|
||||
|
||||
|
||||
excel_io.seek(0)
|
||||
binary_content = excel_io.read()
|
||||
|
||||
|
||||
logging.info(f"Generated Excel with {len(tables)} sheet(s): {[t[0] for t in tables]}")
|
||||
|
||||
else: # pdf, docx
|
||||
@@ -543,10 +530,7 @@ class Message(ComponentBase):
|
||||
os.remove(tmp_name)
|
||||
|
||||
settings.STORAGE_IMPL.put(self._canvas._tenant_id, doc_id, binary_content)
|
||||
self.set_output("attachment", {
|
||||
"doc_id":doc_id,
|
||||
"format":self._param.output_format,
|
||||
"file_name":f"{doc_id[:8]}.{self._param.output_format}"})
|
||||
self.set_output("attachment", {"doc_id": doc_id, "format": self._param.output_format, "file_name": f"{doc_id[:8]}.{self._param.output_format}"})
|
||||
|
||||
logging.info(f"Converted content uploaded as {doc_id} (format={self._param.output_format})")
|
||||
|
||||
@@ -560,15 +544,10 @@ class Message(ComponentBase):
|
||||
user_id = self._param.user_id if hasattr(self._param, "user_id") else ""
|
||||
if user_id:
|
||||
import re
|
||||
|
||||
# is variable
|
||||
if re.match(r"^{.*}$", user_id):
|
||||
user_id = self._canvas.get_variable_value(user_id)
|
||||
|
||||
message_dict = {
|
||||
"user_id": user_id,
|
||||
"agent_id": self._canvas._id,
|
||||
"session_id": self._canvas.task_id,
|
||||
"user_input": self._canvas.get_sys_query(),
|
||||
"agent_response": content
|
||||
}
|
||||
message_dict = {"user_id": user_id, "agent_id": self._canvas._id, "session_id": self._canvas.task_id, "user_input": self._canvas.get_sys_query(), "agent_response": content}
|
||||
return await queue_save_to_memory_task(self._param.memory_ids, message_dict)
|
||||
|
||||
@@ -131,10 +131,7 @@ class PipelineChunker(ComponentBase, ABC):
|
||||
try:
|
||||
return FileService.get_blob(created_by, file_id), filename
|
||||
except Exception as e:
|
||||
logging.exception(
|
||||
f"[PipelineChunker] FileService.get_blob failed for "
|
||||
f"file_id={file_id} created_by={created_by} filename={filename}: {e}"
|
||||
)
|
||||
logging.exception(f"[PipelineChunker] FileService.get_blob failed for file_id={file_id} created_by={created_by} filename={filename}: {e}")
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -52,18 +52,10 @@ class StringTransform(Message, ABC):
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
if self._param.method == "split":
|
||||
return {
|
||||
"line": {
|
||||
"name": "String",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {k: {
|
||||
"name": o["name"],
|
||||
"type": "line"
|
||||
} for k, o in self.get_input_elements_from_text(self._param.script).items()}
|
||||
return {"line": {"name": "String", "type": "line"}}
|
||||
return {k: {"name": o["name"], "type": "line"} for k, o in self.get_input_elements_from_text(self._param.script).items()}
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("StringTransform processing"):
|
||||
return
|
||||
@@ -73,7 +65,7 @@ class StringTransform(Message, ABC):
|
||||
else:
|
||||
self._merge(kwargs)
|
||||
|
||||
def _split(self, line:str|None = None):
|
||||
def _split(self, line: str | None = None):
|
||||
if self.check_if_canceled("StringTransform split processing"):
|
||||
return
|
||||
|
||||
@@ -84,13 +76,13 @@ class StringTransform(Message, ABC):
|
||||
self.set_input_value(self._param.split_ref, var)
|
||||
|
||||
res = []
|
||||
for i,s in enumerate(re.split(r"(%s)"%("|".join([re.escape(d) for d in self._param.delimiters])), var, flags=re.DOTALL)):
|
||||
for i, s in enumerate(re.split(r"(%s)" % ("|".join([re.escape(d) for d in self._param.delimiters])), var, flags=re.DOTALL)):
|
||||
if i % 2 == 1:
|
||||
continue
|
||||
res.append(s)
|
||||
self.set_output("result", res)
|
||||
|
||||
def _merge(self, kwargs:dict[str, str] = {}):
|
||||
def _merge(self, kwargs: dict[str, str] = {}):
|
||||
if self.check_if_canceled("StringTransform merge processing"):
|
||||
return
|
||||
|
||||
@@ -104,7 +96,7 @@ class StringTransform(Message, ABC):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for k,v in kwargs.items():
|
||||
for k, v in kwargs.items():
|
||||
if v is None:
|
||||
v = ""
|
||||
script = re.sub(k, lambda match: v, script)
|
||||
@@ -113,5 +105,3 @@ class StringTransform(Message, ABC):
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return f"It's {self._param.method}ing."
|
||||
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ class SwitchParam(ComponentParamBase):
|
||||
"""
|
||||
self.conditions = []
|
||||
self.end_cpn_ids = []
|
||||
self.operators = ['contains', 'not contains', 'start with', 'end with', 'empty', 'not empty', '=', '≠', '>',
|
||||
'<', '≥', '≤']
|
||||
self.operators = ["contains", "not contains", "start with", "end with", "empty", "not empty", "=", "≠", ">", "<", "≥", "≤"]
|
||||
|
||||
def check(self):
|
||||
self.check_empty(self.conditions, "[Switch] conditions")
|
||||
@@ -51,12 +50,8 @@ class SwitchParam(ComponentParamBase):
|
||||
self.check_empty(self.end_cpn_ids, "[Switch] the ELSE/Other destination can not be empty.")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"urls": {
|
||||
"name": "URLs",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"urls": {"name": "URLs", "type": "line"}}
|
||||
|
||||
|
||||
class Switch(ComponentBase, ABC):
|
||||
component_name = "Switch"
|
||||
@@ -137,7 +132,7 @@ class Switch(ComponentBase, ABC):
|
||||
except Exception:
|
||||
return True if input <= value else False
|
||||
|
||||
raise ValueError(f'Not supported operator: {operator}')
|
||||
raise ValueError(f"Not supported operator: {operator}")
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "I’m weighing a few options and will pick the next step shortly."
|
||||
|
||||
@@ -38,13 +38,9 @@ class VariableAggregatorParam(ComponentParamBase):
|
||||
if not g.get("group_name"):
|
||||
raise ValueError("[VariableAggregator] group_name can not be empty!")
|
||||
if not g.get("variables"):
|
||||
raise ValueError(
|
||||
f"[VariableAggregator] variables of group `{g.get('group_name')}` can not be empty"
|
||||
)
|
||||
raise ValueError(f"[VariableAggregator] variables of group `{g.get('group_name')}` can not be empty")
|
||||
if not isinstance(g.get("variables"), list):
|
||||
raise ValueError(
|
||||
f"[VariableAggregator] variables of group `{g.get('group_name')}` should be a list of strings"
|
||||
)
|
||||
raise ValueError(f"[VariableAggregator] variables of group `{g.get('group_name')}` should be a list of strings")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
@@ -67,11 +63,11 @@ class VariableAggregator(ComponentBase):
|
||||
# record candidate selectors within this group
|
||||
self.set_input_value(f"{gname}.variables", list(group.get("variables", [])))
|
||||
for selector in group.get("variables", []):
|
||||
val = self._canvas.get_variable_value(selector['value'])
|
||||
val = self._canvas.get_variable_value(selector["value"])
|
||||
if val:
|
||||
self.set_output(gname, val)
|
||||
break
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _to_object(value: Any) -> Any:
|
||||
# Try to convert value to serializable object if it has to_object()
|
||||
|
||||
@@ -19,32 +19,30 @@ import numbers
|
||||
from agent.component.base import ComponentBase, ComponentParamBase
|
||||
from api.utils.api_utils import timeout
|
||||
|
||||
|
||||
class VariableAssignerParam(ComponentParamBase):
|
||||
"""
|
||||
Define the Variable Assigner component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.variables=[]
|
||||
self.variables = []
|
||||
|
||||
def check(self):
|
||||
return True
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"items": {
|
||||
"type": "json",
|
||||
"name": "Items"
|
||||
}
|
||||
}
|
||||
|
||||
class VariableAssigner(ComponentBase,ABC):
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {"items": {"type": "json", "name": "Items"}}
|
||||
|
||||
|
||||
class VariableAssigner(ComponentBase, ABC):
|
||||
component_name = "VariableAssigner"
|
||||
_NO_PARAMETER_OPERATORS = {"clear", "remove_first", "remove_last"}
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
if not isinstance(self._param.variables,list):
|
||||
if not isinstance(self._param.variables, list):
|
||||
return
|
||||
else:
|
||||
for item in self._param.variables:
|
||||
@@ -56,105 +54,105 @@ class VariableAssigner(ComponentBase,ABC):
|
||||
raise ValueError("Variable is not complete.")
|
||||
if operator not in self._NO_PARAMETER_OPERATORS and parameter is None:
|
||||
raise ValueError("Variable is not complete.")
|
||||
variable_value=self._canvas.get_variable_value(variable)
|
||||
new_variable=self._operate(variable_value,operator,parameter)
|
||||
variable_value = self._canvas.get_variable_value(variable)
|
||||
new_variable = self._operate(variable_value, operator, parameter)
|
||||
self._canvas.set_variable_value(variable, new_variable)
|
||||
|
||||
def _operate(self,variable,operator,parameter):
|
||||
def _operate(self, variable, operator, parameter):
|
||||
if operator == "overwrite":
|
||||
return self._overwrite(parameter)
|
||||
elif operator == "clear":
|
||||
return self._clear(variable)
|
||||
elif operator == "set":
|
||||
return self._set(variable,parameter)
|
||||
return self._set(variable, parameter)
|
||||
elif operator == "append":
|
||||
return self._append(variable,parameter)
|
||||
return self._append(variable, parameter)
|
||||
elif operator == "extend":
|
||||
return self._extend(variable,parameter)
|
||||
return self._extend(variable, parameter)
|
||||
elif operator == "remove_first":
|
||||
return self._remove_first(variable)
|
||||
elif operator == "remove_last":
|
||||
return self._remove_last(variable)
|
||||
elif operator == "+=":
|
||||
return self._add(variable,parameter)
|
||||
return self._add(variable, parameter)
|
||||
elif operator == "-=":
|
||||
return self._subtract(variable,parameter)
|
||||
return self._subtract(variable, parameter)
|
||||
elif operator == "*=":
|
||||
return self._multiply(variable,parameter)
|
||||
return self._multiply(variable, parameter)
|
||||
elif operator == "/=":
|
||||
return self._divide(variable,parameter)
|
||||
return self._divide(variable, parameter)
|
||||
else:
|
||||
return
|
||||
|
||||
def _overwrite(self,parameter):
|
||||
|
||||
def _overwrite(self, parameter):
|
||||
return self._canvas.get_variable_value(parameter)
|
||||
|
||||
def _clear(self,variable):
|
||||
if isinstance(variable,list):
|
||||
def _clear(self, variable):
|
||||
if isinstance(variable, list):
|
||||
return []
|
||||
elif isinstance(variable,str):
|
||||
elif isinstance(variable, str):
|
||||
return ""
|
||||
elif isinstance(variable,dict):
|
||||
elif isinstance(variable, dict):
|
||||
return {}
|
||||
elif isinstance(variable,bool):
|
||||
elif isinstance(variable, bool):
|
||||
return False
|
||||
elif isinstance(variable,int):
|
||||
elif isinstance(variable, int):
|
||||
return 0
|
||||
elif isinstance(variable,float):
|
||||
elif isinstance(variable, float):
|
||||
return 0.0
|
||||
else:
|
||||
return None
|
||||
|
||||
def _set(self,variable,parameter):
|
||||
def _set(self, variable, parameter):
|
||||
if variable is None:
|
||||
return self._canvas.get_value_with_variable(parameter)
|
||||
elif isinstance(variable,str):
|
||||
elif isinstance(variable, str):
|
||||
return self._canvas.get_value_with_variable(parameter)
|
||||
elif isinstance(variable,bool):
|
||||
elif isinstance(variable, bool):
|
||||
return parameter
|
||||
elif isinstance(variable,int):
|
||||
elif isinstance(variable, int):
|
||||
return parameter
|
||||
elif isinstance(variable,float):
|
||||
elif isinstance(variable, float):
|
||||
return parameter
|
||||
else:
|
||||
return parameter
|
||||
|
||||
def _append(self,variable,parameter):
|
||||
parameter=self._canvas.get_variable_value(parameter)
|
||||
def _append(self, variable, parameter):
|
||||
parameter = self._canvas.get_variable_value(parameter)
|
||||
if variable is None:
|
||||
variable=[]
|
||||
if not isinstance(variable,list):
|
||||
variable = []
|
||||
if not isinstance(variable, list):
|
||||
return "ERROR:VARIABLE_NOT_LIST"
|
||||
elif len(variable)!=0 and not isinstance(parameter,type(variable[0])):
|
||||
elif len(variable) != 0 and not isinstance(parameter, type(variable[0])):
|
||||
return "ERROR:PARAMETER_NOT_LIST_ELEMENT_TYPE"
|
||||
else:
|
||||
variable.append(parameter)
|
||||
return variable
|
||||
|
||||
def _extend(self,variable,parameter):
|
||||
parameter=self._canvas.get_variable_value(parameter)
|
||||
def _extend(self, variable, parameter):
|
||||
parameter = self._canvas.get_variable_value(parameter)
|
||||
if variable is None:
|
||||
variable=[]
|
||||
if not isinstance(variable,list):
|
||||
variable = []
|
||||
if not isinstance(variable, list):
|
||||
return "ERROR:VARIABLE_NOT_LIST"
|
||||
elif not isinstance(parameter,list):
|
||||
elif not isinstance(parameter, list):
|
||||
return "ERROR:PARAMETER_NOT_LIST"
|
||||
elif len(variable)!=0 and len(parameter)!=0 and not isinstance(parameter[0],type(variable[0])):
|
||||
elif len(variable) != 0 and len(parameter) != 0 and not isinstance(parameter[0], type(variable[0])):
|
||||
return "ERROR:PARAMETER_NOT_LIST_ELEMENT_TYPE"
|
||||
else:
|
||||
return variable + parameter
|
||||
|
||||
def _remove_first(self,variable):
|
||||
if not isinstance(variable,list):
|
||||
def _remove_first(self, variable):
|
||||
if not isinstance(variable, list):
|
||||
return "ERROR:VARIABLE_NOT_LIST"
|
||||
if len(variable)==0:
|
||||
if len(variable) == 0:
|
||||
return variable
|
||||
return variable[1:]
|
||||
|
||||
def _remove_last(self,variable):
|
||||
if not isinstance(variable,list):
|
||||
def _remove_last(self, variable):
|
||||
if not isinstance(variable, list):
|
||||
return "ERROR:VARIABLE_NOT_LIST"
|
||||
if len(variable)==0:
|
||||
if len(variable) == 0:
|
||||
return variable
|
||||
return variable[:-1]
|
||||
|
||||
@@ -163,32 +161,32 @@ class VariableAssigner(ComponentBase,ABC):
|
||||
return False
|
||||
return isinstance(value, numbers.Number)
|
||||
|
||||
def _add(self,variable,parameter):
|
||||
def _add(self, variable, parameter):
|
||||
if self.is_number(variable) and self.is_number(parameter):
|
||||
return variable + parameter
|
||||
else:
|
||||
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
|
||||
|
||||
def _subtract(self,variable,parameter):
|
||||
def _subtract(self, variable, parameter):
|
||||
if self.is_number(variable) and self.is_number(parameter):
|
||||
return variable - parameter
|
||||
else:
|
||||
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
|
||||
|
||||
def _multiply(self,variable,parameter):
|
||||
def _multiply(self, variable, parameter):
|
||||
if self.is_number(variable) and self.is_number(parameter):
|
||||
return variable * parameter
|
||||
else:
|
||||
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
|
||||
|
||||
def _divide(self,variable,parameter):
|
||||
def _divide(self, variable, parameter):
|
||||
if self.is_number(variable) and self.is_number(parameter):
|
||||
if parameter==0:
|
||||
if parameter == 0:
|
||||
return "ERROR:DIVIDE_BY_ZERO"
|
||||
else:
|
||||
return variable/parameter
|
||||
return variable / parameter
|
||||
else:
|
||||
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
|
||||
return "ERROR:VARIABLE_NOT_NUMBER or PARAMETER_NOT_NUMBER"
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Assign variables from canvas."
|
||||
return "Assign variables from canvas."
|
||||
|
||||
+5
-19
@@ -56,7 +56,7 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
|
||||
for old_name, new_name in COMPONENT_RENAMES.items():
|
||||
prefix = f"{old_name}:"
|
||||
if component_id.startswith(prefix):
|
||||
new_component_id = f"{new_name}:{component_id[len(prefix):]}"
|
||||
new_component_id = f"{new_name}:{component_id[len(prefix) :]}"
|
||||
break
|
||||
component_id_map[component_id] = new_component_id
|
||||
|
||||
@@ -66,12 +66,7 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
|
||||
|
||||
def repl(match: re.Match[str]) -> str:
|
||||
component_id = match.group(2)
|
||||
return (
|
||||
match.group(1)
|
||||
+ component_id_map.get(component_id, component_id)
|
||||
+ match.group(3)
|
||||
+ match.group(4)
|
||||
)
|
||||
return match.group(1) + component_id_map.get(component_id, component_id) + match.group(3) + match.group(4)
|
||||
|
||||
return VARIABLE_REF_PATTERN.sub(repl, text)
|
||||
|
||||
@@ -96,15 +91,9 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
|
||||
obj["component_name"] = COMPONENT_RENAMES.get(component_name, component_name)
|
||||
|
||||
if isinstance(new_component.get("downstream"), list):
|
||||
new_component["downstream"] = [
|
||||
component_id_map.get(component_id, component_id)
|
||||
for component_id in new_component["downstream"]
|
||||
]
|
||||
new_component["downstream"] = [component_id_map.get(component_id, component_id) for component_id in new_component["downstream"]]
|
||||
if isinstance(new_component.get("upstream"), list):
|
||||
new_component["upstream"] = [
|
||||
component_id_map.get(component_id, component_id)
|
||||
for component_id in new_component["upstream"]
|
||||
]
|
||||
new_component["upstream"] = [component_id_map.get(component_id, component_id) for component_id in new_component["upstream"]]
|
||||
|
||||
parent_id = new_component.get("parent_id")
|
||||
if isinstance(parent_id, str):
|
||||
@@ -115,10 +104,7 @@ def normalize_chunker_dsl(dsl: dict) -> dict:
|
||||
normalized["components"] = rewritten_components
|
||||
|
||||
if isinstance(normalized.get("path"), list):
|
||||
normalized["path"] = [
|
||||
component_id_map.get(component_id, component_id)
|
||||
for component_id in normalized["path"]
|
||||
]
|
||||
normalized["path"] = [component_id_map.get(component_id, component_id) for component_id in normalized["path"]]
|
||||
|
||||
graph = normalized.get("graph")
|
||||
if isinstance(graph, dict):
|
||||
|
||||
@@ -1 +1 @@
|
||||
PLUGIN_TYPE_LLM_TOOLS = "llm_tools"
|
||||
PLUGIN_TYPE_LLM_TOOLS = "llm_tools"
|
||||
|
||||
@@ -7,6 +7,7 @@ class BadCalculatorPlugin(LLMToolPlugin):
|
||||
A sample LLM tool plugin, will add two numbers with 100.
|
||||
It only presents for demo purpose. Do not use it in production.
|
||||
"""
|
||||
|
||||
_version_ = "1.0.0"
|
||||
|
||||
@classmethod
|
||||
@@ -17,19 +18,9 @@ class BadCalculatorPlugin(LLMToolPlugin):
|
||||
"description": "A tool to calculate the sum of two numbers (will give wrong answer)",
|
||||
"displayDescription": "$t:bad_calculator.description",
|
||||
"parameters": {
|
||||
"a": {
|
||||
"type": "number",
|
||||
"description": "The first number",
|
||||
"displayDescription": "$t:bad_calculator.params.a",
|
||||
"required": True
|
||||
},
|
||||
"b": {
|
||||
"type": "number",
|
||||
"description": "The second number",
|
||||
"displayDescription": "$t:bad_calculator.params.b",
|
||||
"required": True
|
||||
}
|
||||
}
|
||||
"a": {"type": "number", "description": "The first number", "displayDescription": "$t:bad_calculator.params.a", "required": True},
|
||||
"b": {"type": "number", "description": "The second number", "displayDescription": "$t:bad_calculator.params.b", "required": True},
|
||||
},
|
||||
}
|
||||
|
||||
def invoke(self, a: int, b: int) -> str:
|
||||
|
||||
@@ -38,14 +38,8 @@ def llm_tool_metadata_to_openai_tool(llm_tool_metadata: LLMToolMetadata) -> dict
|
||||
"description": llm_tool_metadata["description"],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
k: {
|
||||
"type": p["type"],
|
||||
"description": p["description"]
|
||||
}
|
||||
for k, p in llm_tool_metadata["parameters"].items()
|
||||
},
|
||||
"required": [k for k, p in llm_tool_metadata["parameters"].items() if p["required"]]
|
||||
}
|
||||
}
|
||||
"properties": {k: {"type": p["type"], "description": p["description"]} for k, p in llm_tool_metadata["parameters"].items()},
|
||||
"required": [k for k, p in llm_tool_metadata["parameters"].items() if p["required"]],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,10 +15,8 @@ class PluginManager:
|
||||
self._llm_tool_plugins = {}
|
||||
|
||||
def load_plugins(self) -> None:
|
||||
loader = pluginlib.PluginLoader(
|
||||
paths=[str(Path(os.path.dirname(__file__), "embedded_plugins"))]
|
||||
)
|
||||
|
||||
loader = pluginlib.PluginLoader(paths=[str(Path(os.path.dirname(__file__), "embedded_plugins"))])
|
||||
|
||||
for type, plugins in loader.plugins.items():
|
||||
for name, plugin in plugins.items():
|
||||
logging.info(f"Loaded {type} plugin {name} version {plugin.version}")
|
||||
|
||||
+6
-16
@@ -111,7 +111,10 @@ def _load_provider_from_settings() -> None:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load sandbox provider from settings: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def _load_provider_config_from_settings(provider_type: str) -> Dict[str, Any]:
|
||||
provider_config_settings = SystemSettingsService.get_by_name(f"sandbox.{provider_type}")
|
||||
if not provider_config_settings:
|
||||
@@ -147,12 +150,7 @@ def reload_provider() -> None:
|
||||
_load_provider_from_settings()
|
||||
|
||||
|
||||
def execute_code(
|
||||
code: str,
|
||||
language: str = "python",
|
||||
timeout: int = 30,
|
||||
arguments: Optional[Dict[str, Any]] = None
|
||||
) -> ExecutionResult:
|
||||
def execute_code(code: str, language: str = "python", timeout: int = 30, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the configured sandbox.
|
||||
|
||||
@@ -173,9 +171,7 @@ def execute_code(
|
||||
provider_manager = get_provider_manager()
|
||||
|
||||
if not provider_manager.is_configured():
|
||||
raise RuntimeError(
|
||||
"No sandbox provider configured. Please configure sandbox settings in the admin panel."
|
||||
)
|
||||
raise RuntimeError("No sandbox provider configured. Please configure sandbox settings in the admin panel.")
|
||||
|
||||
provider = provider_manager.get_provider()
|
||||
provider_name = provider_manager.get_provider_name() or getattr(provider, "__class__", type(provider)).__name__
|
||||
@@ -192,13 +188,7 @@ def execute_code(
|
||||
|
||||
try:
|
||||
# Execute the code
|
||||
result = provider.execute_code(
|
||||
instance_id=instance.instance_id,
|
||||
code=code,
|
||||
language=language,
|
||||
timeout=timeout,
|
||||
arguments=arguments
|
||||
)
|
||||
result = provider.execute_code(instance_id=instance.instance_id, code=code, language=language, timeout=timeout, arguments=arguments)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -22,4 +22,3 @@ router = APIRouter()
|
||||
router.get("/")(healthz_handler)
|
||||
router.get("/healthz")(healthz_handler)
|
||||
router.post("/run")(run_code_handler)
|
||||
|
||||
|
||||
@@ -230,9 +230,7 @@ async def execute_code(req: CodeExecutionRequest):
|
||||
if returncode != 0:
|
||||
raise RuntimeError(f"Directory creation failed: {stderr}")
|
||||
|
||||
tar_proc = await asyncio.create_subprocess_exec(
|
||||
"tar", "czf", "-", "-C", workdir, code_name, runner_name, str(bundle["args_name"]), stdout=asyncio.subprocess.PIPE
|
||||
)
|
||||
tar_proc = await asyncio.create_subprocess_exec("tar", "czf", "-", "-C", workdir, code_name, runner_name, str(bundle["args_name"]), stdout=asyncio.subprocess.PIPE)
|
||||
tar_stdout, _ = await tar_proc.communicate()
|
||||
|
||||
docker_proc = await asyncio.create_subprocess_exec(
|
||||
@@ -334,8 +332,16 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
|
||||
|
||||
# List files in the artifacts directory inside the container
|
||||
returncode, stdout, _ = await async_run_command(
|
||||
"docker", "exec", container, "find", artifacts_path,
|
||||
"-maxdepth", "1", "-type", "f", timeout=5,
|
||||
"docker",
|
||||
"exec",
|
||||
container,
|
||||
"find",
|
||||
artifacts_path,
|
||||
"-maxdepth",
|
||||
"1",
|
||||
"-type",
|
||||
"f",
|
||||
timeout=5,
|
||||
)
|
||||
if returncode != 0 or not stdout.strip():
|
||||
return []
|
||||
@@ -359,7 +365,14 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
|
||||
|
||||
# Check file size inside the container
|
||||
returncode, size_str, _ = await async_run_command(
|
||||
"docker", "exec", container, "stat", "-c", "%s", file_path, timeout=5,
|
||||
"docker",
|
||||
"exec",
|
||||
container,
|
||||
"stat",
|
||||
"-c",
|
||||
"%s",
|
||||
file_path,
|
||||
timeout=5,
|
||||
)
|
||||
if returncode != 0:
|
||||
logger.warning(f"Failed to stat artifact {fname}")
|
||||
@@ -374,7 +387,12 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
|
||||
|
||||
# Read file content via docker exec (docker cp doesn't work with gVisor tmpfs)
|
||||
returncode, content_b64, stderr = await async_run_command(
|
||||
"docker", "exec", container, "base64", file_path, timeout=30,
|
||||
"docker",
|
||||
"exec",
|
||||
container,
|
||||
"base64",
|
||||
file_path,
|
||||
timeout=30,
|
||||
)
|
||||
if returncode != 0:
|
||||
logger.warning(f"Failed to read artifact {fname}: {stderr}")
|
||||
@@ -382,12 +400,14 @@ async def _collect_artifacts(container: str, task_id: str, host_workdir: str) ->
|
||||
|
||||
content_b64 = content_b64.replace("\n", "").strip()
|
||||
|
||||
items.append(ArtifactItem(
|
||||
name=fname,
|
||||
mime_type=mime_type,
|
||||
size=file_size,
|
||||
content_b64=content_b64,
|
||||
))
|
||||
items.append(
|
||||
ArtifactItem(
|
||||
name=fname,
|
||||
mime_type=mime_type,
|
||||
size=file_size,
|
||||
content_b64=content_b64,
|
||||
)
|
||||
)
|
||||
logger.info(f"Collected artifact: {fname} ({file_size} bytes, {mime_type})")
|
||||
|
||||
return items
|
||||
|
||||
@@ -232,11 +232,7 @@ class AliyunCodeInterpreterProvider(SandboxProvider):
|
||||
# Wrap code to call main() function
|
||||
# Matches self_managed provider behavior: call main(**arguments)
|
||||
args_json = json.dumps(arguments or {})
|
||||
wrapped_code = (
|
||||
build_python_wrapper(code, args_json)
|
||||
if normalized_lang == "python"
|
||||
else build_javascript_wrapper(code, args_json)
|
||||
)
|
||||
wrapped_code = build_python_wrapper(code, args_json) if normalized_lang == "python" else build_javascript_wrapper(code, args_json)
|
||||
logger.debug(f"Aliyun Code Interpreter: Wrapped code (first 200 chars): {wrapped_code[:200]}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
@@ -33,6 +33,7 @@ class SandboxProviderConfigError(Exception):
|
||||
@dataclass
|
||||
class SandboxInstance:
|
||||
"""Represents a sandbox execution instance"""
|
||||
|
||||
instance_id: str
|
||||
provider: str
|
||||
status: str # running, stopped, error
|
||||
@@ -46,6 +47,7 @@ class SandboxInstance:
|
||||
@dataclass
|
||||
class ExecutionResult:
|
||||
"""Result of code execution in a sandbox"""
|
||||
|
||||
stdout: str
|
||||
stderr: str
|
||||
exit_code: int
|
||||
@@ -96,14 +98,7 @@ class SandboxProvider(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute_code(
|
||||
self,
|
||||
instance_id: str,
|
||||
code: str,
|
||||
language: str,
|
||||
timeout: int = 10,
|
||||
arguments: Optional[Dict[str, Any]] = None
|
||||
) -> ExecutionResult:
|
||||
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in a sandbox instance.
|
||||
|
||||
|
||||
@@ -97,16 +97,10 @@ class E2BProvider(SandboxProvider):
|
||||
metadata={
|
||||
"language": language,
|
||||
"region": self.region,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def execute_code(
|
||||
self,
|
||||
instance_id: str,
|
||||
code: str,
|
||||
language: str,
|
||||
timeout: int = 10
|
||||
) -> ExecutionResult:
|
||||
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the E2B instance.
|
||||
|
||||
@@ -130,9 +124,7 @@ class E2BProvider(SandboxProvider):
|
||||
# POST /sandbox/{sandboxID}/execute
|
||||
|
||||
raise RuntimeError(
|
||||
"E2B provider is not yet fully implemented. "
|
||||
"Please use the self-managed provider or implement the E2B API integration. "
|
||||
"See https://github.com/e2b-dev/e2b for API documentation."
|
||||
"E2B provider is not yet fully implemented. Please use the self-managed provider or implement the E2B API integration. See https://github.com/e2b-dev/e2b for API documentation."
|
||||
)
|
||||
|
||||
def destroy_instance(self, instance_id: str) -> bool:
|
||||
@@ -208,7 +200,7 @@ class E2BProvider(SandboxProvider):
|
||||
"min": 5,
|
||||
"max": 300,
|
||||
"description": "API request timeout for code execution",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def _normalize_language(self, language: str) -> str:
|
||||
|
||||
@@ -49,6 +49,8 @@ LOCAL_PYTHON_THREAD_ENV_VARS = (
|
||||
"BLIS_NUM_THREADS",
|
||||
"VECLIB_MAXIMUM_THREADS",
|
||||
)
|
||||
|
||||
|
||||
class LocalProvider(SandboxProvider):
|
||||
"""
|
||||
Execute code as a local child process.
|
||||
|
||||
@@ -108,17 +108,10 @@ class SelfManagedProvider(SandboxProvider):
|
||||
"language": language,
|
||||
"endpoint": self.endpoint,
|
||||
"pool_size": self.pool_size,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def execute_code(
|
||||
self,
|
||||
instance_id: str,
|
||||
code: str,
|
||||
language: str,
|
||||
timeout: int = 10,
|
||||
arguments: Optional[Dict[str, Any]] = None
|
||||
) -> ExecutionResult:
|
||||
def execute_code(self, instance_id: str, code: str, language: str, timeout: int = 10, arguments: Optional[Dict[str, Any]] = None) -> ExecutionResult:
|
||||
"""
|
||||
Execute code in the sandbox.
|
||||
|
||||
@@ -144,11 +137,7 @@ class SelfManagedProvider(SandboxProvider):
|
||||
|
||||
# Prepare request
|
||||
code_b64 = base64.b64encode(code.encode("utf-8")).decode("utf-8")
|
||||
payload = {
|
||||
"code_b64": code_b64,
|
||||
"language": normalized_lang,
|
||||
"arguments": arguments or {}
|
||||
}
|
||||
payload = {"code_b64": code_b64, "language": normalized_lang, "arguments": arguments or {}}
|
||||
|
||||
url = f"{self.endpoint}/run"
|
||||
exec_timeout = timeout or self.timeout
|
||||
@@ -156,19 +145,12 @@ class SelfManagedProvider(SandboxProvider):
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
timeout=exec_timeout,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
response = requests.post(url, json=payload, timeout=exec_timeout, headers={"Content-Type": "application/json"})
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"HTTP {response.status_code}: {response.text}"
|
||||
)
|
||||
raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
structured_result = result.get("result") or {}
|
||||
@@ -188,14 +170,12 @@ class SelfManagedProvider(SandboxProvider):
|
||||
"result_present": structured_result.get("present", False),
|
||||
"result_value": structured_result.get("value"),
|
||||
"result_type": structured_result.get("type"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
except requests.Timeout:
|
||||
execution_time = time.time() - start_time
|
||||
raise TimeoutError(
|
||||
f"Execution timed out after {exec_timeout} seconds"
|
||||
)
|
||||
raise TimeoutError(f"Execution timed out after {exec_timeout} seconds")
|
||||
|
||||
except requests.RequestException as e:
|
||||
raise RuntimeError(f"HTTP request failed: {str(e)}")
|
||||
@@ -388,7 +368,8 @@ class SelfManagedProvider(SandboxProvider):
|
||||
if endpoint:
|
||||
# Check if it's a valid HTTP/HTTPS URL or localhost
|
||||
import re
|
||||
url_pattern = r'^(https?://|http://localhost|http://[\d\.]+:[a-z]+:[/]|http://[\w\.]+:)'
|
||||
|
||||
url_pattern = r"^(https?://|http://localhost|http://[\d\.]+:[a-z]+:[/]|http://[\w\.]+:)"
|
||||
if not re.match(url_pattern, endpoint):
|
||||
return False, f"Invalid endpoint format: {endpoint}. Must start with http:// or https://"
|
||||
|
||||
|
||||
@@ -135,9 +135,7 @@ class SSHProvider(SandboxProvider):
|
||||
timeout=min(self.timeout, 10),
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f"Failed to create remote artifacts directory: {stderr or stdout or 'unknown error'}"
|
||||
)
|
||||
raise RuntimeError(f"Failed to create remote artifacts directory: {stderr or stdout or 'unknown error'}")
|
||||
except Exception:
|
||||
sftp.close()
|
||||
client.close()
|
||||
@@ -211,9 +209,7 @@ class SSHProvider(SandboxProvider):
|
||||
"status": "ok" if exit_code == 0 else "error",
|
||||
"timeout": exec_timeout,
|
||||
"command": command,
|
||||
"artifacts": self._collect_artifacts(
|
||||
sftp, posixpath.join(remote_work_dir, "artifacts")
|
||||
),
|
||||
"artifacts": self._collect_artifacts(sftp, posixpath.join(remote_work_dir, "artifacts")),
|
||||
"result_present": structured_result.get("present", False),
|
||||
"result_value": structured_result.get("value"),
|
||||
"result_type": structured_result.get("type"),
|
||||
@@ -269,18 +265,13 @@ class SSHProvider(SandboxProvider):
|
||||
timeout=min(self.timeout, 10),
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise SandboxProviderConfigError(
|
||||
f"SSH connectivity check failed on {self.username}@{self.host}:{self.port}: "
|
||||
f"{stderr or 'remote command returned non-zero exit status'}"
|
||||
)
|
||||
raise SandboxProviderConfigError(f"SSH connectivity check failed on {self.username}@{self.host}:{self.port}: {stderr or 'remote command returned non-zero exit status'}")
|
||||
finally:
|
||||
client.close()
|
||||
except SandboxProviderConfigError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise SandboxProviderConfigError(
|
||||
f"Failed to connect to SSH host {self.username}@{self.host}:{self.port}: {exc}"
|
||||
) from exc
|
||||
raise SandboxProviderConfigError(f"Failed to connect to SSH host {self.username}@{self.host}:{self.port}: {exc}") from exc
|
||||
|
||||
def get_supported_languages(self) -> List[str]:
|
||||
return ["python", "javascript", "nodejs"]
|
||||
@@ -470,9 +461,7 @@ class SSHProvider(SandboxProvider):
|
||||
# Match the Go provider's fail-closed posture (see
|
||||
# internal/agent/sandbox/ssh.go::hostKeyCallback).
|
||||
logging.warning("SSH: failed to load configured known_hosts file; refusing connection")
|
||||
raise SandboxProviderConfigError(
|
||||
"Failed to load configured SSH known_hosts file."
|
||||
) from exc
|
||||
raise SandboxProviderConfigError("Failed to load configured SSH known_hosts file.") from exc
|
||||
# Reject unknown hosts: this is the default fail-closed posture
|
||||
# to prevent silent MITM. Operators must either ship a populated
|
||||
# known_hosts file or accept the warning (paramiko will fail the
|
||||
@@ -522,9 +511,7 @@ class SSHProvider(SandboxProvider):
|
||||
except Exception as exc:
|
||||
errors.append(str(exc))
|
||||
|
||||
raise SandboxProviderConfigError(
|
||||
"Failed to load SSH private key. " + "; ".join(error for error in errors if error)
|
||||
)
|
||||
raise SandboxProviderConfigError("Failed to load SSH private key. " + "; ".join(error for error in errors if error))
|
||||
|
||||
def _create_remote_workspace(self, client: paramiko.SSHClient) -> str:
|
||||
base_dir = self.work_dir.rstrip("/") or "/tmp"
|
||||
@@ -535,9 +522,7 @@ class SSHProvider(SandboxProvider):
|
||||
timeout=min(self.timeout, 10),
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise RuntimeError(
|
||||
f"Failed to create remote workspace on {self.host}: {stderr or stdout or 'unknown error'}"
|
||||
)
|
||||
raise RuntimeError(f"Failed to create remote workspace on {self.host}: {stderr or stdout or 'unknown error'}")
|
||||
|
||||
remote_work_dir = stdout.strip().splitlines()[-1] if stdout.strip() else ""
|
||||
if not remote_work_dir:
|
||||
@@ -577,10 +562,7 @@ class SSHProvider(SandboxProvider):
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported language for SSH provider: {language}")
|
||||
|
||||
return (
|
||||
f"cd {shlex.quote(remote_work_dir)} && "
|
||||
f"{shlex.quote(executable)} {shlex.quote(remote_script_path)}"
|
||||
)
|
||||
return f"cd {shlex.quote(remote_work_dir)} && {shlex.quote(executable)} {shlex.quote(remote_script_path)}"
|
||||
|
||||
def _run_remote_command(
|
||||
self,
|
||||
@@ -700,7 +682,5 @@ def _get_paramiko_module():
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError as exc:
|
||||
raise SandboxProviderConfigError(
|
||||
"paramiko is required for the SSH sandbox provider. Install the project dependencies to enable it."
|
||||
) from exc
|
||||
raise SandboxProviderConfigError("paramiko is required for the SSH sandbox provider. Install the project dependencies to enable it.") from exc
|
||||
return paramiko
|
||||
|
||||
@@ -36,7 +36,7 @@ if __name__ == "__main__":
|
||||
|
||||
|
||||
def build_javascript_wrapper(code: str, args_json: str) -> str:
|
||||
return f'''{code}
|
||||
return f"""{code}
|
||||
|
||||
const __ragflowArgs = {args_json};
|
||||
|
||||
@@ -55,7 +55,7 @@ const __ragflowArgs = {args_json};
|
||||
}}
|
||||
console.log('{RESULT_MARKER_PREFIX}' + Buffer.from(payload, 'utf8').toString('base64'));
|
||||
}})();
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
def extract_structured_result(stdout: str) -> tuple[str, dict[str, Any]]:
|
||||
|
||||
@@ -151,7 +151,7 @@ class TestAliyunCodeInterpreterIntegration:
|
||||
""",
|
||||
language="python",
|
||||
timeout=30,
|
||||
arguments={"name": "World", "count": 2}
|
||||
arguments={"name": "World", "count": 2},
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
@@ -211,7 +211,7 @@ class TestAliyunCodeInterpreterIntegration:
|
||||
}""",
|
||||
language="javascript",
|
||||
timeout=30,
|
||||
arguments={"name": "World", "count": 2}
|
||||
arguments={"name": "World", "count": 2},
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
@@ -32,12 +32,7 @@ class TestSandboxDataclasses:
|
||||
|
||||
def test_sandbox_instance_creation(self):
|
||||
"""Test SandboxInstance dataclass creation."""
|
||||
instance = SandboxInstance(
|
||||
instance_id="test-123",
|
||||
provider="self_managed",
|
||||
status="running",
|
||||
metadata={"language": "python"}
|
||||
)
|
||||
instance = SandboxInstance(instance_id="test-123", provider="self_managed", status="running", metadata={"language": "python"})
|
||||
|
||||
assert instance.instance_id == "test-123"
|
||||
assert instance.provider == "self_managed"
|
||||
@@ -46,24 +41,13 @@ class TestSandboxDataclasses:
|
||||
|
||||
def test_sandbox_instance_default_metadata(self):
|
||||
"""Test SandboxInstance with None metadata."""
|
||||
instance = SandboxInstance(
|
||||
instance_id="test-123",
|
||||
provider="self_managed",
|
||||
status="running",
|
||||
metadata=None
|
||||
)
|
||||
instance = SandboxInstance(instance_id="test-123", provider="self_managed", status="running", metadata=None)
|
||||
|
||||
assert instance.metadata == {}
|
||||
|
||||
def test_execution_result_creation(self):
|
||||
"""Test ExecutionResult dataclass creation."""
|
||||
result = ExecutionResult(
|
||||
stdout="Hello, World!",
|
||||
stderr="",
|
||||
exit_code=0,
|
||||
execution_time=1.5,
|
||||
metadata={"status": "success"}
|
||||
)
|
||||
result = ExecutionResult(stdout="Hello, World!", stderr="", exit_code=0, execution_time=1.5, metadata={"status": "success"})
|
||||
|
||||
assert result.stdout == "Hello, World!"
|
||||
assert result.stderr == ""
|
||||
@@ -73,13 +57,7 @@ class TestSandboxDataclasses:
|
||||
|
||||
def test_execution_result_default_metadata(self):
|
||||
"""Test ExecutionResult with None metadata."""
|
||||
result = ExecutionResult(
|
||||
stdout="output",
|
||||
stderr="error",
|
||||
exit_code=1,
|
||||
execution_time=0.5,
|
||||
metadata=None
|
||||
)
|
||||
result = ExecutionResult(stdout="output", stderr="error", exit_code=1, execution_time=0.5, metadata=None)
|
||||
|
||||
assert result.metadata == {}
|
||||
|
||||
@@ -145,7 +123,7 @@ class TestSelfManagedProvider:
|
||||
assert provider.pool_size == 10
|
||||
assert not provider._initialized
|
||||
|
||||
@patch('requests.get')
|
||||
@patch("requests.get")
|
||||
def test_initialize_success(self, mock_get):
|
||||
"""Test successful initialization."""
|
||||
mock_response = Mock()
|
||||
@@ -153,12 +131,7 @@ class TestSelfManagedProvider:
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
result = provider.initialize({
|
||||
"endpoint": "http://test-endpoint:9385",
|
||||
"timeout": 60,
|
||||
"max_retries": 5,
|
||||
"pool_size": 20
|
||||
})
|
||||
result = provider.initialize({"endpoint": "http://test-endpoint:9385", "timeout": 60, "max_retries": 5, "pool_size": 20})
|
||||
|
||||
assert result is True
|
||||
assert provider.endpoint == "http://test-endpoint:9385"
|
||||
@@ -168,7 +141,7 @@ class TestSelfManagedProvider:
|
||||
assert provider._initialized
|
||||
mock_get.assert_called_once_with("http://test-endpoint:9385/healthz", timeout=5)
|
||||
|
||||
@patch('requests.get')
|
||||
@patch("requests.get")
|
||||
def test_initialize_failure(self, mock_get):
|
||||
"""Test initialization failure."""
|
||||
mock_get.side_effect = Exception("Connection error")
|
||||
@@ -181,7 +154,7 @@ class TestSelfManagedProvider:
|
||||
|
||||
def test_initialize_default_config(self):
|
||||
"""Test initialization with default config."""
|
||||
with patch('requests.get') as mock_get:
|
||||
with patch("requests.get") as mock_get:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_get.return_value = mock_response
|
||||
@@ -222,30 +195,18 @@ class TestSelfManagedProvider:
|
||||
with pytest.raises(RuntimeError, match="Provider not initialized"):
|
||||
provider.create_instance("python")
|
||||
|
||||
@patch('requests.post')
|
||||
@patch("requests.post")
|
||||
def test_execute_code_success(self, mock_post):
|
||||
"""Test successful code execution."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"status": "success",
|
||||
"stdout": '{"result": 42}',
|
||||
"stderr": "",
|
||||
"exit_code": 0,
|
||||
"time_used_ms": 100.0,
|
||||
"memory_used_kb": 1024.0
|
||||
}
|
||||
mock_response.json.return_value = {"status": "success", "stdout": '{"result": 42}', "stderr": "", "exit_code": 0, "time_used_ms": 100.0, "memory_used_kb": 1024.0}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
result = provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="def main(): return {'result': 42}",
|
||||
language="python",
|
||||
timeout=10
|
||||
)
|
||||
result = provider.execute_code(instance_id="test-123", code="def main(): return {'result': 42}", language="python", timeout=10)
|
||||
|
||||
assert result.stdout == '{"result": 42}'
|
||||
assert result.stderr == ""
|
||||
@@ -254,7 +215,7 @@ class TestSelfManagedProvider:
|
||||
assert result.metadata["status"] == "success"
|
||||
assert result.metadata["instance_id"] == "test-123"
|
||||
|
||||
@patch('requests.post')
|
||||
@patch("requests.post")
|
||||
def test_execute_code_maps_structured_result_into_metadata(self, mock_post):
|
||||
"""Test successful code execution with structured result envelope."""
|
||||
mock_response = Mock()
|
||||
@@ -277,19 +238,14 @@ class TestSelfManagedProvider:
|
||||
provider = SelfManagedProvider()
|
||||
provider._initialized = True
|
||||
|
||||
result = provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="def main(): return {'items': ['a', 'b']}",
|
||||
language="python",
|
||||
timeout=10
|
||||
)
|
||||
result = provider.execute_code(instance_id="test-123", code="def main(): return {'items': ['a', 'b']}", language="python", timeout=10)
|
||||
|
||||
assert result.stdout == "debug line\n"
|
||||
assert result.metadata["result_present"] is True
|
||||
assert result.metadata["result_value"] == {"items": ["a", "b"]}
|
||||
assert result.metadata["result_type"] == "json"
|
||||
|
||||
@patch('requests.post')
|
||||
@patch("requests.post")
|
||||
def test_execute_code_timeout(self, mock_post):
|
||||
"""Test code execution timeout."""
|
||||
mock_post.side_effect = requests.Timeout()
|
||||
@@ -298,14 +254,9 @@ class TestSelfManagedProvider:
|
||||
provider._initialized = True
|
||||
|
||||
with pytest.raises(TimeoutError, match="Execution timed out"):
|
||||
provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="while True: pass",
|
||||
language="python",
|
||||
timeout=5
|
||||
)
|
||||
provider.execute_code(instance_id="test-123", code="while True: pass", language="python", timeout=5)
|
||||
|
||||
@patch('requests.post')
|
||||
@patch("requests.post")
|
||||
def test_execute_code_http_error(self, mock_post):
|
||||
"""Test code execution with HTTP error."""
|
||||
mock_response = Mock()
|
||||
@@ -317,22 +268,14 @@ class TestSelfManagedProvider:
|
||||
provider._initialized = True
|
||||
|
||||
with pytest.raises(RuntimeError, match="HTTP 500"):
|
||||
provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="invalid code",
|
||||
language="python"
|
||||
)
|
||||
provider.execute_code(instance_id="test-123", code="invalid code", language="python")
|
||||
|
||||
def test_execute_code_not_initialized(self):
|
||||
"""Test executing code when provider not initialized."""
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Provider not initialized"):
|
||||
provider.execute_code(
|
||||
instance_id="test-123",
|
||||
code="print('hello')",
|
||||
language="python"
|
||||
)
|
||||
provider.execute_code(instance_id="test-123", code="print('hello')", language="python")
|
||||
|
||||
def test_destroy_instance(self):
|
||||
"""Test destroying an instance (no-op for self-managed)."""
|
||||
@@ -344,7 +287,7 @@ class TestSelfManagedProvider:
|
||||
|
||||
assert result is True
|
||||
|
||||
@patch('requests.get')
|
||||
@patch("requests.get")
|
||||
def test_health_check_success(self, mock_get):
|
||||
"""Test successful health check."""
|
||||
mock_response = Mock()
|
||||
@@ -358,7 +301,7 @@ class TestSelfManagedProvider:
|
||||
assert result is True
|
||||
mock_get.assert_called_once_with("http://localhost:9385/healthz", timeout=5)
|
||||
|
||||
@patch('requests.get')
|
||||
@patch("requests.get")
|
||||
def test_health_check_failure(self, mock_get):
|
||||
"""Test health check failure."""
|
||||
mock_get.side_effect = Exception("Connection error")
|
||||
@@ -439,20 +382,20 @@ class TestProviderInterface:
|
||||
provider = SelfManagedProvider()
|
||||
|
||||
# Check all abstract methods are implemented
|
||||
assert hasattr(provider, 'initialize')
|
||||
assert hasattr(provider, "initialize")
|
||||
assert callable(provider.initialize)
|
||||
|
||||
assert hasattr(provider, 'create_instance')
|
||||
assert hasattr(provider, "create_instance")
|
||||
assert callable(provider.create_instance)
|
||||
|
||||
assert hasattr(provider, 'execute_code')
|
||||
assert hasattr(provider, "execute_code")
|
||||
assert callable(provider.execute_code)
|
||||
|
||||
assert hasattr(provider, 'destroy_instance')
|
||||
assert hasattr(provider, "destroy_instance")
|
||||
assert callable(provider.destroy_instance)
|
||||
|
||||
assert hasattr(provider, 'health_check')
|
||||
assert hasattr(provider, "health_check")
|
||||
assert callable(provider.health_check)
|
||||
|
||||
assert hasattr(provider, 'get_supported_languages')
|
||||
assert hasattr(provider, "get_supported_languages")
|
||||
assert callable(provider.get_supported_languages)
|
||||
|
||||
@@ -76,9 +76,7 @@ def test_python_builtins_import_is_rejected():
|
||||
assert is_safe is False
|
||||
# Pin the specific reason: rejection must come from the new ``builtins``
|
||||
# entry in ``DANGEROUS_IMPORTS``, not from some unrelated parse error.
|
||||
assert any("builtins" in issue for issue, _ in issues), (
|
||||
f"expected an issue mentioning 'builtins', got {issues!r}"
|
||||
)
|
||||
assert any("builtins" in issue for issue, _ in issues), f"expected an issue mentioning 'builtins', got {issues!r}"
|
||||
|
||||
|
||||
def test_python_attribute_eval_call_is_rejected():
|
||||
@@ -94,9 +92,7 @@ def test_python_attribute_eval_call_is_rejected():
|
||||
# not from the ``import builtins`` line above. We assert ``exec`` is in at
|
||||
# least one finding so the test fails if visit_Call's attribute branch is
|
||||
# ever reverted.
|
||||
assert any("exec" in issue for issue, _ in issues), (
|
||||
f"expected an issue mentioning 'exec', got {issues!r}"
|
||||
)
|
||||
assert any("exec" in issue for issue, _ in issues), f"expected an issue mentioning 'exec', got {issues!r}"
|
||||
|
||||
|
||||
def test_javascript_safe_code_still_passes():
|
||||
|
||||
@@ -36,17 +36,14 @@ print("✓ Provider has all required methods")
|
||||
print("\n[3/5] Testing SDK imports...")
|
||||
try:
|
||||
# Check if agentrun SDK is available using importlib
|
||||
if (
|
||||
importlib.util.find_spec("agentrun.sandbox") is None
|
||||
or importlib.util.find_spec("agentrun.utils.config") is None
|
||||
or importlib.util.find_spec("agentrun.utils.exception") is None
|
||||
):
|
||||
if importlib.util.find_spec("agentrun.sandbox") is None or importlib.util.find_spec("agentrun.utils.config") is None or importlib.util.find_spec("agentrun.utils.exception") is None:
|
||||
raise ImportError("agentrun SDK not found")
|
||||
|
||||
# Verify imports work (assign to _ to indicate they're intentionally unused)
|
||||
from agentrun.sandbox import CodeInterpreterSandbox, TemplateType, CodeLanguage
|
||||
from agentrun.utils.config import Config
|
||||
from agentrun.utils.exception import ServerError
|
||||
|
||||
_ = (CodeInterpreterSandbox, TemplateType, CodeLanguage, Config, ServerError)
|
||||
|
||||
print("✓ SDK modules imported successfully")
|
||||
|
||||
@@ -18,29 +18,29 @@ import os
|
||||
from agent.canvas import Canvas
|
||||
from common import settings
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
dsl_default_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"dsl_examples",
|
||||
"retrieval_and_generate.json",
|
||||
)
|
||||
parser.add_argument('-s', '--dsl', default=dsl_default_path, help="input dsl", action='store', required=True)
|
||||
parser.add_argument('-t', '--tenant_id', default=False, help="Tenant ID", action='store', required=True)
|
||||
parser.add_argument('-m', '--stream', default=False, help="Stream output", action='store_true', required=False)
|
||||
parser.add_argument("-s", "--dsl", default=dsl_default_path, help="input dsl", action="store", required=True)
|
||||
parser.add_argument("-t", "--tenant_id", default=False, help="Tenant ID", action="store", required=True)
|
||||
parser.add_argument("-m", "--stream", default=False, help="Stream output", action="store_true", required=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
settings.init_settings()
|
||||
canvas = Canvas(open(args.dsl, "r").read(), args.tenant_id)
|
||||
if canvas.get_prologue():
|
||||
print(f"==================== Bot =====================\n> {canvas.get_prologue()}", end='')
|
||||
print(f"==================== Bot =====================\n> {canvas.get_prologue()}", end="")
|
||||
query = ""
|
||||
while True:
|
||||
canvas.reset(True)
|
||||
query = input("\n==================== User =====================\n> ")
|
||||
ans = canvas.run(query=query)
|
||||
print("==================== Bot =====================\n> ", end='')
|
||||
print("==================== Bot =====================\n> ", end="")
|
||||
for ans in canvas.run(query=query):
|
||||
print(ans, end='\n', flush=True)
|
||||
print(ans, end="\n", flush=True)
|
||||
|
||||
print(canvas.path)
|
||||
|
||||
@@ -22,8 +22,9 @@ from typing import Dict, Type
|
||||
_package_path = os.path.dirname(__file__)
|
||||
__all_classes: Dict[str, Type] = {}
|
||||
|
||||
|
||||
def _import_submodules() -> None:
|
||||
for filename in os.listdir(_package_path): # noqa: F821
|
||||
for filename in os.listdir(_package_path): # noqa: F821
|
||||
if filename.startswith("__") or not filename.endswith(".py") or filename.startswith("base"):
|
||||
continue
|
||||
module_name = filename[:-3]
|
||||
@@ -34,15 +35,16 @@ def _import_submodules() -> None:
|
||||
except ImportError as e:
|
||||
print(f"Warning: Failed to import module {module_name}: {str(e)}")
|
||||
|
||||
|
||||
def _extract_classes_from_module(module: ModuleType) -> None:
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (inspect.isclass(obj) and
|
||||
obj.__module__ == module.__name__ and not name.startswith("_")):
|
||||
if inspect.isclass(obj) and obj.__module__ == module.__name__ and not name.startswith("_"):
|
||||
__all_classes[name] = obj
|
||||
globals()[name] = obj
|
||||
|
||||
|
||||
_import_submodules()
|
||||
|
||||
__all__ = list(__all_classes.keys()) + ["__all_classes"]
|
||||
|
||||
del _package_path, _import_submodules, _extract_classes_from_module
|
||||
del _package_path, _import_submodules, _extract_classes_from_module
|
||||
|
||||
+10
-25
@@ -28,7 +28,7 @@ class ArXivParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "arxiv_search",
|
||||
"description": """arXiv is a free distribution service and an open-access archive for nearly 2.4 million scholarly articles in the fields of physics, mathematics, computer science, quantitative biology, quantitative finance, statistics, electrical engineering and systems science, and economics. Materials on this site are not peer-reviewed by arXiv.""",
|
||||
"parameters": {
|
||||
@@ -36,26 +36,20 @@ class ArXivParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The search keywords to execute with arXiv. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 12
|
||||
self.sort_by = 'submittedDate'
|
||||
self.sort_by = "submittedDate"
|
||||
|
||||
def check(self):
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
self.check_valid_value(self.sort_by, "ArXiv Search Sort_by",
|
||||
['submittedDate', 'lastUpdatedDate', 'relevance'])
|
||||
self.check_valid_value(self.sort_by, "ArXiv Search Sort_by", ["submittedDate", "lastUpdatedDate", "relevance"])
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class ArXiv(ToolBase, ABC):
|
||||
@@ -71,29 +65,20 @@ class ArXiv(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("ArXiv processing"):
|
||||
return
|
||||
|
||||
try:
|
||||
sort_choices = {"relevance": arxiv.SortCriterion.Relevance,
|
||||
"lastUpdatedDate": arxiv.SortCriterion.LastUpdatedDate,
|
||||
'submittedDate': arxiv.SortCriterion.SubmittedDate}
|
||||
sort_choices = {"relevance": arxiv.SortCriterion.Relevance, "lastUpdatedDate": arxiv.SortCriterion.LastUpdatedDate, "submittedDate": arxiv.SortCriterion.SubmittedDate}
|
||||
arxiv_client = arxiv.Client()
|
||||
search = arxiv.Search(
|
||||
query=kwargs["query"],
|
||||
max_results=self._param.top_n,
|
||||
sort_by=sort_choices[self._param.sort_by]
|
||||
)
|
||||
search = arxiv.Search(query=kwargs["query"], max_results=self._param.top_n, sort_by=sort_choices[self._param.sort_by])
|
||||
results = list(arxiv_client.results(search))
|
||||
|
||||
if self.check_if_canceled("ArXiv processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(results,
|
||||
get_title=lambda r: r.title,
|
||||
get_url=lambda r: r.pdf_url,
|
||||
get_content=lambda r: r.summary)
|
||||
self._retrieve_chunks(results, get_title=lambda r: r.title, get_url=lambda r: r.pdf_url, get_content=lambda r: r.summary)
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
if self.check_if_canceled("ArXiv processing"):
|
||||
|
||||
+12
-30
@@ -28,10 +28,9 @@ from common.mcp_tool_call_conn import MCPToolBinding, MCPToolCallSession, ToolCa
|
||||
from timeit import default_timer as timer
|
||||
|
||||
|
||||
|
||||
|
||||
from common.misc_utils import thread_pool_exec
|
||||
|
||||
|
||||
class ToolParameter(TypedDict):
|
||||
type: str
|
||||
description: str
|
||||
@@ -81,7 +80,9 @@ class LLMToolPluginCallSession(ToolCallSession):
|
||||
resp = fallback_output
|
||||
else:
|
||||
resp = fallback_output
|
||||
logging.warning(f"[ToolCall] resp is None, fallback to output name={name} output_keys={list(fallback_output.keys()) if isinstance(fallback_output, dict) else type(fallback_output).__name__}")
|
||||
logging.warning(
|
||||
f"[ToolCall] resp is None, fallback to output name={name} output_keys={list(fallback_output.keys()) if isinstance(fallback_output, dict) else type(fallback_output).__name__}"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(f"[ToolCall] resp is None and output fallback failed name={name} err={e}")
|
||||
|
||||
@@ -96,28 +97,25 @@ class LLMToolPluginCallSession(ToolCallSession):
|
||||
|
||||
class ToolParamBase(ComponentParamBase):
|
||||
def __init__(self):
|
||||
#self.meta:ToolMeta = None
|
||||
# self.meta:ToolMeta = None
|
||||
super().__init__()
|
||||
self._init_inputs()
|
||||
self._init_attr_by_meta()
|
||||
|
||||
def _init_inputs(self):
|
||||
self.inputs = {}
|
||||
for k,p in self.meta["parameters"].items():
|
||||
for k, p in self.meta["parameters"].items():
|
||||
self.inputs[k] = deepcopy(p)
|
||||
|
||||
def _init_attr_by_meta(self):
|
||||
for k,p in self.meta["parameters"].items():
|
||||
for k, p in self.meta["parameters"].items():
|
||||
if not hasattr(self, k):
|
||||
setattr(self, k, p.get("default"))
|
||||
|
||||
def get_meta(self):
|
||||
params = {}
|
||||
for k, p in self.meta["parameters"].items():
|
||||
params[k] = {
|
||||
"type": p["type"],
|
||||
"description": p["description"]
|
||||
}
|
||||
params[k] = {"type": p["type"], "description": p["description"]}
|
||||
if "enum" in p:
|
||||
params[k]["enum"] = p["enum"]
|
||||
|
||||
@@ -129,12 +127,8 @@ class ToolParamBase(ComponentParamBase):
|
||||
"function": {
|
||||
"name": function_name,
|
||||
"description": desc,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": params,
|
||||
"required": [k for k, p in self.meta["parameters"].items() if p["required"]]
|
||||
}
|
||||
}
|
||||
"parameters": {"type": "object", "properties": params, "required": [k for k, p in self.meta["parameters"].items() if p["required"]]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -209,20 +203,8 @@ class ToolBase(ComponentBase):
|
||||
title = get_title(r)
|
||||
url = get_url(r)
|
||||
score = get_score(r) if get_score else 1
|
||||
chunks.append({
|
||||
"chunk_id": id,
|
||||
"content": content,
|
||||
"doc_id": id,
|
||||
"docnm_kwd": title,
|
||||
"similarity": score,
|
||||
"url": url
|
||||
})
|
||||
aggs.append({
|
||||
"doc_name": title,
|
||||
"doc_id": id,
|
||||
"count": 1,
|
||||
"url": url
|
||||
})
|
||||
chunks.append({"chunk_id": id, "content": content, "doc_id": id, "docnm_kwd": title, "similarity": score, "url": url})
|
||||
aggs.append({"doc_name": title, "doc_id": id, "count": 1, "url": url})
|
||||
self._canvas.add_reference(chunks, aggs)
|
||||
self.set_output("formalized_content", "\n".join(kb_prompt({"chunks": chunks, "doc_aggs": aggs}, 200000, True)))
|
||||
|
||||
|
||||
+46
-12
@@ -27,18 +27,53 @@ class DeepLParam(ComponentParamBase):
|
||||
super().__init__()
|
||||
self.auth_key = "xxx"
|
||||
self.parameters = []
|
||||
self.source_lang = 'ZH'
|
||||
self.target_lang = 'EN-GB'
|
||||
self.source_lang = "ZH"
|
||||
self.target_lang = "EN-GB"
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.source_lang, "Source language",
|
||||
['AR', 'BG', 'CS', 'DA', 'DE', 'EL', 'EN', 'ES', 'ET', 'FI', 'FR', 'HU', 'ID', 'IT',
|
||||
'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT', 'RO', 'RU', 'SK', 'SL', 'SV', 'TR',
|
||||
'UK', 'ZH'])
|
||||
self.check_valid_value(self.target_lang, "Target language",
|
||||
['AR', 'BG', 'CS', 'DA', 'DE', 'EL', 'EN-GB', 'EN-US', 'ES', 'ET', 'FI', 'FR', 'HU',
|
||||
'ID', 'IT', 'JA', 'KO', 'LT', 'LV', 'NB', 'NL', 'PL', 'PT-BR', 'PT-PT', 'RO', 'RU',
|
||||
'SK', 'SL', 'SV', 'TR', 'UK', 'ZH'])
|
||||
self.check_valid_value(
|
||||
self.source_lang,
|
||||
"Source language",
|
||||
["AR", "BG", "CS", "DA", "DE", "EL", "EN", "ES", "ET", "FI", "FR", "HU", "ID", "IT", "JA", "KO", "LT", "LV", "NB", "NL", "PL", "PT", "RO", "RU", "SK", "SL", "SV", "TR", "UK", "ZH"],
|
||||
)
|
||||
self.check_valid_value(
|
||||
self.target_lang,
|
||||
"Target language",
|
||||
[
|
||||
"AR",
|
||||
"BG",
|
||||
"CS",
|
||||
"DA",
|
||||
"DE",
|
||||
"EL",
|
||||
"EN-GB",
|
||||
"EN-US",
|
||||
"ES",
|
||||
"ET",
|
||||
"FI",
|
||||
"FR",
|
||||
"HU",
|
||||
"ID",
|
||||
"IT",
|
||||
"JA",
|
||||
"KO",
|
||||
"LT",
|
||||
"LV",
|
||||
"NB",
|
||||
"NL",
|
||||
"PL",
|
||||
"PT-BR",
|
||||
"PT-PT",
|
||||
"RO",
|
||||
"RU",
|
||||
"SK",
|
||||
"SL",
|
||||
"SV",
|
||||
"TR",
|
||||
"UK",
|
||||
"ZH",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class DeepL(ComponentBase, ABC):
|
||||
@@ -57,8 +92,7 @@ class DeepL(ComponentBase, ABC):
|
||||
|
||||
try:
|
||||
translator = deepl.Translator(self._param.auth_key)
|
||||
result = translator.translate_text(ans, source_lang=self._param.source_lang,
|
||||
target_lang=self._param.target_lang)
|
||||
result = translator.translate_text(ans, source_lang=self._param.source_lang, target_lang=self._param.target_lang)
|
||||
|
||||
return DeepL.be_output(result.text)
|
||||
except Exception as e:
|
||||
|
||||
@@ -28,7 +28,7 @@ class DuckDuckGoParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "duckduckgo_search",
|
||||
"description": "DuckDuckGo is a search engine focused on privacy. It offers search capabilities for web pages, images, and provides translation services. DuckDuckGo also features a private AI chat interface, providing users with an AI assistant that prioritizes data protection.",
|
||||
"parameters": {
|
||||
@@ -36,7 +36,7 @@ class DuckDuckGoParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The search keywords to execute with DuckDuckGo. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
},
|
||||
"channel": {
|
||||
"type": "string",
|
||||
@@ -45,7 +45,7 @@ class DuckDuckGoParam(ToolParamBase):
|
||||
"default": "general",
|
||||
"required": False,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 10
|
||||
@@ -56,18 +56,7 @@ class DuckDuckGoParam(ToolParamBase):
|
||||
self.check_valid_value(self.channel, "Web Search or News", ["text", "news"])
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
},
|
||||
"channel": {
|
||||
"name": "Channel",
|
||||
"type": "options",
|
||||
"value": "general",
|
||||
"options": ["general", "news"]
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}, "channel": {"name": "Channel", "type": "options", "value": "general", "options": ["general", "news"]}}
|
||||
|
||||
|
||||
class DuckDuckGo(ToolBase, ABC):
|
||||
@@ -83,7 +72,7 @@ class DuckDuckGo(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("DuckDuckGo processing"):
|
||||
return
|
||||
|
||||
@@ -99,10 +88,7 @@ class DuckDuckGo(ToolBase, ABC):
|
||||
if self.check_if_canceled("DuckDuckGo processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(duck_res,
|
||||
get_title=lambda r: r["title"],
|
||||
get_url=lambda r: r.get("href", r.get("url")),
|
||||
get_content=lambda r: r["body"])
|
||||
self._retrieve_chunks(duck_res, get_title=lambda r: r["title"], get_url=lambda r: r.get("href", r.get("url")), get_content=lambda r: r["body"])
|
||||
self.set_output("json", duck_res)
|
||||
return self.output("formalized_content")
|
||||
else:
|
||||
@@ -116,10 +102,7 @@ class DuckDuckGo(ToolBase, ABC):
|
||||
if self.check_if_canceled("DuckDuckGo processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(duck_res,
|
||||
get_title=lambda r: r["title"],
|
||||
get_url=lambda r: r.get("href", r.get("url")),
|
||||
get_content=lambda r: r["body"])
|
||||
self._retrieve_chunks(duck_res, get_title=lambda r: r["title"], get_url=lambda r: r.get("href", r.get("url")), get_content=lambda r: r["body"])
|
||||
self.set_output("json", duck_res)
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
|
||||
+18
-48
@@ -32,36 +32,17 @@ class EmailParam(ToolParamBase):
|
||||
"""
|
||||
Define the Email component parameters.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "email",
|
||||
"description": "The email is a method of electronic communication for sending and receiving information through the Internet. This tool helps users to send emails to one person or to multiple recipients with support for CC, BCC, file attachments, and markdown-to-HTML conversion.",
|
||||
"parameters": {
|
||||
"to_email": {
|
||||
"type": "string",
|
||||
"description": "The target email address.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
},
|
||||
"cc_email": {
|
||||
"type": "string",
|
||||
"description": "The other email addresses needs to be send to. Comma splited.",
|
||||
"default": "",
|
||||
"required": False
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The content of the email.",
|
||||
"default": "",
|
||||
"required": False
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "The subject/title of the email.",
|
||||
"default": "",
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
"to_email": {"type": "string", "description": "The target email address.", "default": "{sys.query}", "required": True},
|
||||
"cc_email": {"type": "string", "description": "The other email addresses needs to be send to. Comma splited.", "default": "", "required": False},
|
||||
"content": {"type": "string", "description": "The content of the email.", "default": "", "required": False},
|
||||
"subject": {"type": "string", "description": "The subject/title of the email.", "default": "", "required": False},
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
# Fixed configuration parameters
|
||||
@@ -81,20 +62,9 @@ class EmailParam(ToolParamBase):
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"to_email": {
|
||||
"name": "To ",
|
||||
"type": "line"
|
||||
},
|
||||
"subject": {
|
||||
"name": "Subject",
|
||||
"type": "line",
|
||||
"optional": True
|
||||
},
|
||||
"cc_email": {
|
||||
"name": "CC To",
|
||||
"type": "line",
|
||||
"optional": True
|
||||
},
|
||||
"to_email": {"name": "To ", "type": "line"},
|
||||
"subject": {"name": "Subject", "type": "line", "optional": True},
|
||||
"cc_email": {"name": "CC To", "type": "line", "optional": True},
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +81,7 @@ class Email(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("Email processing"):
|
||||
return
|
||||
|
||||
@@ -126,19 +96,19 @@ class Email(ToolBase, ABC):
|
||||
return False
|
||||
|
||||
# Create email object
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg = MIMEMultipart("alternative")
|
||||
|
||||
# Properly handle sender name encoding
|
||||
msg['From'] = formataddr((str(Header(self._param.sender_name,'utf-8')), self._param.email))
|
||||
msg['To'] = email_data["to_email"]
|
||||
msg["From"] = formataddr((str(Header(self._param.sender_name, "utf-8")), self._param.email))
|
||||
msg["To"] = email_data["to_email"]
|
||||
if email_data.get("cc_email"):
|
||||
msg['Cc'] = email_data["cc_email"]
|
||||
msg['Subject'] = Header(email_data.get("subject", "No Subject"), 'utf-8').encode()
|
||||
msg["Cc"] = email_data["cc_email"]
|
||||
msg["Subject"] = Header(email_data.get("subject", "No Subject"), "utf-8").encode()
|
||||
|
||||
# Use content from email_data or default content
|
||||
email_content = email_data.get("content", "No content provided")
|
||||
# msg.attach(MIMEText(email_content, 'plain', 'utf-8'))
|
||||
msg.attach(MIMEText(email_content, 'html', 'utf-8'))
|
||||
msg.attach(MIMEText(email_content, "html", "utf-8"))
|
||||
|
||||
# Connect to SMTP server and send
|
||||
logging.info(f"Connecting to SMTP server {self._param.smtp_server}:{self._param.smtp_port}")
|
||||
@@ -160,7 +130,7 @@ class Email(ToolBase, ABC):
|
||||
# Get all recipient list
|
||||
recipients = [email_data["to_email"]]
|
||||
if email_data.get("cc_email"):
|
||||
recipients.extend(email_data["cc_email"].split(','))
|
||||
recipients.extend(email_data["cc_email"].split(","))
|
||||
|
||||
# Send email
|
||||
logging.info(f"Sending email to recipients: {recipients}")
|
||||
|
||||
+26
-53
@@ -34,17 +34,10 @@ class ExeSQLParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "execute_sql",
|
||||
"description": "This is a tool that can execute SQL.",
|
||||
"parameters": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The SQL needs to be executed.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
}
|
||||
}
|
||||
"parameters": {"sql": {"type": "string", "description": "The SQL needs to be executed.", "default": "{sys.query}", "required": True}},
|
||||
}
|
||||
super().__init__()
|
||||
self.db_type = "mysql"
|
||||
@@ -56,7 +49,7 @@ class ExeSQLParam(ToolParamBase):
|
||||
self.max_records = 1024
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.db_type, "Choose DB type", ['mysql', 'postgres', 'mariadb', 'mssql', 'IBM DB2', 'trino', 'oceanbase'])
|
||||
self.check_valid_value(self.db_type, "Choose DB type", ["mysql", "postgres", "mariadb", "mssql", "IBM DB2", "trino", "oceanbase"])
|
||||
self.check_empty(self.database, "Database name")
|
||||
self.check_empty(self.username, "database username")
|
||||
self.check_empty(self.host, "IP Address")
|
||||
@@ -71,12 +64,7 @@ class ExeSQLParam(ToolParamBase):
|
||||
raise ValueError("For the security reason, it does not support database named rag_flow.")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"sql": {
|
||||
"name": "SQL",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"sql": {"name": "SQL", "type": "line"}}
|
||||
|
||||
|
||||
class ExeSQL(ToolBase, ABC):
|
||||
@@ -90,6 +78,7 @@ class ExeSQL(ToolBase, ABC):
|
||||
def convert_decimals(obj):
|
||||
from decimal import Decimal
|
||||
import math
|
||||
|
||||
if isinstance(obj, float):
|
||||
# Handle NaN and Infinity which are not valid JSON values
|
||||
if math.isnan(obj) or math.isinf(obj):
|
||||
@@ -140,24 +129,21 @@ class ExeSQL(ToolBase, ABC):
|
||||
|
||||
sqls = sql.split(";")
|
||||
if self._param.db_type in ["mysql", "mariadb"]:
|
||||
db = pymysql.connect(db=self._param.database, user=self._param.username, host=safe_host,
|
||||
port=self._param.port, password=self._param.password)
|
||||
elif self._param.db_type == 'oceanbase':
|
||||
db = pymysql.connect(db=self._param.database, user=self._param.username, host=safe_host,
|
||||
port=self._param.port, password=self._param.password, charset='utf8mb4')
|
||||
elif self._param.db_type == 'postgres':
|
||||
db = psycopg2.connect(dbname=self._param.database, user=self._param.username, host=safe_host,
|
||||
port=self._param.port, password=self._param.password)
|
||||
elif self._param.db_type == 'mssql':
|
||||
db = pymysql.connect(db=self._param.database, user=self._param.username, host=safe_host, port=self._param.port, password=self._param.password)
|
||||
elif self._param.db_type == "oceanbase":
|
||||
db = pymysql.connect(db=self._param.database, user=self._param.username, host=safe_host, port=self._param.port, password=self._param.password, charset="utf8mb4")
|
||||
elif self._param.db_type == "postgres":
|
||||
db = psycopg2.connect(dbname=self._param.database, user=self._param.username, host=safe_host, port=self._param.port, password=self._param.password)
|
||||
elif self._param.db_type == "mssql":
|
||||
conn_str = (
|
||||
r'DRIVER={ODBC Driver 17 for SQL Server};'
|
||||
r'SERVER=' + safe_host + ',' + str(self._param.port) + ';'
|
||||
r'DATABASE=' + self._param.database + ';'
|
||||
r'UID=' + self._param.username + ';'
|
||||
r'PWD=' + self._param.password
|
||||
r"DRIVER={ODBC Driver 17 for SQL Server};"
|
||||
r"SERVER=" + safe_host + "," + str(self._param.port) + ";"
|
||||
r"DATABASE=" + self._param.database + ";"
|
||||
r"UID=" + self._param.username + ";"
|
||||
r"PWD=" + self._param.password
|
||||
)
|
||||
db = pyodbc.connect(conn_str)
|
||||
elif self._param.db_type == 'trino':
|
||||
elif self._param.db_type == "trino":
|
||||
try:
|
||||
import trino
|
||||
from trino.auth import BasicAuthentication
|
||||
@@ -186,26 +172,14 @@ class ExeSQL(ToolBase, ABC):
|
||||
|
||||
try:
|
||||
db = trino.dbapi.connect(
|
||||
host=safe_host,
|
||||
port=int(self._param.port or 8080),
|
||||
user=self._param.username or "ragflow",
|
||||
catalog=catalog,
|
||||
schema=schema or "default",
|
||||
http_scheme=http_scheme,
|
||||
auth=auth
|
||||
host=safe_host, port=int(self._param.port or 8080), user=self._param.username or "ragflow", catalog=catalog, schema=schema or "default", http_scheme=http_scheme, auth=auth
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception("Database Connection Failed! \n" + str(e))
|
||||
elif self._param.db_type == 'IBM DB2':
|
||||
elif self._param.db_type == "IBM DB2":
|
||||
import ibm_db
|
||||
conn_str = (
|
||||
f"DATABASE={self._param.database};"
|
||||
f"HOSTNAME={safe_host};"
|
||||
f"PORT={self._param.port};"
|
||||
f"PROTOCOL=TCPIP;"
|
||||
f"UID={self._param.username};"
|
||||
f"PWD={self._param.password};"
|
||||
)
|
||||
|
||||
conn_str = f"DATABASE={self._param.database};HOSTNAME={safe_host};PORT={self._param.port};PROTOCOL=TCPIP;UID={self._param.username};PWD={self._param.password};"
|
||||
try:
|
||||
conn = ibm_db.connect(conn_str, "", "")
|
||||
except Exception as e:
|
||||
@@ -275,7 +249,7 @@ class ExeSQL(ToolBase, ABC):
|
||||
if self.check_if_canceled("ExeSQL processing"):
|
||||
return
|
||||
|
||||
single_sql = single_sql.replace('```', '').strip()
|
||||
single_sql = single_sql.replace("```", "").strip()
|
||||
if not single_sql:
|
||||
continue
|
||||
single_sql = re.sub(r"\[ID:[0-9]+\]", "", single_sql)
|
||||
@@ -288,20 +262,19 @@ class ExeSQL(ToolBase, ABC):
|
||||
if cursor.rowcount == 0:
|
||||
sql_res.append({"content": "No record in the database!"})
|
||||
break
|
||||
if self._param.db_type == 'mssql':
|
||||
single_res = pd.DataFrame.from_records(cursor.fetchmany(self._param.max_records),
|
||||
columns=[desc[0] for desc in cursor.description])
|
||||
if self._param.db_type == "mssql":
|
||||
single_res = pd.DataFrame.from_records(cursor.fetchmany(self._param.max_records), columns=[desc[0] for desc in cursor.description])
|
||||
else:
|
||||
single_res = pd.DataFrame([i for i in cursor.fetchmany(self._param.max_records)])
|
||||
single_res.columns = [i[0] for i in cursor.description]
|
||||
|
||||
for col in single_res.columns:
|
||||
if pd.api.types.is_datetime64_any_dtype(single_res[col]):
|
||||
single_res[col] = single_res[col].dt.strftime('%Y-%m-%d')
|
||||
single_res[col] = single_res[col].dt.strftime("%Y-%m-%d")
|
||||
|
||||
single_res = single_res.where(pd.notnull(single_res), None)
|
||||
|
||||
sql_res.append(convert_decimals(single_res.to_dict(orient='records')))
|
||||
sql_res.append(convert_decimals(single_res.to_dict(orient="records")))
|
||||
formalized_content.append(single_res.to_markdown(index=False, floatfmt=".6f"))
|
||||
except Exception as e:
|
||||
# A failing statement must not abort the node: report it and keep
|
||||
|
||||
+10
-18
@@ -29,7 +29,7 @@ class GitHubParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "github_search",
|
||||
"description": """GitHub repository search is a feature that enables users to find specific repositories on the GitHub platform. This search functionality allows users to locate projects, codebases, and other content hosted on GitHub based on various criteria.""",
|
||||
"parameters": {
|
||||
@@ -37,9 +37,9 @@ class GitHubParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The search keywords to execute with GitHub. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 10
|
||||
@@ -48,12 +48,8 @@ class GitHubParam(ToolParamBase):
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class GitHub(ToolBase, ABC):
|
||||
component_name = "GitHub"
|
||||
@@ -68,24 +64,20 @@ class GitHub(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("GitHub processing"):
|
||||
return
|
||||
|
||||
try:
|
||||
url = 'https://api.github.com/search/repositories?q=' + kwargs["query"] + '&sort=stars&order=desc&per_page=' + str(
|
||||
self._param.top_n)
|
||||
headers = {"Content-Type": "application/vnd.github+json", "X-GitHub-Api-Version": '2022-11-28'}
|
||||
url = "https://api.github.com/search/repositories?q=" + kwargs["query"] + "&sort=stars&order=desc&per_page=" + str(self._param.top_n)
|
||||
headers = {"Content-Type": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
|
||||
response = requests.get(url=url, headers=headers, timeout=DEFAULT_TIMEOUT).json()
|
||||
|
||||
if self.check_if_canceled("GitHub processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(response['items'],
|
||||
get_title=lambda r: r["name"],
|
||||
get_url=lambda r: r["html_url"],
|
||||
get_content=lambda r: str(r["description"]) + '\n stars:' + str(r['watchers']))
|
||||
self.set_output("json", response['items'])
|
||||
self._retrieve_chunks(response["items"], get_title=lambda r: r["name"], get_url=lambda r: r["html_url"], get_content=lambda r: str(r["description"]) + "\n stars:" + str(r["watchers"]))
|
||||
self.set_output("json", response["items"])
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
if self.check_if_canceled("GitHub processing"):
|
||||
|
||||
+422
-67
@@ -28,7 +28,7 @@ class GoogleParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "google_search",
|
||||
"description": """Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking ...""",
|
||||
"parameters": {
|
||||
@@ -36,7 +36,7 @@ class GoogleParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The search keywords to execute with Google. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
},
|
||||
"start": {
|
||||
"type": "integer",
|
||||
@@ -49,8 +49,8 @@ class GoogleParam(ToolParamBase):
|
||||
"description": "Parameter defines the maximum number of results to return. (e.g., 10 (default) returns 10 results, 40 returns 40 results, and 100 returns 100 results). The use of num may introduce latency, and/or prevent the inclusion of specialized result types. It is better to omit this parameter unless it is strictly necessary to increase the number of results per page. Results are not guaranteed to have the number of results specified in num.",
|
||||
"default": "6",
|
||||
"required": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.start = 0
|
||||
@@ -61,57 +61,418 @@ class GoogleParam(ToolParamBase):
|
||||
|
||||
def check(self):
|
||||
self.check_empty(self.api_key, "SerpApi API key")
|
||||
self.check_valid_value(self.country, "Google Country",
|
||||
['af', 'al', 'dz', 'as', 'ad', 'ao', 'ai', 'aq', 'ag', 'ar', 'am', 'aw', 'au', 'at',
|
||||
'az', 'bs', 'bh', 'bd', 'bb', 'by', 'be', 'bz', 'bj', 'bm', 'bt', 'bo', 'ba', 'bw',
|
||||
'bv', 'br', 'io', 'bn', 'bg', 'bf', 'bi', 'kh', 'cm', 'ca', 'cv', 'ky', 'cf', 'td',
|
||||
'cl', 'cn', 'cx', 'cc', 'co', 'km', 'cg', 'cd', 'ck', 'cr', 'ci', 'hr', 'cu', 'cy',
|
||||
'cz', 'dk', 'dj', 'dm', 'do', 'ec', 'eg', 'sv', 'gq', 'er', 'ee', 'et', 'fk', 'fo',
|
||||
'fj', 'fi', 'fr', 'gf', 'pf', 'tf', 'ga', 'gm', 'ge', 'de', 'gh', 'gi', 'gr', 'gl',
|
||||
'gd', 'gp', 'gu', 'gt', 'gn', 'gw', 'gy', 'ht', 'hm', 'va', 'hn', 'hk', 'hu', 'is',
|
||||
'in', 'id', 'ir', 'iq', 'ie', 'il', 'it', 'jm', 'jp', 'jo', 'kz', 'ke', 'ki', 'kp',
|
||||
'kr', 'kw', 'kg', 'la', 'lv', 'lb', 'ls', 'lr', 'ly', 'li', 'lt', 'lu', 'mo', 'mk',
|
||||
'mg', 'mw', 'my', 'mv', 'ml', 'mt', 'mh', 'mq', 'mr', 'mu', 'yt', 'mx', 'fm', 'md',
|
||||
'mc', 'mn', 'ms', 'ma', 'mz', 'mm', 'na', 'nr', 'np', 'nl', 'an', 'nc', 'nz', 'ni',
|
||||
'ne', 'ng', 'nu', 'nf', 'mp', 'no', 'om', 'pk', 'pw', 'ps', 'pa', 'pg', 'py', 'pe',
|
||||
'ph', 'pn', 'pl', 'pt', 'pr', 'qa', 're', 'ro', 'ru', 'rw', 'sh', 'kn', 'lc', 'pm',
|
||||
'vc', 'ws', 'sm', 'st', 'sa', 'sn', 'rs', 'sc', 'sl', 'sg', 'sk', 'si', 'sb', 'so',
|
||||
'za', 'gs', 'es', 'lk', 'sd', 'sr', 'sj', 'sz', 'se', 'ch', 'sy', 'tw', 'tj', 'tz',
|
||||
'th', 'tl', 'tg', 'tk', 'to', 'tt', 'tn', 'tr', 'tm', 'tc', 'tv', 'ug', 'ua', 'ae',
|
||||
'uk', 'gb', 'us', 'um', 'uy', 'uz', 'vu', 've', 'vn', 'vg', 'vi', 'wf', 'eh', 'ye',
|
||||
'zm', 'zw'])
|
||||
self.check_valid_value(self.language, "Google languages",
|
||||
['af', 'ak', 'sq', 'ws', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bem', 'bn', 'bh',
|
||||
'xx-bork', 'bs', 'br', 'bg', 'bt', 'km', 'ca', 'chr', 'ny', 'zh-cn', 'zh-tw', 'co',
|
||||
'hr', 'cs', 'da', 'nl', 'xx-elmer', 'en', 'eo', 'et', 'ee', 'fo', 'tl', 'fi', 'fr',
|
||||
'fy', 'gaa', 'gl', 'ka', 'de', 'el', 'kl', 'gn', 'gu', 'xx-hacker', 'ht', 'ha', 'haw',
|
||||
'iw', 'hi', 'hu', 'is', 'ig', 'id', 'ia', 'ga', 'it', 'ja', 'jw', 'kn', 'kk', 'rw',
|
||||
'rn', 'xx-klingon', 'kg', 'ko', 'kri', 'ku', 'ckb', 'ky', 'lo', 'la', 'lv', 'ln', 'lt',
|
||||
'loz', 'lg', 'ach', 'mk', 'mg', 'ms', 'ml', 'mt', 'mv', 'mi', 'mr', 'mfe', 'mo', 'mn',
|
||||
'sr-me', 'my', 'ne', 'pcm', 'nso', 'no', 'nn', 'oc', 'or', 'om', 'ps', 'fa',
|
||||
'xx-pirate', 'pl', 'pt', 'pt-br', 'pt-pt', 'pa', 'qu', 'ro', 'rm', 'nyn', 'ru', 'gd',
|
||||
'sr', 'sh', 'st', 'tn', 'crs', 'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es', 'es-419', 'su',
|
||||
'sw', 'sv', 'tg', 'ta', 'tt', 'te', 'th', 'ti', 'to', 'lua', 'tum', 'tr', 'tk', 'tw',
|
||||
'ug', 'uk', 'ur', 'uz', 'vu', 'vi', 'cy', 'wo', 'xh', 'yi', 'yo', 'zu']
|
||||
)
|
||||
self.check_valid_value(
|
||||
self.country,
|
||||
"Google Country",
|
||||
[
|
||||
"af",
|
||||
"al",
|
||||
"dz",
|
||||
"as",
|
||||
"ad",
|
||||
"ao",
|
||||
"ai",
|
||||
"aq",
|
||||
"ag",
|
||||
"ar",
|
||||
"am",
|
||||
"aw",
|
||||
"au",
|
||||
"at",
|
||||
"az",
|
||||
"bs",
|
||||
"bh",
|
||||
"bd",
|
||||
"bb",
|
||||
"by",
|
||||
"be",
|
||||
"bz",
|
||||
"bj",
|
||||
"bm",
|
||||
"bt",
|
||||
"bo",
|
||||
"ba",
|
||||
"bw",
|
||||
"bv",
|
||||
"br",
|
||||
"io",
|
||||
"bn",
|
||||
"bg",
|
||||
"bf",
|
||||
"bi",
|
||||
"kh",
|
||||
"cm",
|
||||
"ca",
|
||||
"cv",
|
||||
"ky",
|
||||
"cf",
|
||||
"td",
|
||||
"cl",
|
||||
"cn",
|
||||
"cx",
|
||||
"cc",
|
||||
"co",
|
||||
"km",
|
||||
"cg",
|
||||
"cd",
|
||||
"ck",
|
||||
"cr",
|
||||
"ci",
|
||||
"hr",
|
||||
"cu",
|
||||
"cy",
|
||||
"cz",
|
||||
"dk",
|
||||
"dj",
|
||||
"dm",
|
||||
"do",
|
||||
"ec",
|
||||
"eg",
|
||||
"sv",
|
||||
"gq",
|
||||
"er",
|
||||
"ee",
|
||||
"et",
|
||||
"fk",
|
||||
"fo",
|
||||
"fj",
|
||||
"fi",
|
||||
"fr",
|
||||
"gf",
|
||||
"pf",
|
||||
"tf",
|
||||
"ga",
|
||||
"gm",
|
||||
"ge",
|
||||
"de",
|
||||
"gh",
|
||||
"gi",
|
||||
"gr",
|
||||
"gl",
|
||||
"gd",
|
||||
"gp",
|
||||
"gu",
|
||||
"gt",
|
||||
"gn",
|
||||
"gw",
|
||||
"gy",
|
||||
"ht",
|
||||
"hm",
|
||||
"va",
|
||||
"hn",
|
||||
"hk",
|
||||
"hu",
|
||||
"is",
|
||||
"in",
|
||||
"id",
|
||||
"ir",
|
||||
"iq",
|
||||
"ie",
|
||||
"il",
|
||||
"it",
|
||||
"jm",
|
||||
"jp",
|
||||
"jo",
|
||||
"kz",
|
||||
"ke",
|
||||
"ki",
|
||||
"kp",
|
||||
"kr",
|
||||
"kw",
|
||||
"kg",
|
||||
"la",
|
||||
"lv",
|
||||
"lb",
|
||||
"ls",
|
||||
"lr",
|
||||
"ly",
|
||||
"li",
|
||||
"lt",
|
||||
"lu",
|
||||
"mo",
|
||||
"mk",
|
||||
"mg",
|
||||
"mw",
|
||||
"my",
|
||||
"mv",
|
||||
"ml",
|
||||
"mt",
|
||||
"mh",
|
||||
"mq",
|
||||
"mr",
|
||||
"mu",
|
||||
"yt",
|
||||
"mx",
|
||||
"fm",
|
||||
"md",
|
||||
"mc",
|
||||
"mn",
|
||||
"ms",
|
||||
"ma",
|
||||
"mz",
|
||||
"mm",
|
||||
"na",
|
||||
"nr",
|
||||
"np",
|
||||
"nl",
|
||||
"an",
|
||||
"nc",
|
||||
"nz",
|
||||
"ni",
|
||||
"ne",
|
||||
"ng",
|
||||
"nu",
|
||||
"nf",
|
||||
"mp",
|
||||
"no",
|
||||
"om",
|
||||
"pk",
|
||||
"pw",
|
||||
"ps",
|
||||
"pa",
|
||||
"pg",
|
||||
"py",
|
||||
"pe",
|
||||
"ph",
|
||||
"pn",
|
||||
"pl",
|
||||
"pt",
|
||||
"pr",
|
||||
"qa",
|
||||
"re",
|
||||
"ro",
|
||||
"ru",
|
||||
"rw",
|
||||
"sh",
|
||||
"kn",
|
||||
"lc",
|
||||
"pm",
|
||||
"vc",
|
||||
"ws",
|
||||
"sm",
|
||||
"st",
|
||||
"sa",
|
||||
"sn",
|
||||
"rs",
|
||||
"sc",
|
||||
"sl",
|
||||
"sg",
|
||||
"sk",
|
||||
"si",
|
||||
"sb",
|
||||
"so",
|
||||
"za",
|
||||
"gs",
|
||||
"es",
|
||||
"lk",
|
||||
"sd",
|
||||
"sr",
|
||||
"sj",
|
||||
"sz",
|
||||
"se",
|
||||
"ch",
|
||||
"sy",
|
||||
"tw",
|
||||
"tj",
|
||||
"tz",
|
||||
"th",
|
||||
"tl",
|
||||
"tg",
|
||||
"tk",
|
||||
"to",
|
||||
"tt",
|
||||
"tn",
|
||||
"tr",
|
||||
"tm",
|
||||
"tc",
|
||||
"tv",
|
||||
"ug",
|
||||
"ua",
|
||||
"ae",
|
||||
"uk",
|
||||
"gb",
|
||||
"us",
|
||||
"um",
|
||||
"uy",
|
||||
"uz",
|
||||
"vu",
|
||||
"ve",
|
||||
"vn",
|
||||
"vg",
|
||||
"vi",
|
||||
"wf",
|
||||
"eh",
|
||||
"ye",
|
||||
"zm",
|
||||
"zw",
|
||||
],
|
||||
)
|
||||
self.check_valid_value(
|
||||
self.language,
|
||||
"Google languages",
|
||||
[
|
||||
"af",
|
||||
"ak",
|
||||
"sq",
|
||||
"ws",
|
||||
"am",
|
||||
"ar",
|
||||
"hy",
|
||||
"az",
|
||||
"eu",
|
||||
"be",
|
||||
"bem",
|
||||
"bn",
|
||||
"bh",
|
||||
"xx-bork",
|
||||
"bs",
|
||||
"br",
|
||||
"bg",
|
||||
"bt",
|
||||
"km",
|
||||
"ca",
|
||||
"chr",
|
||||
"ny",
|
||||
"zh-cn",
|
||||
"zh-tw",
|
||||
"co",
|
||||
"hr",
|
||||
"cs",
|
||||
"da",
|
||||
"nl",
|
||||
"xx-elmer",
|
||||
"en",
|
||||
"eo",
|
||||
"et",
|
||||
"ee",
|
||||
"fo",
|
||||
"tl",
|
||||
"fi",
|
||||
"fr",
|
||||
"fy",
|
||||
"gaa",
|
||||
"gl",
|
||||
"ka",
|
||||
"de",
|
||||
"el",
|
||||
"kl",
|
||||
"gn",
|
||||
"gu",
|
||||
"xx-hacker",
|
||||
"ht",
|
||||
"ha",
|
||||
"haw",
|
||||
"iw",
|
||||
"hi",
|
||||
"hu",
|
||||
"is",
|
||||
"ig",
|
||||
"id",
|
||||
"ia",
|
||||
"ga",
|
||||
"it",
|
||||
"ja",
|
||||
"jw",
|
||||
"kn",
|
||||
"kk",
|
||||
"rw",
|
||||
"rn",
|
||||
"xx-klingon",
|
||||
"kg",
|
||||
"ko",
|
||||
"kri",
|
||||
"ku",
|
||||
"ckb",
|
||||
"ky",
|
||||
"lo",
|
||||
"la",
|
||||
"lv",
|
||||
"ln",
|
||||
"lt",
|
||||
"loz",
|
||||
"lg",
|
||||
"ach",
|
||||
"mk",
|
||||
"mg",
|
||||
"ms",
|
||||
"ml",
|
||||
"mt",
|
||||
"mv",
|
||||
"mi",
|
||||
"mr",
|
||||
"mfe",
|
||||
"mo",
|
||||
"mn",
|
||||
"sr-me",
|
||||
"my",
|
||||
"ne",
|
||||
"pcm",
|
||||
"nso",
|
||||
"no",
|
||||
"nn",
|
||||
"oc",
|
||||
"or",
|
||||
"om",
|
||||
"ps",
|
||||
"fa",
|
||||
"xx-pirate",
|
||||
"pl",
|
||||
"pt",
|
||||
"pt-br",
|
||||
"pt-pt",
|
||||
"pa",
|
||||
"qu",
|
||||
"ro",
|
||||
"rm",
|
||||
"nyn",
|
||||
"ru",
|
||||
"gd",
|
||||
"sr",
|
||||
"sh",
|
||||
"st",
|
||||
"tn",
|
||||
"crs",
|
||||
"sn",
|
||||
"sd",
|
||||
"si",
|
||||
"sk",
|
||||
"sl",
|
||||
"so",
|
||||
"es",
|
||||
"es-419",
|
||||
"su",
|
||||
"sw",
|
||||
"sv",
|
||||
"tg",
|
||||
"ta",
|
||||
"tt",
|
||||
"te",
|
||||
"th",
|
||||
"ti",
|
||||
"to",
|
||||
"lua",
|
||||
"tum",
|
||||
"tr",
|
||||
"tk",
|
||||
"tw",
|
||||
"ug",
|
||||
"uk",
|
||||
"ur",
|
||||
"uz",
|
||||
"vu",
|
||||
"vi",
|
||||
"cy",
|
||||
"wo",
|
||||
"xh",
|
||||
"yi",
|
||||
"yo",
|
||||
"zu",
|
||||
],
|
||||
)
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"q": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
},
|
||||
"start": {
|
||||
"name": "From",
|
||||
"type": "integer",
|
||||
"value": 0
|
||||
},
|
||||
"num": {
|
||||
"name": "Limit",
|
||||
"type": "integer",
|
||||
"value": 12
|
||||
}
|
||||
}
|
||||
return {"q": {"name": "Query", "type": "line"}, "start": {"name": "From", "type": "integer", "value": 0}, "num": {"name": "Limit", "type": "integer", "value": 12}}
|
||||
|
||||
|
||||
class Google(ToolBase, ABC):
|
||||
component_name = "Google"
|
||||
@@ -125,16 +486,9 @@ class Google(ToolBase, ABC):
|
||||
self.set_output("formalized_content", "")
|
||||
return ""
|
||||
|
||||
params = {
|
||||
"api_key": self._param.api_key,
|
||||
"engine": "google",
|
||||
"q": kwargs["q"],
|
||||
"google_domain": "google.com",
|
||||
"gl": self._param.country,
|
||||
"hl": self._param.language
|
||||
}
|
||||
params = {"api_key": self._param.api_key, "engine": "google", "q": kwargs["q"], "google_domain": "google.com", "gl": self._param.country, "hl": self._param.language}
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("Google processing"):
|
||||
return
|
||||
|
||||
@@ -144,11 +498,12 @@ class Google(ToolBase, ABC):
|
||||
if self.check_if_canceled("Google processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(search["organic_results"],
|
||||
get_title=lambda r: r["title"],
|
||||
get_url=lambda r: r["link"],
|
||||
get_content=lambda r: r.get("about_this_result", {}).get("source", {}).get("description", r["snippet"])
|
||||
)
|
||||
self._retrieve_chunks(
|
||||
search["organic_results"],
|
||||
get_title=lambda r: r["title"],
|
||||
get_url=lambda r: r["link"],
|
||||
get_content=lambda r: r.get("about_this_result", {}).get("source", {}).get("description", r["snippet"]),
|
||||
)
|
||||
self.set_output("json", search["organic_results"])
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
|
||||
@@ -29,7 +29,7 @@ class GoogleScholarParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "google_scholar_search",
|
||||
"description": """Google Scholar provides a simple way to broadly search for scholarly literature. From one place, you can search across many disciplines and sources: articles, theses, books, abstracts and court opinions, from academic publishers, professional societies, online repositories, universities and other web sites. Google Scholar helps you find relevant work across the world of scholarly research.""",
|
||||
"parameters": {
|
||||
@@ -37,29 +37,25 @@ class GoogleScholarParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The search keyword to execute with Google Scholar. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 12
|
||||
self.sort_by = 'relevance'
|
||||
self.sort_by = "relevance"
|
||||
self.year_low = None
|
||||
self.year_high = None
|
||||
self.patents = True
|
||||
|
||||
def check(self):
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
self.check_valid_value(self.sort_by, "GoogleScholar Sort_by", ['date', 'relevance'])
|
||||
self.check_valid_value(self.sort_by, "GoogleScholar Sort_by", ["date", "relevance"])
|
||||
self.check_boolean(self.patents, "Whether or not to include patents, defaults to True")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class GoogleScholar(ToolBase, ABC):
|
||||
component_name = "GoogleScholar"
|
||||
@@ -77,13 +73,12 @@ class GoogleScholar(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("GoogleScholar processing"):
|
||||
return
|
||||
|
||||
try:
|
||||
scholar_client = scholarly.search_pubs(kwargs["query"], patents=self._param.patents, year_low=self._param.year_low,
|
||||
year_high=self._param.year_high, sort_by=self._param.sort_by)
|
||||
scholar_client = scholarly.search_pubs(kwargs["query"], patents=self._param.patents, year_low=self._param.year_low, year_high=self._param.year_high, sort_by=self._param.sort_by)
|
||||
|
||||
if self.check_if_canceled("GoogleScholar processing"):
|
||||
return
|
||||
@@ -94,11 +89,12 @@ class GoogleScholar(ToolBase, ABC):
|
||||
# would otherwise leave json empty).
|
||||
results = list(itertools.islice(scholar_client, self._param.top_n))
|
||||
|
||||
self._retrieve_chunks(results,
|
||||
get_title=lambda r: r['bib']['title'],
|
||||
get_url=lambda r: r["pub_url"],
|
||||
get_content=lambda r: "\n author: " + ",".join(r['bib']['author']) + '\n Abstract: ' + r['bib'].get('abstract', 'no abstract')
|
||||
)
|
||||
self._retrieve_chunks(
|
||||
results,
|
||||
get_title=lambda r: r["bib"]["title"],
|
||||
get_url=lambda r: r["pub_url"],
|
||||
get_content=lambda r: "\n author: " + ",".join(r["bib"]["author"]) + "\n Abstract: " + r["bib"].get("abstract", "no abstract"),
|
||||
)
|
||||
self.set_output("json", results)
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
|
||||
+52
-61
@@ -30,21 +30,21 @@ class Jin10Param(ComponentParamBase):
|
||||
super().__init__()
|
||||
self.type = "flash"
|
||||
self.secret_key = "xxx"
|
||||
self.flash_type = '1'
|
||||
self.calendar_type = 'cj'
|
||||
self.calendar_datatype = 'data'
|
||||
self.symbols_type = 'GOODS'
|
||||
self.symbols_datatype = 'symbols'
|
||||
self.flash_type = "1"
|
||||
self.calendar_type = "cj"
|
||||
self.calendar_datatype = "data"
|
||||
self.symbols_type = "GOODS"
|
||||
self.symbols_datatype = "symbols"
|
||||
self.contain = ""
|
||||
self.filter = ""
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.type, "Type", ['flash', 'calendar', 'symbols', 'news'])
|
||||
self.check_valid_value(self.flash_type, "Flash Type", ['1', '2', '3', '4', '5'])
|
||||
self.check_valid_value(self.calendar_type, "Calendar Type", ['cj', 'qh', 'hk', 'us'])
|
||||
self.check_valid_value(self.calendar_datatype, "Calendar DataType", ['data', 'event', 'holiday'])
|
||||
self.check_valid_value(self.symbols_type, "Symbols Type", ['GOODS', 'FOREX', 'FUTURE', 'CRYPTO'])
|
||||
self.check_valid_value(self.symbols_datatype, 'Symbols DataType', ['symbols', 'quotes'])
|
||||
self.check_valid_value(self.type, "Type", ["flash", "calendar", "symbols", "news"])
|
||||
self.check_valid_value(self.flash_type, "Flash Type", ["1", "2", "3", "4", "5"])
|
||||
self.check_valid_value(self.calendar_type, "Calendar Type", ["cj", "qh", "hk", "us"])
|
||||
self.check_valid_value(self.calendar_datatype, "Calendar DataType", ["data", "event", "holiday"])
|
||||
self.check_valid_value(self.symbols_type, "Symbols Type", ["GOODS", "FOREX", "FUTURE", "CRYPTO"])
|
||||
self.check_valid_value(self.symbols_datatype, "Symbols DataType", ["symbols", "quotes"])
|
||||
|
||||
|
||||
class Jin10(ComponentBase, ABC):
|
||||
@@ -60,86 +60,77 @@ class Jin10(ComponentBase, ABC):
|
||||
return Jin10.be_output("")
|
||||
|
||||
jin10_res = []
|
||||
headers = {'secret-key': self._param.secret_key}
|
||||
headers = {"secret-key": self._param.secret_key}
|
||||
try:
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
|
||||
if self._param.type == "flash":
|
||||
params = {
|
||||
'category': self._param.flash_type,
|
||||
'contain': self._param.contain,
|
||||
'filter': self._param.filter
|
||||
}
|
||||
response = requests.get(
|
||||
url='https://open-data-api.jin10.com/data-api/flash?category=' + self._param.flash_type,
|
||||
headers=headers, data=json.dumps(params), timeout=DEFAULT_TIMEOUT)
|
||||
params = {"category": self._param.flash_type, "contain": self._param.contain, "filter": self._param.filter}
|
||||
response = requests.get(url="https://open-data-api.jin10.com/data-api/flash?category=" + self._param.flash_type, headers=headers, data=json.dumps(params), timeout=DEFAULT_TIMEOUT)
|
||||
response = response.json()
|
||||
for i in response['data']:
|
||||
for i in response["data"]:
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
jin10_res.append({"content": i['data']['content']})
|
||||
jin10_res.append({"content": i["data"]["content"]})
|
||||
if self._param.type == "calendar":
|
||||
params = {
|
||||
'category': self._param.calendar_type
|
||||
}
|
||||
params = {"category": self._param.calendar_type}
|
||||
response = requests.get(
|
||||
url='https://open-data-api.jin10.com/data-api/calendar/' + self._param.calendar_datatype + '?category=' + self._param.calendar_type,
|
||||
headers=headers, data=json.dumps(params), timeout=DEFAULT_TIMEOUT)
|
||||
url="https://open-data-api.jin10.com/data-api/calendar/" + self._param.calendar_datatype + "?category=" + self._param.calendar_type,
|
||||
headers=headers,
|
||||
data=json.dumps(params),
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
response = response.json()
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
jin10_res.append({"content": pd.DataFrame(response['data']).to_markdown()})
|
||||
jin10_res.append({"content": pd.DataFrame(response["data"]).to_markdown()})
|
||||
if self._param.type == "symbols":
|
||||
params = {
|
||||
'type': self._param.symbols_type
|
||||
}
|
||||
params = {"type": self._param.symbols_type}
|
||||
if self._param.symbols_datatype == "quotes":
|
||||
params['codes'] = 'BTCUSD'
|
||||
params["codes"] = "BTCUSD"
|
||||
response = requests.get(
|
||||
url='https://open-data-api.jin10.com/data-api/' + self._param.symbols_datatype + '?type=' + self._param.symbols_type,
|
||||
headers=headers, data=json.dumps(params), timeout=DEFAULT_TIMEOUT)
|
||||
url="https://open-data-api.jin10.com/data-api/" + self._param.symbols_datatype + "?type=" + self._param.symbols_type,
|
||||
headers=headers,
|
||||
data=json.dumps(params),
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
response = response.json()
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
if self._param.symbols_datatype == "symbols":
|
||||
for i in response['data']:
|
||||
for i in response["data"]:
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
i['Commodity Code'] = i['c']
|
||||
i['Stock Exchange'] = i['e']
|
||||
i['Commodity Name'] = i['n']
|
||||
i['Commodity Type'] = i['t']
|
||||
del i['c'], i['e'], i['n'], i['t']
|
||||
i["Commodity Code"] = i["c"]
|
||||
i["Stock Exchange"] = i["e"]
|
||||
i["Commodity Name"] = i["n"]
|
||||
i["Commodity Type"] = i["t"]
|
||||
del i["c"], i["e"], i["n"], i["t"]
|
||||
if self._param.symbols_datatype == "quotes":
|
||||
for i in response['data']:
|
||||
for i in response["data"]:
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
i['Selling Price'] = i['a']
|
||||
i['Buying Price'] = i['b']
|
||||
i['Commodity Code'] = i['c']
|
||||
i['Stock Exchange'] = i['e']
|
||||
i['Highest Price'] = i['h']
|
||||
i['Yesterday’s Closing Price'] = i['hc']
|
||||
i['Lowest Price'] = i['l']
|
||||
i['Opening Price'] = i['o']
|
||||
i['Latest Price'] = i['p']
|
||||
i['Market Quote Time'] = i['t']
|
||||
del i['a'], i['b'], i['c'], i['e'], i['h'], i['hc'], i['l'], i['o'], i['p'], i['t']
|
||||
jin10_res.append({"content": pd.DataFrame(response['data']).to_markdown()})
|
||||
i["Selling Price"] = i["a"]
|
||||
i["Buying Price"] = i["b"]
|
||||
i["Commodity Code"] = i["c"]
|
||||
i["Stock Exchange"] = i["e"]
|
||||
i["Highest Price"] = i["h"]
|
||||
i["Yesterday’s Closing Price"] = i["hc"]
|
||||
i["Lowest Price"] = i["l"]
|
||||
i["Opening Price"] = i["o"]
|
||||
i["Latest Price"] = i["p"]
|
||||
i["Market Quote Time"] = i["t"]
|
||||
del i["a"], i["b"], i["c"], i["e"], i["h"], i["hc"], i["l"], i["o"], i["p"], i["t"]
|
||||
jin10_res.append({"content": pd.DataFrame(response["data"]).to_markdown()})
|
||||
if self._param.type == "news":
|
||||
params = {
|
||||
'contain': self._param.contain,
|
||||
'filter': self._param.filter
|
||||
}
|
||||
response = requests.get(
|
||||
url='https://open-data-api.jin10.com/data-api/news',
|
||||
headers=headers, data=json.dumps(params), timeout=DEFAULT_TIMEOUT)
|
||||
params = {"contain": self._param.contain, "filter": self._param.filter}
|
||||
response = requests.get(url="https://open-data-api.jin10.com/data-api/news", headers=headers, data=json.dumps(params), timeout=DEFAULT_TIMEOUT)
|
||||
response = response.json()
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
jin10_res.append({"content": pd.DataFrame(response['data']).to_markdown()})
|
||||
jin10_res.append({"content": pd.DataFrame(response["data"]).to_markdown()})
|
||||
except Exception as e:
|
||||
if self.check_if_canceled("Jin10 processing"):
|
||||
return
|
||||
|
||||
+16
-27
@@ -30,7 +30,7 @@ class PubMedParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "pubmed_search",
|
||||
"description": """
|
||||
PubMed is an openly accessible, free database which includes primarily the MEDLINE database of references and abstracts on life sciences and biomedical topics.
|
||||
@@ -47,9 +47,9 @@ In addition to MEDLINE, PubMed provides access to:
|
||||
"type": "string",
|
||||
"description": "The search keywords to execute with PubMed. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 12
|
||||
@@ -59,12 +59,8 @@ In addition to MEDLINE, PubMed provides access to:
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class PubMed(ToolBase, ABC):
|
||||
component_name = "PubMed"
|
||||
@@ -79,27 +75,28 @@ class PubMed(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("PubMed processing"):
|
||||
return
|
||||
|
||||
try:
|
||||
Entrez.email = self._param.email
|
||||
pubmedids = Entrez.read(Entrez.esearch(db='pubmed', retmax=self._param.top_n, term=kwargs["query"]))['IdList']
|
||||
pubmedids = Entrez.read(Entrez.esearch(db="pubmed", retmax=self._param.top_n, term=kwargs["query"]))["IdList"]
|
||||
|
||||
if self.check_if_canceled("PubMed processing"):
|
||||
return
|
||||
|
||||
pubmedcnt = ET.fromstring(re.sub(r'<(/?)b>|<(/?)i>', '', Entrez.efetch(db='pubmed', id=",".join(pubmedids),
|
||||
retmode="xml").read().decode("utf-8")))
|
||||
pubmedcnt = ET.fromstring(re.sub(r"<(/?)b>|<(/?)i>", "", Entrez.efetch(db="pubmed", id=",".join(pubmedids), retmode="xml").read().decode("utf-8")))
|
||||
|
||||
if self.check_if_canceled("PubMed processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(pubmedcnt.findall("PubmedArticle"),
|
||||
get_title=lambda child: child.find("MedlineCitation").find("Article").find("ArticleTitle").text,
|
||||
get_url=lambda child: "https://pubmed.ncbi.nlm.nih.gov/" + child.find("MedlineCitation").find("PMID").text,
|
||||
get_content=lambda child: self._format_pubmed_content(child),)
|
||||
self._retrieve_chunks(
|
||||
pubmedcnt.findall("PubmedArticle"),
|
||||
get_title=lambda child: child.find("MedlineCitation").find("Article").find("ArticleTitle").text,
|
||||
get_url=lambda child: "https://pubmed.ncbi.nlm.nih.gov/" + child.find("MedlineCitation").find("PMID").text,
|
||||
get_content=lambda child: self._format_pubmed_content(child),
|
||||
)
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
if self.check_if_canceled("PubMed processing"):
|
||||
@@ -117,6 +114,7 @@ class PubMed(ToolBase, ABC):
|
||||
|
||||
def _format_pubmed_content(self, child):
|
||||
"""Extract structured reference info from PubMed XML"""
|
||||
|
||||
def safe_find(path, base=None):
|
||||
node = child if base is None else base
|
||||
for p in path.split("/"):
|
||||
@@ -149,16 +147,7 @@ class PubMed(ToolBase, ABC):
|
||||
doi = eid.text
|
||||
break
|
||||
|
||||
return (
|
||||
f"Title: {title}\n"
|
||||
f"Authors: {authors_str}\n"
|
||||
f"Journal: {journal}\n"
|
||||
f"Volume: {volume}\n"
|
||||
f"Issue: {issue}\n"
|
||||
f"Pages: {pages}\n"
|
||||
f"DOI: {doi or '-'}\n"
|
||||
f"Abstract: {abstract.strip()}"
|
||||
)
|
||||
return f"Title: {title}\nAuthors: {authors_str}\nJournal: {journal}\nVolume: {volume}\nIssue: {issue}\nPages: {pages}\nDOI: {doi or '-'}\nAbstract: {abstract.strip()}"
|
||||
|
||||
def thoughts(self) -> str:
|
||||
return "Looking for scholarly papers on `{}`,” prioritising reputable sources.".format(self.get_input().get("query", "-_-!"))
|
||||
|
||||
+45
-15
@@ -30,7 +30,7 @@ class QWeatherParam(ComponentParamBase):
|
||||
self.web_apikey = "xxx"
|
||||
self.lang = "zh"
|
||||
self.type = "weather"
|
||||
self.user_type = 'free'
|
||||
self.user_type = "free"
|
||||
self.error_code = {
|
||||
"204": "The request was successful, but the region you are querying does not have the data you need at this time.",
|
||||
"400": "Request error, may contain incorrect request parameters or missing mandatory request parameters.",
|
||||
@@ -39,20 +39,53 @@ class QWeatherParam(ComponentParamBase):
|
||||
"403": "No access, may be the binding PackageName, BundleID, domain IP address is inconsistent, or the data that requires additional payment.",
|
||||
"404": "The queried data or region does not exist.",
|
||||
"429": "Exceeded the limited QPM (number of accesses per minute), please refer to the QPM description",
|
||||
"500": "No response or timeout, interface service abnormality please contact us"
|
||||
}
|
||||
"500": "No response or timeout, interface service abnormality please contact us",
|
||||
}
|
||||
# Weather
|
||||
self.time_period = 'now'
|
||||
self.time_period = "now"
|
||||
|
||||
def check(self):
|
||||
self.check_empty(self.web_apikey, "BaiduFanyi APPID")
|
||||
self.check_valid_value(self.type, "Type", ["weather", "indices", "airquality"])
|
||||
self.check_valid_value(self.user_type, "Free subscription or paid subscription", ["free", "paid"])
|
||||
self.check_valid_value(self.lang, "Use language",
|
||||
['zh', 'zh-hant', 'en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'hi', 'th', 'ar', 'pt',
|
||||
'bn', 'ms', 'nl', 'el', 'la', 'sv', 'id', 'pl', 'tr', 'cs', 'et', 'vi', 'fil', 'fi',
|
||||
'he', 'is', 'nb'])
|
||||
self.check_valid_value(self.time_period, "Time period", ['now', '3d', '7d', '10d', '15d', '30d'])
|
||||
self.check_valid_value(
|
||||
self.lang,
|
||||
"Use language",
|
||||
[
|
||||
"zh",
|
||||
"zh-hant",
|
||||
"en",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ru",
|
||||
"hi",
|
||||
"th",
|
||||
"ar",
|
||||
"pt",
|
||||
"bn",
|
||||
"ms",
|
||||
"nl",
|
||||
"el",
|
||||
"la",
|
||||
"sv",
|
||||
"id",
|
||||
"pl",
|
||||
"tr",
|
||||
"cs",
|
||||
"et",
|
||||
"vi",
|
||||
"fil",
|
||||
"fi",
|
||||
"he",
|
||||
"is",
|
||||
"nb",
|
||||
],
|
||||
)
|
||||
self.check_valid_value(self.time_period, "Time period", ["now", "3d", "7d", "10d", "15d", "30d"])
|
||||
|
||||
|
||||
class QWeather(ComponentBase, ABC):
|
||||
@@ -71,9 +104,7 @@ class QWeather(ComponentBase, ABC):
|
||||
if self.check_if_canceled("Qweather processing"):
|
||||
return
|
||||
|
||||
response = requests.get(
|
||||
url="https://geoapi.qweather.com/v2/city/lookup?location=" + ans + "&key=" + self._param.web_apikey,
|
||||
timeout=DEFAULT_TIMEOUT).json()
|
||||
response = requests.get(url="https://geoapi.qweather.com/v2/city/lookup?location=" + ans + "&key=" + self._param.web_apikey, timeout=DEFAULT_TIMEOUT).json()
|
||||
if response["code"] == "200":
|
||||
location_id = response["location"][0]["id"]
|
||||
else:
|
||||
@@ -82,7 +113,7 @@ class QWeather(ComponentBase, ABC):
|
||||
if self.check_if_canceled("Qweather processing"):
|
||||
return
|
||||
|
||||
base_url = "https://api.qweather.com/v7/" if self._param.user_type == 'paid' else "https://devapi.qweather.com/v7/"
|
||||
base_url = "https://api.qweather.com/v7/" if self._param.user_type == "paid" else "https://devapi.qweather.com/v7/"
|
||||
|
||||
if self._param.type == "weather":
|
||||
url = base_url + "weather/" + self._param.time_period + "?location=" + location_id + "&key=" + self._param.web_apikey + "&lang=" + self._param.lang
|
||||
@@ -110,8 +141,7 @@ class QWeather(ComponentBase, ABC):
|
||||
if self.check_if_canceled("Qweather processing"):
|
||||
return
|
||||
if response["code"] == "200":
|
||||
indices_res = response["daily"][0]["date"] + "\n" + "\n".join(
|
||||
[i["name"] + ": " + i["category"] + ", " + i["text"] for i in response["daily"]])
|
||||
indices_res = response["daily"][0]["date"] + "\n" + "\n".join([i["name"] + ": " + i["category"] + ", " + i["text"] for i in response["daily"]])
|
||||
return QWeather.be_output(indices_res)
|
||||
|
||||
else:
|
||||
|
||||
+16
-30
@@ -40,7 +40,7 @@ class RetrievalParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "search_my_dateset",
|
||||
"description": "This tool can be utilized for relevant content searching in the datasets.",
|
||||
"parameters": {
|
||||
@@ -48,9 +48,9 @@ class RetrievalParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The keywords to search the dataset. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "",
|
||||
"required": True
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.function_name = "search_my_dateset"
|
||||
@@ -68,7 +68,7 @@ class RetrievalParam(ToolParamBase):
|
||||
self.use_kg = False
|
||||
self.cross_languages = []
|
||||
self.toc_enhance = False
|
||||
self.meta_data_filter={}
|
||||
self.meta_data_filter = {}
|
||||
|
||||
def check(self):
|
||||
self.check_decimal_float(self.similarity_threshold, "[Retrieval] Similarity threshold")
|
||||
@@ -76,12 +76,8 @@ class RetrievalParam(ToolParamBase):
|
||||
self.check_positive_number(self.top_n, "[Retrieval] Top N")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class Retrieval(ToolBase, ABC):
|
||||
component_name = "Retrieval"
|
||||
@@ -101,8 +97,7 @@ class Retrieval(ToolBase, ABC):
|
||||
# if kb_nm is a list
|
||||
kb_nm_list = kb_nm if isinstance(kb_nm, list) else [kb_nm]
|
||||
for nm_or_id in kb_nm_list:
|
||||
e, kb = KnowledgebaseService.get_by_name(nm_or_id,
|
||||
self._canvas._tenant_id)
|
||||
e, kb = KnowledgebaseService.get_by_name(nm_or_id, self._canvas._tenant_id)
|
||||
if not e:
|
||||
e, kb = KnowledgebaseService.get_by_id(nm_or_id)
|
||||
if not e:
|
||||
@@ -153,7 +148,7 @@ class Retrieval(ToolBase, ABC):
|
||||
last = 0
|
||||
|
||||
for m in pat.finditer(s):
|
||||
out_parts.append(s[last:m.start()])
|
||||
out_parts.append(s[last : m.start()])
|
||||
key = m.group(1)
|
||||
v = self._canvas.get_variable_value(key)
|
||||
if v is None:
|
||||
@@ -220,22 +215,16 @@ class Retrieval(ToolBase, ABC):
|
||||
tenant_id = self._canvas._tenant_id
|
||||
chat_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.CHAT)
|
||||
chat_mdl = LLMBundle(tenant_id, chat_model_config)
|
||||
cks = await settings.retriever.retrieval_by_toc(query, kbinfos["chunks"], [kb.tenant_id for kb in kbs],
|
||||
chat_mdl, self._param.top_n)
|
||||
cks = await settings.retriever.retrieval_by_toc(query, kbinfos["chunks"], [kb.tenant_id for kb in kbs], chat_mdl, self._param.top_n)
|
||||
if self.check_if_canceled("Retrieval processing"):
|
||||
return
|
||||
if cks:
|
||||
kbinfos["chunks"] = cks
|
||||
kbinfos["chunks"] = settings.retriever.retrieval_by_children(kbinfos["chunks"],
|
||||
[kb.tenant_id for kb in kbs])
|
||||
kbinfos["chunks"] = settings.retriever.retrieval_by_children(kbinfos["chunks"], [kb.tenant_id for kb in kbs])
|
||||
if self._param.use_kg:
|
||||
tenant_id = self._canvas.get_tenant_id()
|
||||
chat_model_config = get_tenant_default_model_by_type(tenant_id, LLMType.CHAT)
|
||||
ck = await settings.kg_retriever.retrieval(query,
|
||||
[kb.tenant_id for kb in kbs],
|
||||
kb_ids,
|
||||
embd_mdl,
|
||||
LLMBundle(tenant_id, chat_model_config))
|
||||
ck = await settings.kg_retriever.retrieval(query, [kb.tenant_id for kb in kbs], kb_ids, embd_mdl, LLMBundle(tenant_id, chat_model_config))
|
||||
if self.check_if_canceled("Retrieval processing"):
|
||||
return
|
||||
if ck["content_with_weight"]:
|
||||
@@ -245,8 +234,7 @@ class Retrieval(ToolBase, ABC):
|
||||
|
||||
if self._param.use_kg and kbs:
|
||||
chat_model_config = get_tenant_default_model_by_type(kbs[0].tenant_id, LLMType.CHAT)
|
||||
ck = await settings.kg_retriever.retrieval(query, [kb.tenant_id for kb in kbs], filtered_kb_ids, embd_mdl,
|
||||
LLMBundle(kbs[0].tenant_id, chat_model_config))
|
||||
ck = await settings.kg_retriever.retrieval(query, [kb.tenant_id for kb in kbs], filtered_kb_ids, embd_mdl, LLMBundle(kbs[0].tenant_id, chat_model_config))
|
||||
if self.check_if_canceled("Retrieval processing"):
|
||||
return
|
||||
if ck["content_with_weight"]:
|
||||
@@ -293,16 +281,14 @@ class Retrieval(ToolBase, ABC):
|
||||
filter_dict: dict = {"memory_id": memory_ids}
|
||||
if user_id:
|
||||
import re
|
||||
|
||||
# is variable
|
||||
if re.match(r"^{.*}$", user_id):
|
||||
user_id = self._canvas.get_variable_value(user_id)
|
||||
filter_dict["user_id"] = user_id
|
||||
message_list = memory_message_service.query_message(filter_dict, {
|
||||
"query": query,
|
||||
"similarity_threshold": self._param.similarity_threshold,
|
||||
"keywords_similarity_weight": self._param.keywords_similarity_weight,
|
||||
"top_n": self._param.top_n
|
||||
})
|
||||
message_list = memory_message_service.query_message(
|
||||
filter_dict, {"query": query, "similarity_threshold": self._param.similarity_threshold, "keywords_similarity_weight": self._param.keywords_similarity_weight, "top_n": self._param.top_n}
|
||||
)
|
||||
if not message_list:
|
||||
self.set_output("formalized_content", self._param.empty_response)
|
||||
return ""
|
||||
|
||||
+28
-43
@@ -28,7 +28,7 @@ class TavilySearchParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "tavily_search",
|
||||
"description": """
|
||||
Tavily is a search engine optimized for LLMs, aimed at efficient, quick and persistent search results.
|
||||
@@ -43,7 +43,7 @@ When searching:
|
||||
"type": "string",
|
||||
"description": "The search keywords to execute with Tavily. The keywords should be the most important words/terms(includes synonyms) from the original request.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
},
|
||||
"topic": {
|
||||
"type": "string",
|
||||
@@ -56,27 +56,21 @@ When searching:
|
||||
"type": "array",
|
||||
"description": "default:[]. A list of domains only from which the search results can be included.",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Domain name that must be included, e.g. www.yahoo.com"
|
||||
},
|
||||
"required": False
|
||||
"items": {"type": "string", "description": "Domain name that must be included, e.g. www.yahoo.com"},
|
||||
"required": False,
|
||||
},
|
||||
"exclude_domains": {
|
||||
"type": "array",
|
||||
"description": "default:[]. A list of domains from which the search results can not be included",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Domain name that must be excluded, e.g. www.yahoo.com"
|
||||
},
|
||||
"required": False
|
||||
"items": {"type": "string", "description": "Domain name that must be excluded, e.g. www.yahoo.com"},
|
||||
"required": False,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.api_key = ""
|
||||
self.search_depth = "basic" # basic/advanced
|
||||
self.search_depth = "basic" # basic/advanced
|
||||
self.max_results = 6
|
||||
self.days = 14
|
||||
self.include_answer = False
|
||||
@@ -91,12 +85,8 @@ When searching:
|
||||
self.check_positive_integer(self.days, "Tavily days should be greater than 1")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class TavilySearch(ToolBase, ABC):
|
||||
component_name = "TavilySearch"
|
||||
@@ -115,7 +105,7 @@ class TavilySearch(ToolBase, ABC):
|
||||
for fld in ["search_depth", "topic", "max_results", "days", "include_answer", "include_raw_content", "include_images", "include_image_descriptions", "include_domains", "exclude_domains"]:
|
||||
if fld not in kwargs:
|
||||
kwargs[fld] = getattr(self._param, fld)
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("TavilySearch processing"):
|
||||
return
|
||||
|
||||
@@ -126,11 +116,13 @@ class TavilySearch(ToolBase, ABC):
|
||||
if self.check_if_canceled("TavilySearch processing"):
|
||||
return
|
||||
|
||||
self._retrieve_chunks(res["results"],
|
||||
get_title=lambda r: r["title"],
|
||||
get_url=lambda r: r["url"],
|
||||
get_content=lambda r: r["raw_content"] if r["raw_content"] else r["content"],
|
||||
get_score=lambda r: r["score"])
|
||||
self._retrieve_chunks(
|
||||
res["results"],
|
||||
get_title=lambda r: r["title"],
|
||||
get_url=lambda r: r["url"],
|
||||
get_content=lambda r: r["raw_content"] if r["raw_content"] else r["content"],
|
||||
get_score=lambda r: r["score"],
|
||||
)
|
||||
self.set_output("json", res["results"])
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
@@ -159,7 +151,7 @@ class TavilyExtractParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "tavily_extract",
|
||||
"description": "Extract web page content from one or more specified URLs using Tavily Extract.",
|
||||
"parameters": {
|
||||
@@ -167,11 +159,8 @@ class TavilyExtractParam(ToolParamBase):
|
||||
"type": "array",
|
||||
"description": "The URLs to extract content from.",
|
||||
"default": "",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The URL to extract content from, e.g. www.yahoo.com"
|
||||
},
|
||||
"required": True
|
||||
"items": {"type": "string", "description": "The URL to extract content from, e.g. www.yahoo.com"},
|
||||
"required": True,
|
||||
},
|
||||
"extract_depth": {
|
||||
"type": "string",
|
||||
@@ -186,12 +175,12 @@ class TavilyExtractParam(ToolParamBase):
|
||||
"enum": ["markdown", "text"],
|
||||
"default": "markdown",
|
||||
"required": False,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.api_key = ""
|
||||
self.extract_depth = "basic" # basic/advanced
|
||||
self.extract_depth = "basic" # basic/advanced
|
||||
self.urls = []
|
||||
self.format = "markdown"
|
||||
self.include_images = False
|
||||
@@ -201,17 +190,13 @@ class TavilyExtractParam(ToolParamBase):
|
||||
self.check_valid_value(self.format, "Tavily extract format should be in 'markdown/text'", ["markdown", "text"])
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"urls": {
|
||||
"name": "URLs",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"urls": {"name": "URLs", "type": "line"}}
|
||||
|
||||
|
||||
class TavilyExtract(ToolBase, ABC):
|
||||
component_name = "TavilyExtract"
|
||||
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10*60)))
|
||||
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 10 * 60)))
|
||||
def _invoke(self, **kwargs):
|
||||
if self.check_if_canceled("TavilyExtract processing"):
|
||||
return
|
||||
@@ -223,7 +208,7 @@ class TavilyExtract(ToolBase, ABC):
|
||||
kwargs[fld] = getattr(self._param, fld)
|
||||
if kwargs.get("urls") and isinstance(kwargs["urls"], str):
|
||||
kwargs["urls"] = kwargs["urls"].split(",")
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("TavilyExtract processing"):
|
||||
return
|
||||
|
||||
|
||||
+8
-20
@@ -37,8 +37,7 @@ class TuShareParam(ComponentParamBase):
|
||||
self.keyword = ""
|
||||
|
||||
def check(self):
|
||||
self.check_valid_value(self.src, "Quick News Source",
|
||||
["sina", "wallstreetcn", "10jqka", "eastmoney", "yuncaijing", "fenghuang", "jinrongjie"])
|
||||
self.check_valid_value(self.src, "Quick News Source", ["sina", "wallstreetcn", "10jqka", "eastmoney", "yuncaijing", "fenghuang", "jinrongjie"])
|
||||
|
||||
|
||||
class TuShare(ComponentBase, ABC):
|
||||
@@ -58,20 +57,15 @@ class TuShare(ComponentBase, ABC):
|
||||
return
|
||||
|
||||
tus_res = []
|
||||
params = {
|
||||
"api_name": "news",
|
||||
"token": self._param.token,
|
||||
"params": {"src": self._param.src, "start_date": self._param.start_date,
|
||||
"end_date": self._param.end_date}
|
||||
}
|
||||
response = requests.post(url="http://api.tushare.pro", data=json.dumps(params).encode('utf-8'), timeout=DEFAULT_TIMEOUT)
|
||||
params = {"api_name": "news", "token": self._param.token, "params": {"src": self._param.src, "start_date": self._param.start_date, "end_date": self._param.end_date}}
|
||||
response = requests.post(url="http://api.tushare.pro", data=json.dumps(params).encode("utf-8"), timeout=DEFAULT_TIMEOUT)
|
||||
response = response.json()
|
||||
if self.check_if_canceled("TuShare processing"):
|
||||
return
|
||||
if response['code'] != 0:
|
||||
return TuShare.be_output(response['msg'])
|
||||
df = pd.DataFrame(response['data']['items'])
|
||||
df.columns = response['data']['fields']
|
||||
if response["code"] != 0:
|
||||
return TuShare.be_output(response["msg"])
|
||||
df = pd.DataFrame(response["data"]["items"])
|
||||
df.columns = response["data"]["fields"]
|
||||
if self.check_if_canceled("TuShare processing"):
|
||||
return
|
||||
keyword = self._param.keyword or ans
|
||||
@@ -79,13 +73,7 @@ class TuShare(ComponentBase, ABC):
|
||||
"TuShare news filter keyword source=%s",
|
||||
"param.keyword" if self._param.keyword else "upstream_input",
|
||||
)
|
||||
tus_res.append(
|
||||
{
|
||||
"content": (
|
||||
df[df["content"].str.contains(keyword, case=False, na=False, regex=False)]
|
||||
).to_markdown()
|
||||
}
|
||||
)
|
||||
tus_res.append({"content": (df[df["content"].str.contains(keyword, case=False, na=False, regex=False)]).to_markdown()})
|
||||
except Exception as e:
|
||||
if self.check_if_canceled("TuShare processing"):
|
||||
return
|
||||
|
||||
+7
-21
@@ -30,21 +30,14 @@ class WenCaiParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "iwencai",
|
||||
"description": """
|
||||
iwencai search: search platform is committed to providing hundreds of millions of investors with the most timely, accurate and comprehensive information, covering news, announcements, research reports, blogs, forums, Weibo, characters, etc.
|
||||
robo-advisor intelligent stock selection platform: through AI technology, is committed to providing investors with intelligent stock selection, quantitative investment, main force tracking, value investment, technical analysis and other types of stock selection technologies.
|
||||
fund selection platform: through AI technology, is committed to providing excellent fund, value investment, quantitative analysis and other fund selection technologies for foundation citizens.
|
||||
""",
|
||||
"parameters": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The question/conditions to select stocks.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
}
|
||||
}
|
||||
"parameters": {"query": {"type": "string", "description": "The question/conditions to select stocks.", "default": "{sys.query}", "required": True}},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 10
|
||||
@@ -52,18 +45,11 @@ fund selection platform: through AI technology, is committed to providing excell
|
||||
|
||||
def check(self):
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
self.check_valid_value(self.query_type, "Query type",
|
||||
['stock', 'zhishu', 'fund', 'hkstock', 'usstock', 'threeboard', 'conbond', 'insurance',
|
||||
'futures', 'lccp',
|
||||
'foreign_exchange'])
|
||||
self.check_valid_value(self.query_type, "Query type", ["stock", "zhishu", "fund", "hkstock", "usstock", "threeboard", "conbond", "insurance", "futures", "lccp", "foreign_exchange"])
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class WenCai(ToolBase, ABC):
|
||||
component_name = "WenCai"
|
||||
@@ -78,7 +64,7 @@ class WenCai(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("WenCai processing"):
|
||||
return
|
||||
|
||||
@@ -103,7 +89,7 @@ class WenCai(ToolBase, ABC):
|
||||
elif isinstance(item[1], dict):
|
||||
if "meta" in item[1].keys():
|
||||
continue
|
||||
wencai_res.append(pd.DataFrame.from_dict(item[1], orient='index').to_markdown())
|
||||
wencai_res.append(pd.DataFrame.from_dict(item[1], orient="index").to_markdown())
|
||||
elif isinstance(item[1], pd.DataFrame):
|
||||
if "image_url" in item[1].columns:
|
||||
continue
|
||||
|
||||
+83
-20
@@ -28,7 +28,7 @@ class WikipediaParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "wikipedia_search",
|
||||
"description": """A wide range of how-to and information pages are made available in wikipedia. Since 2001, it has grown rapidly to become the world's largest reference website. From Wikipedia, the free encyclopedia.""",
|
||||
"parameters": {
|
||||
@@ -36,9 +36,9 @@ class WikipediaParam(ToolParamBase):
|
||||
"type": "string",
|
||||
"description": "The search keyword to execute with wikipedia. The keyword MUST be a specific subject that can match the title.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
"required": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
super().__init__()
|
||||
self.top_n = 10
|
||||
@@ -46,20 +46,86 @@ class WikipediaParam(ToolParamBase):
|
||||
|
||||
def check(self):
|
||||
self.check_positive_integer(self.top_n, "Top N")
|
||||
self.check_valid_value(self.language, "Wikipedia languages",
|
||||
['af', 'pl', 'ar', 'ast', 'az', 'bg', 'nan', 'bn', 'be', 'ca', 'cs', 'cy', 'da', 'de',
|
||||
'et', 'el', 'en', 'es', 'eo', 'eu', 'fa', 'fr', 'gl', 'ko', 'hy', 'hi', 'hr', 'id',
|
||||
'it', 'he', 'ka', 'lld', 'la', 'lv', 'lt', 'hu', 'mk', 'arz', 'ms', 'min', 'my', 'nl',
|
||||
'ja', 'nb', 'nn', 'ce', 'uz', 'pt', 'kk', 'ro', 'ru', 'ceb', 'sk', 'sl', 'sr', 'sh',
|
||||
'fi', 'sv', 'ta', 'tt', 'th', 'tg', 'azb', 'tr', 'uk', 'ur', 'vi', 'war', 'zh', 'yue'])
|
||||
self.check_valid_value(
|
||||
self.language,
|
||||
"Wikipedia languages",
|
||||
[
|
||||
"af",
|
||||
"pl",
|
||||
"ar",
|
||||
"ast",
|
||||
"az",
|
||||
"bg",
|
||||
"nan",
|
||||
"bn",
|
||||
"be",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"et",
|
||||
"el",
|
||||
"en",
|
||||
"es",
|
||||
"eo",
|
||||
"eu",
|
||||
"fa",
|
||||
"fr",
|
||||
"gl",
|
||||
"ko",
|
||||
"hy",
|
||||
"hi",
|
||||
"hr",
|
||||
"id",
|
||||
"it",
|
||||
"he",
|
||||
"ka",
|
||||
"lld",
|
||||
"la",
|
||||
"lv",
|
||||
"lt",
|
||||
"hu",
|
||||
"mk",
|
||||
"arz",
|
||||
"ms",
|
||||
"min",
|
||||
"my",
|
||||
"nl",
|
||||
"ja",
|
||||
"nb",
|
||||
"nn",
|
||||
"ce",
|
||||
"uz",
|
||||
"pt",
|
||||
"kk",
|
||||
"ro",
|
||||
"ru",
|
||||
"ceb",
|
||||
"sk",
|
||||
"sl",
|
||||
"sr",
|
||||
"sh",
|
||||
"fi",
|
||||
"sv",
|
||||
"ta",
|
||||
"tt",
|
||||
"th",
|
||||
"tg",
|
||||
"azb",
|
||||
"tr",
|
||||
"uk",
|
||||
"ur",
|
||||
"vi",
|
||||
"war",
|
||||
"zh",
|
||||
"yue",
|
||||
],
|
||||
)
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"query": {
|
||||
"name": "Query",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"query": {"name": "Query", "type": "line"}}
|
||||
|
||||
|
||||
class Wikipedia(ToolBase, ABC):
|
||||
"""Wikipedia search tool that retrieves and processes Wikipedia articles."""
|
||||
@@ -84,7 +150,7 @@ class Wikipedia(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("Wikipedia processing"):
|
||||
return
|
||||
|
||||
@@ -104,10 +170,7 @@ class Wikipedia(ToolBase, ABC):
|
||||
logging.info(f"Wikipedia page not found: '{p}'")
|
||||
except Exception as e:
|
||||
logging.exception(f"Unexpected error fetching Wikipedia page '{p}': {e}")
|
||||
self._retrieve_chunks(pages,
|
||||
get_title=lambda r: r.title,
|
||||
get_url=lambda r: r.url,
|
||||
get_content=lambda r: r.summary)
|
||||
self._retrieve_chunks(pages, get_title=lambda r: r.title, get_url=lambda r: r.url, get_content=lambda r: r.summary)
|
||||
return self.output("formalized_content")
|
||||
except Exception as e:
|
||||
if self.check_if_canceled("Wikipedia processing"):
|
||||
|
||||
@@ -29,17 +29,10 @@ class YahooFinanceParam(ToolParamBase):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.meta:ToolMeta = {
|
||||
self.meta: ToolMeta = {
|
||||
"name": "yahoo_finance",
|
||||
"description": "The Yahoo Finance is a service that provides access to real-time and historical stock market data. It enables users to fetch various types of stock information, such as price quotes, historical prices, company profiles, and financial news. The API offers structured data, allowing developers to integrate market data into their applications and analysis tools.",
|
||||
"parameters": {
|
||||
"stock_code": {
|
||||
"type": "string",
|
||||
"description": "The stock code or company name.",
|
||||
"default": "{sys.query}",
|
||||
"required": True
|
||||
}
|
||||
}
|
||||
"parameters": {"stock_code": {"type": "string", "description": "The stock code or company name.", "default": "{sys.query}", "required": True}},
|
||||
}
|
||||
super().__init__()
|
||||
self.info = True
|
||||
@@ -62,12 +55,8 @@ class YahooFinanceParam(ToolParamBase):
|
||||
self.check_boolean(self.news, "show news")
|
||||
|
||||
def get_input_form(self) -> dict[str, dict]:
|
||||
return {
|
||||
"stock_code": {
|
||||
"name": "Stock code/Company name",
|
||||
"type": "line"
|
||||
}
|
||||
}
|
||||
return {"stock_code": {"name": "Stock code/Company name", "type": "line"}}
|
||||
|
||||
|
||||
class YahooFinance(ToolBase, ABC):
|
||||
component_name = "YahooFinance"
|
||||
@@ -82,7 +71,7 @@ class YahooFinance(ToolBase, ABC):
|
||||
return ""
|
||||
|
||||
last_e = ""
|
||||
for _ in range(self._param.max_retries+1):
|
||||
for _ in range(self._param.max_retries + 1):
|
||||
if self.check_if_canceled("YahooFinance processing"):
|
||||
return None
|
||||
|
||||
|
||||
@@ -19,14 +19,10 @@ from .oidc import OIDCClient
|
||||
from .github import GithubOAuthClient
|
||||
|
||||
|
||||
CLIENT_TYPES = {
|
||||
"oauth2": OAuthClient,
|
||||
"oidc": OIDCClient,
|
||||
"github": GithubOAuthClient
|
||||
}
|
||||
CLIENT_TYPES = {"oauth2": OAuthClient, "oidc": OIDCClient, "github": GithubOAuthClient}
|
||||
|
||||
|
||||
def get_auth_client(config)->OAuthClient:
|
||||
def get_auth_client(config) -> OAuthClient:
|
||||
channel_type = str(config.get("type", "")).lower()
|
||||
if channel_type == "":
|
||||
if config.get("issuer"):
|
||||
|
||||
+9
-11
@@ -23,15 +23,16 @@ class GithubOAuthClient(OAuthClient):
|
||||
"""
|
||||
Initialize the GithubOAuthClient with the provider's configuration.
|
||||
"""
|
||||
config.update({
|
||||
"authorization_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"userinfo_url": "https://api.github.com/user",
|
||||
"scope": "user:email"
|
||||
})
|
||||
config.update(
|
||||
{
|
||||
"authorization_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"userinfo_url": "https://api.github.com/user",
|
||||
"scope": "user:email",
|
||||
}
|
||||
)
|
||||
super().__init__(config)
|
||||
|
||||
|
||||
def fetch_user_info(self, access_token, **kwargs):
|
||||
"""
|
||||
Fetch GitHub user info (synchronous).
|
||||
@@ -42,9 +43,7 @@ class GithubOAuthClient(OAuthClient):
|
||||
response = sync_request("GET", self.userinfo_url, headers=headers, timeout=self.http_request_timeout)
|
||||
response.raise_for_status()
|
||||
user_info.update(response.json())
|
||||
email_response = sync_request(
|
||||
"GET", self.userinfo_url + "/emails", headers=headers, timeout=self.http_request_timeout
|
||||
)
|
||||
email_response = sync_request("GET", self.userinfo_url + "/emails", headers=headers, timeout=self.http_request_timeout)
|
||||
email_response.raise_for_status()
|
||||
email_info = email_response.json()
|
||||
user_info["email"] = next((email for email in email_info if email["primary"]), None)["email"]
|
||||
@@ -79,7 +78,6 @@ class GithubOAuthClient(OAuthClient):
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to fetch github user info: {e}")
|
||||
|
||||
|
||||
def normalize_user_info(self, user_info):
|
||||
email = user_info.get("email")
|
||||
username = user_info.get("login", str(email).split("@")[0])
|
||||
|
||||
+2
-12
@@ -24,7 +24,7 @@ class UserInfo:
|
||||
self.username = username
|
||||
self.nickname = nickname
|
||||
self.avatar_url = avatar_url
|
||||
|
||||
|
||||
def to_dict(self):
|
||||
return {key: value for key, value in self.__dict__.items()}
|
||||
|
||||
@@ -44,7 +44,6 @@ class OAuthClient:
|
||||
|
||||
self.http_request_timeout = 7
|
||||
|
||||
|
||||
def get_authorization_url(self, state=None):
|
||||
"""
|
||||
Generate the authorization URL for user login.
|
||||
@@ -61,19 +60,12 @@ class OAuthClient:
|
||||
authorization_url = f"{self.authorization_url}?{urllib.parse.urlencode(params)}"
|
||||
return authorization_url
|
||||
|
||||
|
||||
def exchange_code_for_token(self, code):
|
||||
"""
|
||||
Exchange authorization code for access token.
|
||||
"""
|
||||
try:
|
||||
payload = {
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
"code": code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"grant_type": "authorization_code"
|
||||
}
|
||||
payload = {"client_id": self.client_id, "client_secret": self.client_secret, "code": code, "redirect_uri": self.redirect_uri, "grant_type": "authorization_code"}
|
||||
response = sync_request(
|
||||
"POST",
|
||||
self.token_url,
|
||||
@@ -110,7 +102,6 @@ class OAuthClient:
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to exchange authorization code for token: {e}")
|
||||
|
||||
|
||||
def fetch_user_info(self, access_token, **kwargs):
|
||||
"""
|
||||
Fetch user information using access token.
|
||||
@@ -140,7 +131,6 @@ class OAuthClient:
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to fetch user info: {e}")
|
||||
|
||||
|
||||
def normalize_user_info(self, user_info):
|
||||
email = user_info.get("email")
|
||||
username = user_info.get("username", str(email).split("@")[0])
|
||||
|
||||
+25
-19
@@ -26,12 +26,20 @@ from .oauth import OAuthClient
|
||||
# forge tokens by HMAC-signing them with the public key bytes
|
||||
# (RSA/HMAC algorithm-confusion attack, CWE-347). "none" is excluded for the
|
||||
# obvious reason that it disables signature verification entirely.
|
||||
_ALLOWED_OIDC_SIGNING_ALGS = frozenset({
|
||||
"RS256", "RS384", "RS512",
|
||||
"ES256", "ES384", "ES512",
|
||||
"PS256", "PS384", "PS512",
|
||||
"EdDSA",
|
||||
})
|
||||
_ALLOWED_OIDC_SIGNING_ALGS = frozenset(
|
||||
{
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"EdDSA",
|
||||
}
|
||||
)
|
||||
|
||||
# OIDC Core 1.0 § 2 makes RS256 the spec-default ``id_token_signing_alg``,
|
||||
# so this is the safe fallback when a provider's discovery document does not
|
||||
@@ -69,24 +77,25 @@ class OIDCClient(OAuthClient):
|
||||
raise ValueError("Missing issuer in configuration.")
|
||||
|
||||
oidc_metadata = self._load_oidc_metadata(self.issuer)
|
||||
config.update({
|
||||
'issuer': oidc_metadata['issuer'],
|
||||
'jwks_uri': oidc_metadata['jwks_uri'],
|
||||
'authorization_url': oidc_metadata['authorization_endpoint'],
|
||||
'token_url': oidc_metadata['token_endpoint'],
|
||||
'userinfo_url': oidc_metadata['userinfo_endpoint']
|
||||
})
|
||||
config.update(
|
||||
{
|
||||
"issuer": oidc_metadata["issuer"],
|
||||
"jwks_uri": oidc_metadata["jwks_uri"],
|
||||
"authorization_url": oidc_metadata["authorization_endpoint"],
|
||||
"token_url": oidc_metadata["token_endpoint"],
|
||||
"userinfo_url": oidc_metadata["userinfo_endpoint"],
|
||||
}
|
||||
)
|
||||
|
||||
super().__init__(config)
|
||||
self.issuer = config['issuer']
|
||||
self.jwks_uri = config['jwks_uri']
|
||||
self.issuer = config["issuer"]
|
||||
self.jwks_uri = config["jwks_uri"]
|
||||
# Pin the accepted ID-token signing algorithms at construction time
|
||||
# from a trusted source (provider metadata + safe allowlist) so the
|
||||
# JWT verification step in :meth:`parse_id_token` cannot be tricked
|
||||
# by attacker-controlled JWT headers (CWE-345 / CWE-347).
|
||||
self.id_token_signing_algs = _resolve_id_token_signing_algs(oidc_metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _load_oidc_metadata(issuer):
|
||||
"""
|
||||
@@ -100,7 +109,6 @@ class OIDCClient(OAuthClient):
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to fetch OIDC metadata: {e}")
|
||||
|
||||
|
||||
def parse_id_token(self, id_token):
|
||||
"""
|
||||
Parse and validate OIDC ID Token (JWT format) with signature verification.
|
||||
@@ -134,7 +142,6 @@ class OIDCClient(OAuthClient):
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error parsing ID Token: {e}")
|
||||
|
||||
|
||||
def fetch_user_info(self, access_token, id_token=None, **kwargs):
|
||||
"""
|
||||
Fetch user info.
|
||||
@@ -152,6 +159,5 @@ class OIDCClient(OAuthClient):
|
||||
user_info.update((await super().async_fetch_user_info(access_token)).to_dict())
|
||||
return self.normalize_user_info(user_info)
|
||||
|
||||
|
||||
def normalize_user_info(self, user_info):
|
||||
return super().normalize_user_info(user_info)
|
||||
|
||||
+77
-103
@@ -44,6 +44,7 @@ Deprecated APIs and their replacements:
|
||||
- POST /api/v1/sessions/related_questions -> POST /api/v1/chat/recommandation
|
||||
- PUT (chunk update) -> PATCH (chunk update)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
@@ -68,6 +69,7 @@ def _index_result(success, result):
|
||||
# System APIs
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@legacy_v1_manager.route("/system/healthz", methods=["GET"])
|
||||
async def deprecated_system_healthz():
|
||||
"""
|
||||
@@ -76,17 +78,16 @@ async def deprecated_system_healthz():
|
||||
Old path: GET /v1/system/healthz
|
||||
New path: GET /api/v1/system/healthz
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /v1/system/healthz is deprecated. "
|
||||
"Please use /api/v1/system/healthz instead."
|
||||
)
|
||||
logging.warning("API endpoint /v1/system/healthz is deprecated. Please use /api/v1/system/healthz instead.")
|
||||
result, all_ok = run_health_checks()
|
||||
return jsonify(result), (200 if all_ok else 500)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Chat Completion APIs
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/chats/<chat_id>/completions", methods=["POST"])
|
||||
@login_required
|
||||
async def deprecated_chat_completions(chat_id):
|
||||
@@ -97,8 +98,7 @@ async def deprecated_chat_completions(chat_id):
|
||||
New path: POST /api/v1/chat/completions
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/chats/%s/completions is deprecated. "
|
||||
"Please use /api/v1/chat/completions instead.",
|
||||
"API endpoint /api/v1/chats/%s/completions is deprecated. Please use /api/v1/chat/completions instead.",
|
||||
chat_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
@@ -115,9 +115,9 @@ async def deprecated_openai_chat_completions(chat_id):
|
||||
New path: POST /api/v1/openai/{chat_id}/chat/completions
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/chats_openai/%s/chat/completions is deprecated. "
|
||||
"Please use /api/v1/openai/%s/chat/completions instead.",
|
||||
chat_id, chat_id,
|
||||
"API endpoint /api/v1/chats_openai/%s/chat/completions is deprecated. Please use /api/v1/openai/%s/chat/completions instead.",
|
||||
chat_id,
|
||||
chat_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
return await openai_api.openai_chat_completions(chat_id)
|
||||
@@ -134,8 +134,7 @@ async def deprecated_agents_openai_chat_completions(agent_id, tenant_id=None):
|
||||
New path: POST /api/v1/agents/chat/completions
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/agents_openai/%s/chat/completions is deprecated. "
|
||||
"Please use /api/v1/agents/chat/completions with `openai-compatible` instead.",
|
||||
"API endpoint /api/v1/agents_openai/%s/chat/completions is deprecated. Please use /api/v1/agents/chat/completions with `openai-compatible` instead.",
|
||||
agent_id,
|
||||
)
|
||||
req = dict(await get_request_json())
|
||||
@@ -148,6 +147,7 @@ async def deprecated_agents_openai_chat_completions(agent_id, tenant_id=None):
|
||||
# Dataset Graph and Index APIs
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/knowledge_graph", methods=["GET"])
|
||||
@login_required
|
||||
async def deprecated_get_knowledge_graph(dataset_id):
|
||||
@@ -158,9 +158,9 @@ async def deprecated_get_knowledge_graph(dataset_id):
|
||||
New path: GET /api/v1/datasets/{dataset_id}/graph
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/datasets/%s/knowledge_graph is deprecated. "
|
||||
"Please use /api/v1/datasets/%s/graph instead.",
|
||||
dataset_id, dataset_id,
|
||||
"API endpoint /api/v1/datasets/%s/knowledge_graph is deprecated. Please use /api/v1/datasets/%s/graph instead.",
|
||||
dataset_id,
|
||||
dataset_id,
|
||||
)
|
||||
return await dataset_api.get_knowledge_graph(dataset_id=dataset_id)
|
||||
|
||||
@@ -175,9 +175,9 @@ async def deprecated_delete_knowledge_graph(dataset_id):
|
||||
New path: DELETE /api/v1/datasets/{dataset_id}/graph
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint DELETE /api/v1/datasets/%s/knowledge_graph is deprecated. "
|
||||
"Please use DELETE /api/v1/datasets/%s/graph instead.",
|
||||
dataset_id, dataset_id,
|
||||
"API endpoint DELETE /api/v1/datasets/%s/knowledge_graph is deprecated. Please use DELETE /api/v1/datasets/%s/graph instead.",
|
||||
dataset_id,
|
||||
dataset_id,
|
||||
)
|
||||
return await dataset_api.delete_knowledge_graph(dataset_id=dataset_id)
|
||||
|
||||
@@ -193,9 +193,9 @@ async def deprecated_run_graphrag(dataset_id, tenant_id=None):
|
||||
New path: POST /api/v1/datasets/{dataset_id}/index?type=graph
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/datasets/%s/run_graphrag is deprecated. "
|
||||
"Please use /api/v1/datasets/%s/index?type=graph instead.",
|
||||
dataset_id, dataset_id,
|
||||
"API endpoint /api/v1/datasets/%s/run_graphrag is deprecated. Please use /api/v1/datasets/%s/index?type=graph instead.",
|
||||
dataset_id,
|
||||
dataset_id,
|
||||
)
|
||||
return _index_result(*dataset_api_service.run_index(dataset_id, tenant_id, "graph"))
|
||||
|
||||
@@ -211,9 +211,9 @@ async def deprecated_trace_graphrag(dataset_id, tenant_id=None):
|
||||
New path: GET /api/v1/datasets/{dataset_id}/index?type=graph
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/datasets/%s/trace_graphrag is deprecated. "
|
||||
"Please use /api/v1/datasets/%s/index?type=graph instead.",
|
||||
dataset_id, dataset_id,
|
||||
"API endpoint /api/v1/datasets/%s/trace_graphrag is deprecated. Please use /api/v1/datasets/%s/index?type=graph instead.",
|
||||
dataset_id,
|
||||
dataset_id,
|
||||
)
|
||||
return _index_result(*dataset_api_service.trace_index(dataset_id, tenant_id, "graph"))
|
||||
|
||||
@@ -229,9 +229,9 @@ async def deprecated_run_raptor(dataset_id, tenant_id=None):
|
||||
New path: POST /api/v1/datasets/{dataset_id}/index?type=raptor
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/datasets/%s/run_raptor is deprecated. "
|
||||
"Please use /api/v1/datasets/%s/index?type=raptor instead.",
|
||||
dataset_id, dataset_id,
|
||||
"API endpoint /api/v1/datasets/%s/run_raptor is deprecated. Please use /api/v1/datasets/%s/index?type=raptor instead.",
|
||||
dataset_id,
|
||||
dataset_id,
|
||||
)
|
||||
return _index_result(*dataset_api_service.run_index(dataset_id, tenant_id, "raptor"))
|
||||
|
||||
@@ -247,9 +247,9 @@ async def deprecated_trace_raptor(dataset_id, tenant_id=None):
|
||||
New path: GET /api/v1/datasets/{dataset_id}/index?type=raptor
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/datasets/%s/trace_raptor is deprecated. "
|
||||
"Please use /api/v1/datasets/%s/index?type=raptor instead.",
|
||||
dataset_id, dataset_id,
|
||||
"API endpoint /api/v1/datasets/%s/trace_raptor is deprecated. Please use /api/v1/datasets/%s/index?type=raptor instead.",
|
||||
dataset_id,
|
||||
dataset_id,
|
||||
)
|
||||
return _index_result(*dataset_api_service.trace_index(dataset_id, tenant_id, "raptor"))
|
||||
|
||||
@@ -258,6 +258,7 @@ async def deprecated_trace_raptor(dataset_id, tenant_id=None):
|
||||
# Chat Session APIs
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/chats/<chat_id>/sessions/<session_id>", methods=["PUT"])
|
||||
@login_required
|
||||
async def deprecated_update_session(chat_id, session_id):
|
||||
@@ -268,9 +269,11 @@ async def deprecated_update_session(chat_id, session_id):
|
||||
New path: PATCH /api/v1/chats/{chat_id}/sessions/{session_id}
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint PUT /api/v1/chats/%s/sessions/%s is deprecated. "
|
||||
"Please use PATCH /api/v1/chats/%s/sessions/%s instead.",
|
||||
chat_id, session_id, chat_id, session_id,
|
||||
"API endpoint PUT /api/v1/chats/%s/sessions/%s is deprecated. Please use PATCH /api/v1/chats/%s/sessions/%s instead.",
|
||||
chat_id,
|
||||
session_id,
|
||||
chat_id,
|
||||
session_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
return await chat_api.update_session(chat_id, session_id)
|
||||
@@ -280,6 +283,7 @@ async def deprecated_update_session(chat_id, session_id):
|
||||
# File APIs (Old /api/v1/file/* -> New /api/v1/files*)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/file/get/<file_id>", methods=["GET"])
|
||||
@login_required
|
||||
async def deprecated_file_get(file_id):
|
||||
@@ -290,9 +294,9 @@ async def deprecated_file_get(file_id):
|
||||
New path: GET /api/v1/files/{file_id}
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/get/%s is deprecated. "
|
||||
"Please use /api/v1/files/%s instead.",
|
||||
file_id, file_id,
|
||||
"API endpoint /api/v1/file/get/%s is deprecated. Please use /api/v1/files/%s instead.",
|
||||
file_id,
|
||||
file_id,
|
||||
)
|
||||
# Forward to the new API implementation (download)
|
||||
return await file_api.download(file_id=file_id)
|
||||
@@ -307,10 +311,7 @@ async def deprecated_file_list():
|
||||
Old path: GET /api/v1/file/list?...
|
||||
New path: GET /api/v1/files?...
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/list is deprecated. "
|
||||
"Please use /api/v1/files instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/list is deprecated. Please use /api/v1/files instead.")
|
||||
# Forward to the new API implementation
|
||||
return await file_api.list_files()
|
||||
|
||||
@@ -328,8 +329,7 @@ async def deprecated_file_all_parent_folder():
|
||||
if not file_id:
|
||||
return get_data_error_result(message="`file_id` query parameter is required")
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/all_parent_folder is deprecated. "
|
||||
"Please use /api/v1/files/%s/ancestors instead.",
|
||||
"API endpoint /api/v1/file/all_parent_folder is deprecated. Please use /api/v1/files/%s/ancestors instead.",
|
||||
file_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
@@ -349,8 +349,7 @@ async def deprecated_file_parent_folder():
|
||||
if not file_id:
|
||||
return get_data_error_result(message="`file_id` query parameter is required")
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/parent_folder is deprecated. "
|
||||
"Please use /api/v1/files/%s/parent instead.",
|
||||
"API endpoint /api/v1/file/parent_folder is deprecated. Please use /api/v1/files/%s/parent instead.",
|
||||
file_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
@@ -366,10 +365,7 @@ async def deprecated_file_root_folder():
|
||||
Old path: GET /api/v1/file/root_folder
|
||||
New path: GET /api/v1/files?parent_id=<root_id>
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/root_folder is deprecated. "
|
||||
"Please use /api/v1/files with appropriate parent_id instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/root_folder is deprecated. Please use /api/v1/files with appropriate parent_id instead.")
|
||||
# Forward to the new API implementation with empty parent_id to get root
|
||||
return await file_api.list_files()
|
||||
|
||||
@@ -384,10 +380,7 @@ async def deprecated_file_create(tenant_id=None):
|
||||
Old path: POST /api/v1/file/create
|
||||
New path: POST /api/v1/files
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/create is deprecated. "
|
||||
"Please use POST /api/v1/files instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/create is deprecated. Please use POST /api/v1/files instead.")
|
||||
# Forward to the new API implementation
|
||||
return await file_api.create_or_upload(tenant_id=tenant_id)
|
||||
|
||||
@@ -402,10 +395,7 @@ async def deprecated_file_upload(tenant_id=None):
|
||||
Old path: POST /api/v1/file/upload
|
||||
New path: POST /api/v1/files
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/upload is deprecated. "
|
||||
"Please use POST /api/v1/files with multipart/form-data instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/upload is deprecated. Please use POST /api/v1/files with multipart/form-data instead.")
|
||||
# Forward to the new API implementation
|
||||
return await file_api.create_or_upload(tenant_id=tenant_id)
|
||||
|
||||
@@ -419,10 +409,7 @@ async def deprecated_file_convert():
|
||||
Old path: POST /api/v1/file/convert
|
||||
New path: POST /api/v1/files/link-to-datasets
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/convert is deprecated. "
|
||||
"Please use POST /api/v1/files/link-to-datasets instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/convert is deprecated. Please use POST /api/v1/files/link-to-datasets instead.")
|
||||
return await file2document_api.convert()
|
||||
|
||||
|
||||
@@ -436,10 +423,7 @@ async def deprecated_file_mv(tenant_id=None):
|
||||
Old path: POST /api/v1/file/mv
|
||||
New path: POST /api/v1/files/move
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/mv is deprecated. "
|
||||
"Please use POST /api/v1/files/move instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/mv is deprecated. Please use POST /api/v1/files/move instead.")
|
||||
# Forward to the new API implementation
|
||||
return await file_api.move(tenant_id=tenant_id)
|
||||
|
||||
@@ -454,10 +438,7 @@ async def deprecated_file_rename(tenant_id=None):
|
||||
Old path: POST /api/v1/file/rename
|
||||
New path: POST /api/v1/files/move
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/rename is deprecated. "
|
||||
"Please use POST /api/v1/files/move with `new_name` instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/rename is deprecated. Please use POST /api/v1/files/move with `new_name` instead.")
|
||||
# Transform the old API format to new format
|
||||
req = await request.get_json()
|
||||
# Old API used `file_id` and `name`, new API uses `src_file_ids` and `new_name`
|
||||
@@ -465,9 +446,7 @@ async def deprecated_file_rename(tenant_id=None):
|
||||
new_name = req.get("name")
|
||||
# Call the underlying service directly with transformed data
|
||||
try:
|
||||
success, result = await file_api_service.move_files(
|
||||
tenant_id, src_file_ids, None, new_name
|
||||
)
|
||||
success, result = await file_api_service.move_files(tenant_id, src_file_ids, None, new_name)
|
||||
if success:
|
||||
return get_json_result(data=result)
|
||||
else:
|
||||
@@ -487,10 +466,7 @@ async def deprecated_file_rm(tenant_id=None):
|
||||
Old path: POST /api/v1/file/rm
|
||||
New path: DELETE /api/v1/files
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/rm is deprecated. "
|
||||
"Please use DELETE /api/v1/files instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/rm is deprecated. Please use DELETE /api/v1/files instead.")
|
||||
# Transform POST with body to DELETE behavior
|
||||
# The new API expects a JSON body with `ids`
|
||||
return await file_api.delete(tenant_id=tenant_id)
|
||||
@@ -500,6 +476,7 @@ async def deprecated_file_rm(tenant_id=None):
|
||||
# Related Questions API
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/sessions/related_questions", methods=["POST"])
|
||||
@login_required
|
||||
async def deprecated_related_questions():
|
||||
@@ -509,10 +486,7 @@ async def deprecated_related_questions():
|
||||
Old path: POST /api/v1/sessions/related_questions
|
||||
New path: POST /api/v1/chat/recommendation
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/sessions/related_questions is deprecated. "
|
||||
"Please use /api/v1/chat/recommendation instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/sessions/related_questions is deprecated. Please use /api/v1/chat/recommendation instead.")
|
||||
# Forward to the new API implementation
|
||||
return await chat_api.recommendation()
|
||||
|
||||
@@ -521,6 +495,7 @@ async def deprecated_related_questions():
|
||||
# Chunk Update API (PUT -> PATCH)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents/<document_id>/chunks/<chunk_id>", methods=["PUT"])
|
||||
@login_required
|
||||
async def deprecated_update_chunk(dataset_id, document_id, chunk_id):
|
||||
@@ -531,9 +506,10 @@ async def deprecated_update_chunk(dataset_id, document_id, chunk_id):
|
||||
New path: PATCH /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks/{chunk_id}
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint PUT /api/v1/datasets/%s/documents/%s/chunks/%s is deprecated. "
|
||||
"Please use PATCH instead.",
|
||||
dataset_id, document_id, chunk_id,
|
||||
"API endpoint PUT /api/v1/datasets/%s/documents/%s/chunks/%s is deprecated. Please use PATCH instead.",
|
||||
dataset_id,
|
||||
document_id,
|
||||
chunk_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
return await chunk_api.update_chunk(dataset_id=dataset_id, document_id=document_id, chunk_id=chunk_id)
|
||||
@@ -543,6 +519,7 @@ async def deprecated_update_chunk(dataset_id, document_id, chunk_id):
|
||||
# File Upload Info API
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/file/upload_info", methods=["POST"])
|
||||
@login_required
|
||||
async def deprecated_file_upload_info():
|
||||
@@ -554,10 +531,7 @@ async def deprecated_file_upload_info():
|
||||
"""
|
||||
from api.apps import current_user
|
||||
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/file/upload_info is deprecated. "
|
||||
"Please use POST /api/v1/documents/upload instead."
|
||||
)
|
||||
logging.warning("API endpoint /api/v1/file/upload_info is deprecated. Please use POST /api/v1/documents/upload instead.")
|
||||
# Forward to the new API implementation
|
||||
# Need to pass tenant_id explicitly since we're calling the function directly
|
||||
tenant_id = current_user.id
|
||||
@@ -575,10 +549,7 @@ async def deprecated_legacy_document_upload_info():
|
||||
"""
|
||||
from api.apps import current_user
|
||||
|
||||
logging.warning(
|
||||
"API endpoint /v1/document/upload_info is deprecated. "
|
||||
"Please use POST /api/v1/documents/upload instead."
|
||||
)
|
||||
logging.warning("API endpoint /v1/document/upload_info is deprecated. Please use POST /api/v1/documents/upload instead.")
|
||||
tenant_id = current_user.id
|
||||
return await document_api.upload_info(tenant_id=tenant_id)
|
||||
|
||||
@@ -587,6 +558,7 @@ async def deprecated_legacy_document_upload_info():
|
||||
# Document APIs
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents/<document_id>", methods=["PUT"])
|
||||
@login_required
|
||||
async def deprecated_update_document(dataset_id, document_id):
|
||||
@@ -597,9 +569,9 @@ async def deprecated_update_document(dataset_id, document_id):
|
||||
New path: PATCH /api/v1/datasets/{dataset_id}/documents/{document_id}
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint PUT /api/v1/datasets/%s/documents/%s is deprecated. "
|
||||
"Please use PATCH instead.",
|
||||
dataset_id, document_id,
|
||||
"API endpoint PUT /api/v1/datasets/%s/documents/%s is deprecated. Please use PATCH instead.",
|
||||
dataset_id,
|
||||
document_id,
|
||||
)
|
||||
# Forward to the new API implementation
|
||||
return await document_api.update_document(dataset_id=dataset_id, document_id=document_id)
|
||||
@@ -615,9 +587,9 @@ async def deprecated_document_get(doc_id):
|
||||
New path: GET /api/v1/documents/{doc_id}/preview
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/document/get/%s is deprecated. "
|
||||
"Please use /api/v1/documents/%s/preview instead.",
|
||||
doc_id, doc_id,
|
||||
"API endpoint /api/v1/document/get/%s is deprecated. Please use /api/v1/documents/%s/preview instead.",
|
||||
doc_id,
|
||||
doc_id,
|
||||
)
|
||||
return await document_api.get(doc_id)
|
||||
|
||||
@@ -632,9 +604,9 @@ async def deprecated_document_download(doc_id):
|
||||
New path: GET /api/v1/agents/attachments/{doc_id}/download
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/document/download/%s is deprecated. "
|
||||
"Please use /api/v1/agents/attachments/%s/download instead.",
|
||||
doc_id, doc_id,
|
||||
"API endpoint /api/v1/document/download/%s is deprecated. Please use /api/v1/agents/attachments/%s/download instead.",
|
||||
doc_id,
|
||||
doc_id,
|
||||
)
|
||||
return await agent_api.download_attachment(attachment_id=doc_id)
|
||||
|
||||
@@ -649,16 +621,18 @@ async def document_download_v1(attachment_id):
|
||||
New path: GET /api/v1/agents/attachments/{attachment_id}/download
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /v1/document/download/%s is deprecated. "
|
||||
"Please use /api/v1/agents/attachments/%s/download instead.",
|
||||
attachment_id, attachment_id,
|
||||
"API endpoint /v1/document/download/%s is deprecated. Please use /api/v1/agents/attachments/%s/download instead.",
|
||||
attachment_id,
|
||||
attachment_id,
|
||||
)
|
||||
return await agent_api.download_attachment(attachment_id=attachment_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Agent Chat API
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@manager.route("/agents/<agent_id>/completions", methods=["POST"])
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
@@ -670,12 +644,12 @@ async def deprecated_agent_completions(agent_id, tenant_id=None):
|
||||
New path: POST /api/v1/agents/chat/completions
|
||||
"""
|
||||
logging.warning(
|
||||
"API endpoint /api/v1/agents/%s/completions is deprecated. "
|
||||
"Please use /api/v1/agents/chat/completions instead.",
|
||||
"API endpoint /api/v1/agents/%s/completions is deprecated. Please use /api/v1/agents/chat/completions instead.",
|
||||
agent_id,
|
||||
)
|
||||
return await agent_api.agent_chat_completion(tenant_id=tenant_id, agent_id=agent_id)
|
||||
|
||||
|
||||
def register_backward_compat_routes(app_instance):
|
||||
"""
|
||||
Register all backward compatibility routes with the app.
|
||||
|
||||
+78
-139
@@ -50,27 +50,25 @@ from api.apps.services.canvas_replica_service import CanvasReplicaService
|
||||
from api.db.services.canvas_service import completion as agent_completion
|
||||
|
||||
|
||||
@manager.route('/templates', methods=['GET']) # noqa: F821
|
||||
@manager.route("/templates", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def templates():
|
||||
return get_json_result(data=[c.to_dict() for c in CanvasTemplateService.get_all()])
|
||||
|
||||
|
||||
@manager.route('/rm', methods=['POST']) # noqa: F821
|
||||
@manager.route("/rm", methods=["POST"]) # noqa: F821
|
||||
@validate_request("canvas_ids")
|
||||
@login_required
|
||||
async def rm():
|
||||
req = await get_request_json()
|
||||
for i in req["canvas_ids"]:
|
||||
if not UserCanvasService.accessible(i, current_user.id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
UserCanvasService.delete_by_id(i)
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/set', methods=['POST']) # noqa: F821
|
||||
@manager.route("/set", methods=["POST"]) # noqa: F821
|
||||
@validate_request("dsl", "title")
|
||||
@login_required
|
||||
async def save():
|
||||
@@ -89,9 +87,7 @@ async def save():
|
||||
return get_data_error_result(message="Fail to save canvas.")
|
||||
else:
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
UserCanvasService.update_by_id(req["id"], req)
|
||||
# save version
|
||||
UserCanvasVersionService.save_or_replace_latest(
|
||||
@@ -112,7 +108,7 @@ async def save():
|
||||
return get_json_result(data=req)
|
||||
|
||||
|
||||
@manager.route('/get/<canvas_id>', methods=['GET']) # noqa: F821
|
||||
@manager.route("/get/<canvas_id>", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def get(canvas_id):
|
||||
if not UserCanvasService.accessible(canvas_id, current_user.id):
|
||||
@@ -135,29 +131,25 @@ def get(canvas_id):
|
||||
return get_json_result(data=c)
|
||||
|
||||
|
||||
@manager.route('/getsse/<canvas_id>', methods=['GET']) # type: ignore # noqa: F821
|
||||
@manager.route("/getsse/<canvas_id>", methods=["GET"]) # type: ignore # noqa: F821
|
||||
def getsse(canvas_id):
|
||||
token = request.headers.get('Authorization').split()
|
||||
token = request.headers.get("Authorization").split()
|
||||
if len(token) != 2:
|
||||
return get_data_error_result(message='Authorization is not valid!')
|
||||
return get_data_error_result(message="Authorization is not valid!")
|
||||
token = token[1]
|
||||
objs = APIToken.query(beta=token)
|
||||
if not objs:
|
||||
return get_data_error_result(message='Authentication error: API key is invalid!"')
|
||||
tenant_id = objs[0].tenant_id
|
||||
if not UserCanvasService.query(user_id=tenant_id, id=canvas_id):
|
||||
return get_json_result(
|
||||
data=False,
|
||||
message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR
|
||||
)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
e, c = UserCanvasService.get_by_id(canvas_id)
|
||||
if not e or c.user_id != tenant_id:
|
||||
return get_data_error_result(message="canvas not found.")
|
||||
return get_json_result(data=c.to_dict())
|
||||
|
||||
|
||||
@manager.route('/completion', methods=['POST']) # noqa: F821
|
||||
@manager.route("/completion", methods=["POST"]) # noqa: F821
|
||||
@validate_request("id")
|
||||
@login_required
|
||||
async def run():
|
||||
@@ -169,9 +161,7 @@ async def run():
|
||||
runtime_user_id = req.get("user_id") or tenant_id
|
||||
user_id = str(runtime_user_id)
|
||||
if not await thread_pool_exec(UserCanvasService.accessible, req["id"], tenant_id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
replica_payload = CanvasReplicaService.load_for_run(
|
||||
canvas_id=req["id"],
|
||||
@@ -234,7 +224,7 @@ async def run():
|
||||
resp.headers.add_header("Connection", "keep-alive")
|
||||
resp.headers.add_header("X-Accel-Buffering", "no")
|
||||
resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
#resp.call_on_close(lambda: canvas.cancel_task())
|
||||
# resp.call_on_close(lambda: canvas.cancel_task())
|
||||
return resp
|
||||
|
||||
|
||||
@@ -244,6 +234,7 @@ async def exp_agent_completion(canvas_id):
|
||||
tenant_id = current_user.id
|
||||
req = await get_request_json()
|
||||
return_trace = bool(req.get("return_trace", False))
|
||||
|
||||
async def generate():
|
||||
trace_items = []
|
||||
async for answer in agent_completion(tenant_id=tenant_id, agent_id=canvas_id, **req):
|
||||
@@ -280,9 +271,9 @@ async def exp_agent_completion(canvas_id):
|
||||
resp.headers.add_header("X-Accel-Buffering", "no")
|
||||
resp.headers.add_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
return resp
|
||||
|
||||
|
||||
@manager.route('/rerun', methods=['POST']) # noqa: F821
|
||||
|
||||
@manager.route("/rerun", methods=["POST"]) # noqa: F821
|
||||
@validate_request("id", "dsl", "component_id")
|
||||
@login_required
|
||||
async def rerun():
|
||||
@@ -310,7 +301,7 @@ async def rerun():
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/cancel/<task_id>', methods=['PUT']) # noqa: F821
|
||||
@manager.route("/cancel/<task_id>", methods=["PUT"]) # noqa: F821
|
||||
@login_required
|
||||
def cancel(task_id):
|
||||
try:
|
||||
@@ -320,15 +311,13 @@ def cancel(task_id):
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@manager.route('/reset', methods=['POST']) # noqa: F821
|
||||
@manager.route("/reset", methods=["POST"]) # noqa: F821
|
||||
@validate_request("id")
|
||||
@login_required
|
||||
async def reset():
|
||||
req = await get_request_json()
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
try:
|
||||
e, user_canvas = UserCanvasService.get_by_id(req["id"])
|
||||
if not e:
|
||||
@@ -361,7 +350,7 @@ async def upload(canvas_id):
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/input_form', methods=['GET']) # noqa: F821
|
||||
@manager.route("/input_form", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def input_form():
|
||||
cvs_id = request.args.get("id")
|
||||
@@ -371,9 +360,7 @@ def input_form():
|
||||
if not e:
|
||||
return get_data_error_result(message="canvas not found.")
|
||||
if not UserCanvasService.query(user_id=current_user.id, id=cvs_id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id, canvas_id=user_canvas.id)
|
||||
return get_json_result(data=canvas.get_component_input_form(cpn_id))
|
||||
@@ -381,15 +368,13 @@ def input_form():
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/debug', methods=['POST']) # noqa: F821
|
||||
@manager.route("/debug", methods=["POST"]) # noqa: F821
|
||||
@validate_request("id", "component_id", "params")
|
||||
@login_required
|
||||
async def debug():
|
||||
req = await get_request_json()
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
try:
|
||||
e, user_canvas = UserCanvasService.get_by_id(req["id"])
|
||||
canvas = Canvas(json.dumps(user_canvas.dsl), current_user.id, canvas_id=user_canvas.id)
|
||||
@@ -400,7 +385,7 @@ async def debug():
|
||||
|
||||
if isinstance(component, LLM):
|
||||
component.set_debug_inputs(req["params"])
|
||||
component.invoke(**{k: o["value"] for k,o in req["params"].items()})
|
||||
component.invoke(**{k: o["value"] for k, o in req["params"].items()})
|
||||
outputs = component.output()
|
||||
for k in outputs.keys():
|
||||
if isinstance(outputs[k], partial):
|
||||
@@ -418,59 +403,39 @@ async def debug():
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/test_db_connect', methods=['POST']) # noqa: F821
|
||||
@manager.route("/test_db_connect", methods=["POST"]) # noqa: F821
|
||||
@validate_request("db_type", "database", "username", "host", "port", "password")
|
||||
@login_required
|
||||
async def test_db_connect():
|
||||
req = await get_request_json()
|
||||
try:
|
||||
if req["db_type"] in ["mysql", "mariadb"]:
|
||||
db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"],
|
||||
password=req["password"])
|
||||
db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"], password=req["password"])
|
||||
elif req["db_type"] == "oceanbase":
|
||||
db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"],
|
||||
password=req["password"], charset="utf8mb4")
|
||||
elif req["db_type"] == 'postgres':
|
||||
db = PostgresqlDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"],
|
||||
password=req["password"])
|
||||
elif req["db_type"] == 'mssql':
|
||||
db = MySQLDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"], password=req["password"], charset="utf8mb4")
|
||||
elif req["db_type"] == "postgres":
|
||||
db = PostgresqlDatabase(req["database"], user=req["username"], host=req["host"], port=req["port"], password=req["password"])
|
||||
elif req["db_type"] == "mssql":
|
||||
import pyodbc
|
||||
connection_string = (
|
||||
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
||||
f"SERVER={req['host']},{req['port']};"
|
||||
f"DATABASE={req['database']};"
|
||||
f"UID={req['username']};"
|
||||
f"PWD={req['password']};"
|
||||
)
|
||||
|
||||
connection_string = f"DRIVER={{ODBC Driver 17 for SQL Server}};SERVER={req['host']},{req['port']};DATABASE={req['database']};UID={req['username']};PWD={req['password']};"
|
||||
db = pyodbc.connect(connection_string)
|
||||
cursor = db.cursor()
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.close()
|
||||
elif req["db_type"] == 'IBM DB2':
|
||||
elif req["db_type"] == "IBM DB2":
|
||||
import ibm_db
|
||||
conn_str = (
|
||||
f"DATABASE={req['database']};"
|
||||
f"HOSTNAME={req['host']};"
|
||||
f"PORT={req['port']};"
|
||||
f"PROTOCOL=TCPIP;"
|
||||
f"UID={req['username']};"
|
||||
f"PWD={req['password']};"
|
||||
)
|
||||
redacted_conn_str = (
|
||||
f"DATABASE={req['database']};"
|
||||
f"HOSTNAME={req['host']};"
|
||||
f"PORT={req['port']};"
|
||||
f"PROTOCOL=TCPIP;"
|
||||
f"UID={req['username']};"
|
||||
f"PWD=****;"
|
||||
)
|
||||
|
||||
conn_str = f"DATABASE={req['database']};HOSTNAME={req['host']};PORT={req['port']};PROTOCOL=TCPIP;UID={req['username']};PWD={req['password']};"
|
||||
redacted_conn_str = f"DATABASE={req['database']};HOSTNAME={req['host']};PORT={req['port']};PROTOCOL=TCPIP;UID={req['username']};PWD=****;"
|
||||
logging.info(redacted_conn_str)
|
||||
conn = ibm_db.connect(conn_str, "", "")
|
||||
stmt = ibm_db.exec_immediate(conn, "SELECT 1 FROM sysibm.sysdummy1")
|
||||
ibm_db.fetch_assoc(stmt)
|
||||
ibm_db.close(conn)
|
||||
return get_json_result(data="Database Connection Successful!")
|
||||
elif req["db_type"] == 'trino':
|
||||
elif req["db_type"] == "trino":
|
||||
|
||||
def _parse_catalog_schema(db_name: str):
|
||||
if not db_name:
|
||||
return None, None
|
||||
@@ -481,6 +446,7 @@ async def test_db_connect():
|
||||
else:
|
||||
catalog_name, schema_name = db_name, "default"
|
||||
return catalog_name, schema_name
|
||||
|
||||
try:
|
||||
import trino
|
||||
import os
|
||||
@@ -498,13 +464,7 @@ async def test_db_connect():
|
||||
auth = trino.BasicAuthentication(req.get("username") or "ragflow", req["password"])
|
||||
|
||||
conn = trino.dbapi.connect(
|
||||
host=req["host"],
|
||||
port=int(req["port"] or 8080),
|
||||
user=req["username"] or "ragflow",
|
||||
catalog=catalog,
|
||||
schema=schema or "default",
|
||||
http_scheme=http_scheme,
|
||||
auth=auth
|
||||
host=req["host"], port=int(req["port"] or 8080), user=req["username"] or "ragflow", catalog=catalog, schema=schema or "default", http_scheme=http_scheme, auth=auth
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT 1")
|
||||
@@ -514,7 +474,7 @@ async def test_db_connect():
|
||||
return get_json_result(data="Database Connection Successful!")
|
||||
else:
|
||||
return server_error_response("Unsupported database type.")
|
||||
if req["db_type"] != 'mssql':
|
||||
if req["db_type"] != "mssql":
|
||||
db.connect()
|
||||
db.close()
|
||||
|
||||
@@ -523,21 +483,21 @@ async def test_db_connect():
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
#api get list version dsl of canvas
|
||||
@manager.route('/getlistversion/<canvas_id>', methods=['GET']) # noqa: F821
|
||||
# api get list version dsl of canvas
|
||||
@manager.route("/getlistversion/<canvas_id>", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def getlistversion(canvas_id):
|
||||
try:
|
||||
versions =sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"]*-1)
|
||||
versions = sorted([c.to_dict() for c in UserCanvasVersionService.list_by_canvas_id(canvas_id)], key=lambda x: x["update_time"] * -1)
|
||||
return get_json_result(data=versions)
|
||||
except Exception as e:
|
||||
return get_data_error_result(message=f"Error getting history files: {e}")
|
||||
|
||||
|
||||
#api get version dsl of canvas
|
||||
@manager.route('/getversion/<version_id>', methods=['GET']) # noqa: F821
|
||||
# api get version dsl of canvas
|
||||
@manager.route("/getversion/<version_id>", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def getversion( version_id):
|
||||
def getversion(version_id):
|
||||
try:
|
||||
e, version = UserCanvasVersionService.get_by_id(version_id)
|
||||
if version:
|
||||
@@ -546,7 +506,7 @@ def getversion( version_id):
|
||||
return get_json_result(data=f"Error getting history file: {e}")
|
||||
|
||||
|
||||
@manager.route('/list', methods=['GET']) # noqa: F821
|
||||
@manager.route("/list", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def list_canvas():
|
||||
keywords = request.args.get("keywords", "")
|
||||
@@ -563,18 +523,14 @@ def list_canvas():
|
||||
tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
|
||||
tenants = [m["tenant_id"] for m in tenants]
|
||||
tenants.append(current_user.id)
|
||||
canvas, total = UserCanvasService.get_by_tenant_ids(
|
||||
tenants, current_user.id, page_number,
|
||||
items_per_page, orderby, desc, keywords, canvas_category)
|
||||
canvas, total = UserCanvasService.get_by_tenant_ids(tenants, current_user.id, page_number, items_per_page, orderby, desc, keywords, canvas_category)
|
||||
else:
|
||||
tenants = owner_ids
|
||||
canvas, total = UserCanvasService.get_by_tenant_ids(
|
||||
tenants, current_user.id, 0,
|
||||
0, orderby, desc, keywords, canvas_category)
|
||||
canvas, total = UserCanvasService.get_by_tenant_ids(tenants, current_user.id, 0, 0, orderby, desc, keywords, canvas_category)
|
||||
return get_json_result(data={"canvas": canvas, "total": total})
|
||||
|
||||
|
||||
@manager.route('/setting', methods=['POST']) # noqa: F821
|
||||
@manager.route("/setting", methods=["POST"]) # noqa: F821
|
||||
@validate_request("id", "title", "permission")
|
||||
@login_required
|
||||
async def setting():
|
||||
@@ -582,11 +538,9 @@ async def setting():
|
||||
req["user_id"] = current_user.id
|
||||
|
||||
if not UserCanvasService.accessible(req["id"], current_user.id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
e,flow = UserCanvasService.get_by_id(req["id"])
|
||||
e, flow = UserCanvasService.get_by_id(req["id"])
|
||||
if not e:
|
||||
return get_data_error_result(message="canvas not found.")
|
||||
flow = flow.to_dict()
|
||||
@@ -596,11 +550,11 @@ async def setting():
|
||||
if value := req.get(key):
|
||||
flow[key] = value
|
||||
|
||||
num= UserCanvasService.update_by_id(req["id"], flow)
|
||||
num = UserCanvasService.update_by_id(req["id"], flow)
|
||||
return get_json_result(data=num)
|
||||
|
||||
|
||||
@manager.route('/trace', methods=['GET']) # noqa: F821
|
||||
@manager.route("/trace", methods=["GET"]) # noqa: F821
|
||||
def trace():
|
||||
cvs_id = request.args.get("canvas_id")
|
||||
msg_id = request.args.get("message_id")
|
||||
@@ -614,14 +568,12 @@ def trace():
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
@manager.route('/<canvas_id>/sessions', methods=['GET']) # noqa: F821
|
||||
@manager.route("/<canvas_id>/sessions", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def sessions(canvas_id):
|
||||
tenant_id = current_user.id
|
||||
if not UserCanvasService.accessible(canvas_id, tenant_id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
user_id = request.args.get("user_id")
|
||||
page_number = int(request.args.get("page", 1))
|
||||
@@ -639,18 +591,17 @@ def sessions(canvas_id):
|
||||
if exp_user_id:
|
||||
sess = API4ConversationService.get_names(canvas_id, exp_user_id)
|
||||
return get_json_result(data={"total": len(sess), "sessions": sess})
|
||||
|
||||
|
||||
# dsl defaults to True in all cases except for False and false
|
||||
include_dsl = request.args.get("dsl") != "False" and request.args.get("dsl") != "false"
|
||||
total, sess = API4ConversationService.get_list(canvas_id, tenant_id, page_number, items_per_page, orderby, desc,
|
||||
None, user_id, include_dsl, keywords, from_date, to_date, exp_user_id=exp_user_id)
|
||||
total, sess = API4ConversationService.get_list(canvas_id, tenant_id, page_number, items_per_page, orderby, desc, None, user_id, include_dsl, keywords, from_date, to_date, exp_user_id=exp_user_id)
|
||||
try:
|
||||
return get_json_result(data={"total": total, "sessions": sess})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route('/<canvas_id>/sessions', methods=['PUT']) # noqa: F821
|
||||
@manager.route("/<canvas_id>/sessions", methods=["PUT"]) # noqa: F821
|
||||
@login_required
|
||||
async def set_session(canvas_id):
|
||||
req = await get_request_json()
|
||||
@@ -659,63 +610,51 @@ async def set_session(canvas_id):
|
||||
assert e, "Agent not found."
|
||||
if not isinstance(cvs.dsl, str):
|
||||
cvs.dsl = json.dumps(cvs.dsl, ensure_ascii=False)
|
||||
session_id=get_uuid()
|
||||
session_id = get_uuid()
|
||||
canvas = Canvas(cvs.dsl, tenant_id, canvas_id, canvas_id=cvs.id)
|
||||
canvas.reset()
|
||||
conv = {
|
||||
"id": session_id,
|
||||
"name": req.get("name", ""),
|
||||
"dialog_id": cvs.id,
|
||||
"user_id": tenant_id,
|
||||
"exp_user_id": tenant_id,
|
||||
"message": [],
|
||||
"source": "agent",
|
||||
"dsl": cvs.dsl,
|
||||
"reference": []
|
||||
}
|
||||
conv = {"id": session_id, "name": req.get("name", ""), "dialog_id": cvs.id, "user_id": tenant_id, "exp_user_id": tenant_id, "message": [], "source": "agent", "dsl": cvs.dsl, "reference": []}
|
||||
API4ConversationService.save(**conv)
|
||||
return get_json_result(data=conv)
|
||||
|
||||
|
||||
@manager.route('/<canvas_id>/sessions/<session_id>', methods=['GET']) # noqa: F821
|
||||
@manager.route("/<canvas_id>/sessions/<session_id>", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def get_session(canvas_id, session_id):
|
||||
tenant_id = current_user.id
|
||||
if not UserCanvasService.accessible(canvas_id, tenant_id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
_, conv = API4ConversationService.get_by_id(session_id)
|
||||
return get_json_result(data=conv.to_dict())
|
||||
|
||||
|
||||
@manager.route('/<canvas_id>/sessions/<session_id>', methods=['DELETE']) # noqa: F821
|
||||
@manager.route("/<canvas_id>/sessions/<session_id>", methods=["DELETE"]) # noqa: F821
|
||||
@login_required
|
||||
def del_session(canvas_id, session_id):
|
||||
tenant_id = current_user.id
|
||||
if not UserCanvasService.accessible(canvas_id, tenant_id):
|
||||
return get_json_result(
|
||||
data=False, message='Only owner of canvas authorized for this operation.',
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of canvas authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=API4ConversationService.delete_by_id(session_id))
|
||||
|
||||
|
||||
@manager.route('/prompts', methods=['GET']) # noqa: F821
|
||||
@manager.route("/prompts", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def prompts():
|
||||
from rag.prompts.generator import ANALYZE_TASK_SYSTEM, ANALYZE_TASK_USER, NEXT_STEP, REFLECT, CITATION_PROMPT_TEMPLATE
|
||||
|
||||
return get_json_result(data={
|
||||
"task_analysis": ANALYZE_TASK_SYSTEM +"\n\n"+ ANALYZE_TASK_USER,
|
||||
"plan_generation": NEXT_STEP,
|
||||
"reflection": REFLECT,
|
||||
#"context_summary": SUMMARY4MEMORY,
|
||||
#"context_ranking": RANK_MEMORY,
|
||||
"citation_guidelines": CITATION_PROMPT_TEMPLATE
|
||||
})
|
||||
return get_json_result(
|
||||
data={
|
||||
"task_analysis": ANALYZE_TASK_SYSTEM + "\n\n" + ANALYZE_TASK_USER,
|
||||
"plan_generation": NEXT_STEP,
|
||||
"reflection": REFLECT,
|
||||
# "context_summary": SUMMARY4MEMORY,
|
||||
# "context_ranking": RANK_MEMORY,
|
||||
"citation_guidelines": CITATION_PROMPT_TEMPLATE,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@manager.route('/download', methods=['GET']) # noqa: F821
|
||||
@manager.route("/download", methods=["GET"]) # noqa: F821
|
||||
async def download():
|
||||
id = request.args.get("id")
|
||||
created_by = request.args.get("created_by")
|
||||
|
||||
+18
-8
@@ -111,6 +111,7 @@ async def set_api_key():
|
||||
assert factory in ChatModel, f"Chat model from {factory} is not supported yet."
|
||||
mdl = ChatModel[factory](req["api_key"], llm.llm_name, base_url=base_url, **extra)
|
||||
try:
|
||||
|
||||
async def check_streamly():
|
||||
async for chunk in mdl.async_chat_streamly(
|
||||
None,
|
||||
@@ -149,7 +150,7 @@ async def set_api_key():
|
||||
break
|
||||
|
||||
if req.get("verify", False):
|
||||
return get_json_result(data={"message": msg, "success": len(msg.strip())==0})
|
||||
return get_json_result(data={"message": msg, "success": len(msg.strip()) == 0})
|
||||
|
||||
if msg:
|
||||
return get_data_error_result(message=msg)
|
||||
@@ -207,7 +208,10 @@ async def add_llm():
|
||||
saved_llm_name = llm_name + _LLM_NAME_SUFFIX.get(factory, "")
|
||||
logging.debug(
|
||||
"add_llm: attempting api_key recovery factory=%s llm_name=%s saved_llm_name=%s tenant_id=%s",
|
||||
factory, llm_name, saved_llm_name, current_user.id,
|
||||
factory,
|
||||
llm_name,
|
||||
saved_llm_name,
|
||||
current_user.id,
|
||||
)
|
||||
existing_llms = TenantLLMService.query(
|
||||
tenant_id=current_user.id,
|
||||
@@ -216,21 +220,25 @@ async def add_llm():
|
||||
)
|
||||
logging.debug(
|
||||
"add_llm: api_key recovery query matched=%d factory=%s saved_llm_name=%s",
|
||||
len(existing_llms) if existing_llms else 0, factory, saved_llm_name,
|
||||
len(existing_llms) if existing_llms else 0,
|
||||
factory,
|
||||
saved_llm_name,
|
||||
)
|
||||
if existing_llms:
|
||||
existing_api_key, _, _ = TenantLLMService._decode_api_key_config(
|
||||
existing_llms[0].api_key
|
||||
)
|
||||
existing_api_key, _, _ = TenantLLMService._decode_api_key_config(existing_llms[0].api_key)
|
||||
logging.debug(
|
||||
"add_llm: api_key recovery decoded=%s factory=%s saved_llm_name=%s",
|
||||
"present" if existing_api_key else "absent", factory, saved_llm_name,
|
||||
"present" if existing_api_key else "absent",
|
||||
factory,
|
||||
saved_llm_name,
|
||||
)
|
||||
if existing_api_key:
|
||||
req["api_key"] = existing_api_key
|
||||
logging.info(
|
||||
"add_llm: recovered saved api_key from existing record factory=%s saved_llm_name=%s tenant_id=%s",
|
||||
factory, saved_llm_name, current_user.id,
|
||||
factory,
|
||||
saved_llm_name,
|
||||
current_user.id,
|
||||
)
|
||||
|
||||
api_key = req.get("api_key", "x")
|
||||
@@ -335,6 +343,7 @@ async def add_llm():
|
||||
**extra,
|
||||
)
|
||||
try:
|
||||
|
||||
async def check_streamly():
|
||||
async for chunk in mdl.async_chat_streamly(
|
||||
None,
|
||||
@@ -387,6 +396,7 @@ async def add_llm():
|
||||
assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
|
||||
mdl = TTSModel[factory](key=model_api_key, model_name=mdl_nm, base_url=model_base_url)
|
||||
try:
|
||||
|
||||
def drain_tts():
|
||||
for _ in mdl.tts("Hello~ RAGFlower!"):
|
||||
pass
|
||||
|
||||
@@ -39,8 +39,7 @@ from api.db.services.search_service import SearchService
|
||||
from api.db.services.user_service import UserTenantService
|
||||
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type, get_model_config_from_provider_instance
|
||||
from common.misc_utils import thread_pool_exec
|
||||
from api.utils.api_utils import get_error_data_result, get_json_result, \
|
||||
add_tenant_id_to_kwargs, get_result, get_request_json, server_error_response, validate_request
|
||||
from api.utils.api_utils import get_error_data_result, get_json_result, add_tenant_id_to_kwargs, get_result, get_request_json, server_error_response, validate_request
|
||||
from rag.app.tag import label_question
|
||||
from rag.prompts.template import load_prompt
|
||||
from rag.prompts.generator import cross_languages, keyword_extraction
|
||||
@@ -58,7 +57,7 @@ def _get_sdk_authorization_token():
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if not auth_header.startswith("Bearer "):
|
||||
return ""
|
||||
return auth_header[len("Bearer "):].strip()
|
||||
return auth_header[len("Bearer ") :].strip()
|
||||
|
||||
|
||||
@manager.route("/chatbots/<dialog_id>/completions", methods=["POST"]) # noqa: F821
|
||||
@@ -68,9 +67,7 @@ async def chatbot_completions(dialog_id, tenant_id=None):
|
||||
req = await get_request_json()
|
||||
|
||||
exists, dialog = DialogService.get_by_id(dialog_id)
|
||||
if (not exists
|
||||
or getattr(dialog, "tenant_id", None) != tenant_id
|
||||
or str(getattr(dialog, "status", "")) != StatusEnum.VALID.value):
|
||||
if not exists or getattr(dialog, "tenant_id", None) != tenant_id or str(getattr(dialog, "status", "")) != StatusEnum.VALID.value:
|
||||
logger.warning(
|
||||
"Denied chatbot access: reason=%s tenant_id=%s dialog_id=%s user_id=%s session_id=%s",
|
||||
"no access to this chatbot",
|
||||
@@ -132,14 +129,13 @@ async def chatbot_completions(dialog_id, tenant_id=None):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@manager.route("/chatbots/<dialog_id>/info", methods=["GET"]) # noqa: F821
|
||||
@login_required(auth_types=AUTH_BETA)
|
||||
@add_tenant_id_to_kwargs
|
||||
async def chatbots_inputs(dialog_id, tenant_id=None):
|
||||
exists, dialog = await thread_pool_exec(DialogService.get_by_id, dialog_id)
|
||||
if (not exists
|
||||
or getattr(dialog, "tenant_id", None) != tenant_id
|
||||
or str(getattr(dialog, "status", "")) != StatusEnum.VALID.value):
|
||||
if not exists or getattr(dialog, "tenant_id", None) != tenant_id or str(getattr(dialog, "status", "")) != StatusEnum.VALID.value:
|
||||
request_args = getattr(request, "args", {}) or {}
|
||||
request_user_id = request_args.get("user_id") if hasattr(request_args, "get") else None
|
||||
request_session_id = request_args.get("session_id") if hasattr(request_args, "get") else None
|
||||
@@ -178,6 +174,7 @@ async def agent_bot_completions(agent_id, tenant_id=None):
|
||||
return get_error_data_result(message=f"Can't find agent by ID: {agent_id}")
|
||||
|
||||
if req.get("stream", True):
|
||||
|
||||
async def stream():
|
||||
try:
|
||||
async for answer in agent_completion(tenant_id, agent_id, **req):
|
||||
@@ -185,14 +182,18 @@ async def agent_bot_completions(agent_id, tenant_id=None):
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
error_result = get_error_data_result(message=str(e) or "Unknown error")
|
||||
yield "data:" + json.dumps(
|
||||
{
|
||||
"event": "message",
|
||||
"data": {"content": f"Error {error_result['code']}: {error_result['message']}\n\n"},
|
||||
**error_result,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
) + "\n\n"
|
||||
yield (
|
||||
"data:"
|
||||
+ json.dumps(
|
||||
{
|
||||
"event": "message",
|
||||
"data": {"content": f"Error {error_result['code']}: {error_result['message']}\n\n"},
|
||||
**error_result,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
+ "\n\n"
|
||||
)
|
||||
|
||||
resp = Response(stream(), mimetype="text/event-stream")
|
||||
resp.headers.add_header("Cache-control", "no-cache")
|
||||
@@ -217,7 +218,7 @@ async def agent_bot_completions(agent_id, tenant_id=None):
|
||||
line = line.strip()
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
payload = line[len("data:"):].strip()
|
||||
payload = line[len("data:") :].strip()
|
||||
if not payload:
|
||||
continue
|
||||
try:
|
||||
@@ -270,9 +271,7 @@ async def begin_inputs(agent_id, tenant_id=None):
|
||||
return get_error_data_result(f"Can't find agent by ID: {agent_id}")
|
||||
|
||||
canvas = Canvas(json.dumps(cvs.dsl), tenant_id, canvas_id=cvs.id)
|
||||
return get_result(
|
||||
data={"title": cvs.title, "avatar": cvs.avatar, "inputs": canvas.get_component_input_form("begin"),
|
||||
"prologue": canvas.get_prologue(), "mode": canvas.get_mode()})
|
||||
return get_result(data={"title": cvs.title, "avatar": cvs.avatar, "inputs": canvas.get_component_input_form("begin"), "prologue": canvas.get_prologue(), "mode": canvas.get_mode()})
|
||||
|
||||
|
||||
@manager.route("/agentbots/<shared_id>/logs/<message_id>", methods=["GET"]) # noqa: F821
|
||||
@@ -289,9 +288,10 @@ async def agent_bot_logs(shared_id, message_id):
|
||||
if not token:
|
||||
logger.warning(
|
||||
"agent_bot_logs: missing Authorization header (shared_id=%s message_id=%s)",
|
||||
shared_id, message_id,
|
||||
shared_id,
|
||||
message_id,
|
||||
)
|
||||
return get_error_data_result(message='Authorization is not valid!')
|
||||
return get_error_data_result(message="Authorization is not valid!")
|
||||
# Non-reversible fingerprint of the share token: lets operators correlate
|
||||
# auth-failure log lines for the same token without leaking a guessable
|
||||
# substring of the secret itself.
|
||||
@@ -300,7 +300,8 @@ async def agent_bot_logs(shared_id, message_id):
|
||||
if not objs:
|
||||
logger.warning(
|
||||
"agent_bot_logs: invalid beta token (fingerprint=%s shared_id=%s)",
|
||||
token_fp, shared_id,
|
||||
token_fp,
|
||||
shared_id,
|
||||
)
|
||||
return get_error_data_result(message='Authentication error: API key is invalid!"')
|
||||
|
||||
@@ -308,9 +309,10 @@ async def agent_bot_logs(shared_id, message_id):
|
||||
if not agent_id:
|
||||
logger.warning(
|
||||
"agent_bot_logs: APIToken has no dialog_id (tenant_id=%s fingerprint=%s)",
|
||||
objs[0].tenant_id, token_fp,
|
||||
objs[0].tenant_id,
|
||||
token_fp,
|
||||
)
|
||||
return get_error_data_result(message='API token is not bound to an agent.')
|
||||
return get_error_data_result(message="API token is not bound to an agent.")
|
||||
|
||||
try:
|
||||
binary = await thread_pool_exec(REDIS_CONN.get, f"{agent_id}-{message_id}-logs")
|
||||
@@ -348,9 +350,7 @@ async def ask_about_embedded(tenant_id=None):
|
||||
async for ans in async_ask(req["question"], req["kb_ids"], uid, chat_llm_name=chat_llm_name, search_config=search_config):
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
except Exception as e:
|
||||
yield "data:" + json.dumps(
|
||||
{"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e), "reference": []}},
|
||||
ensure_ascii=False) + "\n\n"
|
||||
yield "data:" + json.dumps({"code": 500, "message": str(e), "data": {"answer": "**ERROR**: " + str(e), "reference": []}}, ensure_ascii=False) + "\n\n"
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
|
||||
|
||||
resp = Response(stream(), mimetype="text/event-stream")
|
||||
@@ -374,8 +374,7 @@ async def retrieval_test_embedded(tenant_id=None):
|
||||
if isinstance(kb_ids, str):
|
||||
kb_ids = [kb_ids]
|
||||
if not kb_ids:
|
||||
return get_json_result(data=False, message='Please specify dataset firstly.',
|
||||
code=RetCode.DATA_ERROR)
|
||||
return get_json_result(data=False, message="Please specify dataset firstly.", code=RetCode.DATA_ERROR)
|
||||
doc_ids = req.get("doc_ids", [])
|
||||
similarity_threshold = float(req.get("similarity_threshold", 0.0))
|
||||
vector_similarity_weight = float(req.get("vector_similarity_weight", 0.3))
|
||||
@@ -443,8 +442,7 @@ async def retrieval_test_embedded(tenant_id=None):
|
||||
tenant_ids.append(tenant.tenant_id)
|
||||
break
|
||||
else:
|
||||
return get_json_result(data=False, message="Only owner of dataset authorized for this operation.",
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Only owner of dataset authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
e, kb = await thread_pool_exec(KnowledgebaseService.get_by_id, kb_ids[0])
|
||||
if not e:
|
||||
@@ -467,13 +465,23 @@ async def retrieval_test_embedded(tenant_id=None):
|
||||
|
||||
labels = label_question(_question, [kb])
|
||||
ranks = await settings.retriever.retrieval(
|
||||
_question, embd_mdl, tenant_ids, kb_ids, page, size, similarity_threshold, vector_similarity_weight, top,
|
||||
local_doc_ids, rerank_mdl=rerank_mdl, highlight=req.get("highlight"), rank_feature=labels
|
||||
_question,
|
||||
embd_mdl,
|
||||
tenant_ids,
|
||||
kb_ids,
|
||||
page,
|
||||
size,
|
||||
similarity_threshold,
|
||||
vector_similarity_weight,
|
||||
top,
|
||||
local_doc_ids,
|
||||
rerank_mdl=rerank_mdl,
|
||||
highlight=req.get("highlight"),
|
||||
rank_feature=labels,
|
||||
)
|
||||
if use_kg:
|
||||
default_chat_model = await thread_pool_exec(get_tenant_default_model_by_type, kb.tenant_id, LLMType.CHAT)
|
||||
ck = await settings.kg_retriever.retrieval(_question, tenant_ids, kb_ids, embd_mdl,
|
||||
LLMBundle(kb.tenant_id, default_chat_model))
|
||||
ck = await settings.kg_retriever.retrieval(_question, tenant_ids, kb_ids, embd_mdl, LLMBundle(kb.tenant_id, default_chat_model))
|
||||
if ck["content_with_weight"]:
|
||||
ranks["chunks"].insert(0, ck)
|
||||
|
||||
@@ -492,8 +500,7 @@ async def retrieval_test_embedded(tenant_id=None):
|
||||
return await _retrieval()
|
||||
except Exception as e:
|
||||
if "not_found" in str(e):
|
||||
return get_json_result(data=False, message="No chunk found! Check the chunk status please!",
|
||||
code=RetCode.DATA_ERROR)
|
||||
return get_json_result(data=False, message="No chunk found! Check the chunk status please!", code=RetCode.DATA_ERROR)
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@@ -552,8 +559,7 @@ async def detail_share_embedded(tenant_id=None):
|
||||
if await thread_pool_exec(SearchService.query, tenant_id=tenant.tenant_id, id=search_id):
|
||||
break
|
||||
else:
|
||||
return get_json_result(data=False, message="Has no permission for this operation.",
|
||||
code=RetCode.OPERATING_ERROR)
|
||||
return get_json_result(data=False, message="Has no permission for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
|
||||
search = await thread_pool_exec(SearchService.get_detail, search_id)
|
||||
if not search:
|
||||
@@ -573,7 +579,7 @@ async def mindmap(tenant_id=None):
|
||||
search_id = req.get("search_id", "")
|
||||
search_app = await thread_pool_exec(SearchService.get_detail, search_id) if search_id else {}
|
||||
|
||||
mind_map =await gen_mindmap(req["question"], req["kb_ids"], tenant_id, search_app.get("search_config", {}))
|
||||
mind_map = await gen_mindmap(req["question"], req["kb_ids"], tenant_id, search_app.get("search_config", {}))
|
||||
if "error" in mind_map:
|
||||
return server_error_response(Exception(mind_map["error"]))
|
||||
return get_json_result(data=mind_map)
|
||||
|
||||
@@ -27,9 +27,7 @@ from quart import Response, request
|
||||
|
||||
from api.apps import current_user, login_required
|
||||
from api.apps.restful_apis._generation_params import merge_generation_config, pop_generation_config
|
||||
from api.db.joint_services.tenant_model_service import (
|
||||
get_tenant_default_model_by_type, get_model_config_from_provider_instance, get_api_key, split_model_name
|
||||
)
|
||||
from api.db.joint_services.tenant_model_service import get_tenant_default_model_by_type, get_model_config_from_provider_instance, get_api_key, split_model_name
|
||||
from api.db.services.chunk_feedback_service import ChunkFeedbackService
|
||||
from api.db.services.conversation_service import ConversationService, structure_answer
|
||||
from api.db.services.dialog_service import DialogService, async_chat, gen_mindmap
|
||||
@@ -52,6 +50,7 @@ from common.misc_utils import get_uuid, thread_pool_exec
|
||||
from rag.prompts.generator import chunks_format
|
||||
from rag.prompts.template import load_prompt
|
||||
|
||||
|
||||
def _sanitize_json_floats(obj):
|
||||
"""Replace NaN/Infinity floats with None so the result is RFC 8259 JSON.
|
||||
|
||||
@@ -86,8 +85,8 @@ def _sanitize_json_floats(obj):
|
||||
|
||||
_DEFAULT_PROMPT_CONFIG = {
|
||||
"system": (
|
||||
'You are an intelligent assistant. Please summarize the content of the dataset to answer the question. '
|
||||
'Please list the data in the dataset and answer in detail. When all dataset content is irrelevant to the '
|
||||
"You are an intelligent assistant. Please summarize the content of the dataset to answer the question. "
|
||||
"Please list the data in the dataset and answer in detail. When all dataset content is irrelevant to the "
|
||||
'question, your answer must include the sentence "The answer you are looking for is not found in the dataset!" '
|
||||
"Answers need to consider chat history.\n"
|
||||
" Here is the knowledge base:\n"
|
||||
@@ -162,10 +161,7 @@ def _build_session_response(conv: dict) -> dict:
|
||||
|
||||
|
||||
async def _ensure_owned_chat(chat_id):
|
||||
return await thread_pool_exec(
|
||||
DialogService.query,
|
||||
tenant_id=current_user.id, id=chat_id, status=StatusEnum.VALID.value
|
||||
)
|
||||
return await thread_pool_exec(DialogService.query, tenant_id=current_user.id, id=chat_id, status=StatusEnum.VALID.value)
|
||||
|
||||
|
||||
def _build_default_completion_dialog():
|
||||
@@ -292,10 +288,11 @@ async def _validate_llm_id(llm_id, tenant_id, llm_setting=None):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def _validate_rerank_id(rerank_id, tenant_id):
|
||||
if not rerank_id:
|
||||
return None
|
||||
parts = rerank_id.split('@')
|
||||
parts = rerank_id.split("@")
|
||||
llm_name = parts[0]
|
||||
if llm_name in _DEFAULT_RERANK_MODELS:
|
||||
return None
|
||||
@@ -342,7 +339,7 @@ async def _validate_dataset_ids(dataset_ids, tenant_id):
|
||||
|
||||
embd_ids = [split_model_name(kb.embd_id)[0] for kb in kbs]
|
||||
if len(set(embd_ids)) > 1:
|
||||
return f'Datasets use different embedding models: {[kb.embd_id for kb in kbs]}'
|
||||
return f"Datasets use different embedding models: {[kb.embd_id for kb in kbs]}"
|
||||
|
||||
return normalized_ids
|
||||
|
||||
@@ -462,7 +459,14 @@ async def list_chats():
|
||||
if owner_ids:
|
||||
chats, total = await thread_pool_exec(
|
||||
DialogService.get_by_tenant_ids,
|
||||
owner_ids, current_user.id, 0, 0, orderby, desc, keywords, **exact_filters,
|
||||
owner_ids,
|
||||
current_user.id,
|
||||
0,
|
||||
0,
|
||||
orderby,
|
||||
desc,
|
||||
keywords,
|
||||
**exact_filters,
|
||||
)
|
||||
chats = [chat for chat in chats if chat["tenant_id"] in owner_ids]
|
||||
total = len(chats)
|
||||
@@ -472,12 +476,17 @@ async def list_chats():
|
||||
else:
|
||||
chats, total = await thread_pool_exec(
|
||||
DialogService.get_by_tenant_ids,
|
||||
[], current_user.id, page_number, items_per_page, orderby, desc, keywords, **exact_filters,
|
||||
[],
|
||||
current_user.id,
|
||||
page_number,
|
||||
items_per_page,
|
||||
orderby,
|
||||
desc,
|
||||
keywords,
|
||||
**exact_filters,
|
||||
)
|
||||
|
||||
return get_json_result(
|
||||
data={"chats": [_build_chat_response(chat) for chat in chats], "total": total}
|
||||
)
|
||||
return get_json_result(data={"chats": [_build_chat_response(chat) for chat in chats], "total": total})
|
||||
except Exception as ex:
|
||||
return server_error_response(ex)
|
||||
|
||||
@@ -490,7 +499,9 @@ async def get_chat(chat_id):
|
||||
for tenant in tenants:
|
||||
if await thread_pool_exec(
|
||||
DialogService.query,
|
||||
tenant_id=tenant.tenant_id, id=chat_id, status=StatusEnum.VALID.value,
|
||||
tenant_id=tenant.tenant_id,
|
||||
id=chat_id,
|
||||
status=StatusEnum.VALID.value,
|
||||
):
|
||||
break
|
||||
else:
|
||||
@@ -512,9 +523,7 @@ async def get_chat(chat_id):
|
||||
@login_required
|
||||
async def update_chat(chat_id):
|
||||
if not await _ensure_owned_chat(chat_id):
|
||||
return get_json_result(
|
||||
data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
try:
|
||||
req = await get_request_json()
|
||||
@@ -596,9 +605,7 @@ async def update_chat(chat_id):
|
||||
@login_required
|
||||
async def patch_chat(chat_id):
|
||||
if not await _ensure_owned_chat(chat_id):
|
||||
return get_json_result(
|
||||
data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
try:
|
||||
req = await get_request_json()
|
||||
@@ -686,9 +693,7 @@ async def patch_chat(chat_id):
|
||||
@login_required
|
||||
async def delete_chat(chat_id):
|
||||
if not await _ensure_owned_chat(chat_id):
|
||||
return get_json_result(
|
||||
data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR
|
||||
)
|
||||
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
try:
|
||||
if not DialogService.update_by_id(chat_id, {"status": StatusEnum.INVALID.value}):
|
||||
@@ -708,12 +713,7 @@ async def bulk_delete_chats():
|
||||
ids = req.get("ids")
|
||||
if not ids:
|
||||
if req.get("delete_all") is True:
|
||||
ids = [
|
||||
chat.id
|
||||
for chat in DialogService.query(
|
||||
tenant_id=current_user.id, status=StatusEnum.VALID.value
|
||||
)
|
||||
]
|
||||
ids = [chat.id for chat in DialogService.query(tenant_id=current_user.id, status=StatusEnum.VALID.value)]
|
||||
if not ids:
|
||||
return get_json_result(data={})
|
||||
else:
|
||||
@@ -799,9 +799,7 @@ async def list_sessions(chat_id):
|
||||
session_id = request.args.get("id")
|
||||
name = request.args.get("name")
|
||||
user_id = request.args.get("user_id")
|
||||
convs = ConversationService.get_list(
|
||||
chat_id, page_number, items_per_page, orderby, desc, session_id, name, user_id
|
||||
)
|
||||
convs = ConversationService.get_list(chat_id, page_number, items_per_page, orderby, desc, session_id, name, user_id)
|
||||
if items_per_page == 0:
|
||||
convs = []
|
||||
return get_json_result(data=[_build_session_response(c) for c in convs])
|
||||
@@ -1045,17 +1043,21 @@ async def transcription():
|
||||
uploaded = files["file"]
|
||||
|
||||
ALLOWED_EXTS = {
|
||||
".wav", ".mp3", ".m4a", ".aac",
|
||||
".flac", ".ogg", ".webm",
|
||||
".opus", ".wma",
|
||||
".wav",
|
||||
".mp3",
|
||||
".m4a",
|
||||
".aac",
|
||||
".flac",
|
||||
".ogg",
|
||||
".webm",
|
||||
".opus",
|
||||
".wma",
|
||||
}
|
||||
|
||||
filename = uploaded.filename or ""
|
||||
suffix = os.path.splitext(filename)[-1].lower()
|
||||
if suffix not in ALLOWED_EXTS:
|
||||
return get_data_error_result(
|
||||
message=f"Unsupported audio format: {suffix}. Allowed: {', '.join(sorted(ALLOWED_EXTS))}"
|
||||
)
|
||||
return get_data_error_result(message=f"Unsupported audio format: {suffix}. Allowed: {', '.join(sorted(ALLOWED_EXTS))}")
|
||||
|
||||
fd, temp_audio_path = tempfile.mkstemp(suffix=suffix)
|
||||
os.close(fd)
|
||||
|
||||
@@ -37,14 +37,7 @@ def _chat_channel_auth_error(channel_id: str, user_id: str):
|
||||
async def create_chat_channel():
|
||||
"""Create a chat channel bot owned by the current tenant."""
|
||||
req = await get_request_json()
|
||||
channel = {
|
||||
"id": get_uuid(),
|
||||
"tenant_id": current_user.id,
|
||||
"name": req["name"],
|
||||
"channel": req["channel"],
|
||||
"config": req.get("config") or {},
|
||||
"chat_id": req.get("chat_id") or None
|
||||
}
|
||||
channel = {"id": get_uuid(), "tenant_id": current_user.id, "name": req["name"], "channel": req["channel"], "config": req.get("config") or {}, "chat_id": req.get("chat_id") or None}
|
||||
ChatChannelService.insert(**channel)
|
||||
|
||||
e, conn = ChatChannelService.get_by_id(channel["id"])
|
||||
|
||||
@@ -551,6 +551,7 @@ async def google_drive_web_oauth_callback():
|
||||
|
||||
return await _render_web_oauth_popup(state_id, True, "Authorization completed successfully.", source)
|
||||
|
||||
|
||||
@manager.route("/connectors/google/oauth/web/result", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("flow_id")
|
||||
@@ -571,13 +572,14 @@ async def poll_google_web_result():
|
||||
REDIS_CONN.delete(_web_result_cache_key(flow_id, source))
|
||||
return get_json_result(data={"credentials": result.get("credentials")})
|
||||
|
||||
|
||||
@manager.route("/connectors/box/oauth/web/start", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def start_box_web_oauth():
|
||||
req = await get_request_json()
|
||||
|
||||
client_id = req.get("client_id")
|
||||
client_secret = req.get("client_secret")
|
||||
client_secret = req.get("client_secret")
|
||||
redirect_uri = req.get("redirect_uri", BOX_WEB_OAUTH_REDIRECT_URI)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
@@ -608,18 +610,20 @@ async def start_box_web_oauth():
|
||||
}
|
||||
REDIS_CONN.set_obj(_web_state_cache_key(flow_id, "box"), cache_payload, WEB_FLOW_TTL_SECS)
|
||||
return get_json_result(
|
||||
data = {
|
||||
data={
|
||||
"flow_id": flow_id,
|
||||
"authorization_url": auth_url,
|
||||
"expires_in": WEB_FLOW_TTL_SECS,}
|
||||
"expires_in": WEB_FLOW_TTL_SECS,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@manager.route("/connectors/box/oauth/web/callback", methods=["GET"]) # noqa: F821
|
||||
async def box_web_oauth_callback():
|
||||
flow_id = request.args.get("state")
|
||||
if not flow_id:
|
||||
return await _render_web_oauth_popup("", False, "Missing OAuth parameters.", "box")
|
||||
|
||||
|
||||
code = request.args.get("code")
|
||||
if not code:
|
||||
return await _render_web_oauth_popup(flow_id, False, "Missing authorization code from Box.", "box")
|
||||
@@ -633,7 +637,7 @@ async def box_web_oauth_callback():
|
||||
if error:
|
||||
REDIS_CONN.delete(_web_state_cache_key(flow_id, "box"))
|
||||
return await _render_web_oauth_popup(flow_id, False, error_description or "Authorization failed.", "box")
|
||||
|
||||
|
||||
auth = BoxOAuth(
|
||||
OAuthConfig(
|
||||
client_id=cache_payload.get("client_id"),
|
||||
@@ -656,6 +660,7 @@ async def box_web_oauth_callback():
|
||||
|
||||
return await _render_web_oauth_popup(flow_id, True, "Authorization completed successfully.", "box")
|
||||
|
||||
|
||||
@manager.route("/connectors/box/oauth/web/result", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("flow_id")
|
||||
@@ -670,7 +675,7 @@ async def poll_box_web_result():
|
||||
cache_raw = json.loads(cache_blob)
|
||||
if cache_raw.get("user_id") != current_user.id:
|
||||
return get_json_result(code=RetCode.PERMISSION_ERROR, message="You are not allowed to access this authorization result.")
|
||||
|
||||
|
||||
REDIS_CONN.delete(_web_result_cache_key(flow_id, "box"))
|
||||
|
||||
return get_json_result(data={"credentials": cache_raw})
|
||||
|
||||
@@ -108,7 +108,7 @@ def _parse_retrieval_options(retrieval_setting):
|
||||
return retrieval_setting, similarity_threshold, top
|
||||
|
||||
|
||||
@manager.route('/dify/retrieval', methods=['POST', 'GET']) # noqa: F821
|
||||
@manager.route("/dify/retrieval", methods=["POST", "GET"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
async def retrieval(tenant_id):
|
||||
@@ -251,7 +251,6 @@ async def retrieval(tenant_id):
|
||||
|
||||
doc_ids = []
|
||||
try:
|
||||
|
||||
e, kb = KnowledgebaseService.get_by_id(kb_id)
|
||||
if not e:
|
||||
return build_error_result(message="Knowledgebase not found!", code=RetCode.NOT_FOUND)
|
||||
@@ -279,17 +278,13 @@ async def retrieval(tenant_id):
|
||||
vector_similarity_weight=0.3,
|
||||
top=top,
|
||||
doc_ids=doc_ids,
|
||||
rank_feature=label_question(question, [kb])
|
||||
rank_feature=label_question(question, [kb]),
|
||||
)
|
||||
ranks["chunks"] = settings.retriever.retrieval_by_children(ranks["chunks"], [tenant_id])
|
||||
|
||||
if use_kg:
|
||||
model_config = get_tenant_default_model_by_type(kb.tenant_id, LLMType.CHAT)
|
||||
ck = await settings.kg_retriever.retrieval(question,
|
||||
[tenant_id],
|
||||
[kb_id],
|
||||
embd_mdl,
|
||||
LLMBundle(kb.tenant_id, model_config))
|
||||
ck = await settings.kg_retriever.retrieval(question, [tenant_id], [kb_id], embd_mdl, LLMBundle(kb.tenant_id, model_config))
|
||||
if ck["content_with_weight"]:
|
||||
ranks["chunks"].insert(0, ck)
|
||||
|
||||
@@ -303,30 +298,21 @@ async def retrieval(tenant_id):
|
||||
if not doc:
|
||||
continue
|
||||
c.pop("vector", None)
|
||||
meta = getattr(doc, 'meta_fields', {})
|
||||
meta = getattr(doc, "meta_fields", {})
|
||||
meta["doc_id"] = c["doc_id"]
|
||||
# Dify expects metadata.document_id for external retrieval sources.
|
||||
meta["document_id"] = c["doc_id"]
|
||||
records.append({
|
||||
"content": c["content_with_weight"],
|
||||
"score": c["similarity"],
|
||||
"title": c["docnm_kwd"],
|
||||
"metadata": meta
|
||||
})
|
||||
records.append({"content": c["content_with_weight"], "score": c["similarity"], "title": c["docnm_kwd"], "metadata": meta})
|
||||
|
||||
return jsonify({"records": records})
|
||||
except Exception as e:
|
||||
if "not_found" in str(e):
|
||||
return build_error_result(
|
||||
message='No chunk found! Check the chunk status please!',
|
||||
code=RetCode.NOT_FOUND
|
||||
)
|
||||
return build_error_result(message="No chunk found! Check the chunk status please!", code=RetCode.NOT_FOUND)
|
||||
logging.exception(e)
|
||||
return build_error_result(message=str(e), code=RetCode.SERVER_ERROR)
|
||||
|
||||
|
||||
@manager.route('/dify/retrieval/health', methods=['GET']) # noqa: F821
|
||||
|
||||
|
||||
@manager.route("/dify/retrieval/health", methods=["GET"]) # noqa: F821
|
||||
async def retrieval_health_check():
|
||||
"""Health check endpoint for Dify external knowledge base connectivity verification."""
|
||||
return get_json_result(data=True)
|
||||
|
||||
|
||||
@@ -56,27 +56,31 @@ def _convert_files(file_ids, kb_ids, user_id):
|
||||
e, kb = KnowledgebaseService.get_by_id(kb_id)
|
||||
if not e:
|
||||
continue
|
||||
doc = DocumentService.insert({
|
||||
"id": get_uuid(),
|
||||
"kb_id": kb.id,
|
||||
"parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
|
||||
"pipeline_id": kb.pipeline_id,
|
||||
"parser_config": kb.parser_config,
|
||||
"created_by": user_id,
|
||||
"type": file.type,
|
||||
"name": file.name,
|
||||
"suffix": Path(file.name).suffix.lstrip("."),
|
||||
"location": file.location,
|
||||
"size": file.size
|
||||
})
|
||||
File2DocumentService.insert({
|
||||
"id": get_uuid(),
|
||||
"file_id": id,
|
||||
"document_id": doc.id,
|
||||
})
|
||||
doc = DocumentService.insert(
|
||||
{
|
||||
"id": get_uuid(),
|
||||
"kb_id": kb.id,
|
||||
"parser_id": FileService.get_parser(file.type, file.name, kb.parser_id),
|
||||
"pipeline_id": kb.pipeline_id,
|
||||
"parser_config": kb.parser_config,
|
||||
"created_by": user_id,
|
||||
"type": file.type,
|
||||
"name": file.name,
|
||||
"suffix": Path(file.name).suffix.lstrip("."),
|
||||
"location": file.location,
|
||||
"size": file.size,
|
||||
}
|
||||
)
|
||||
File2DocumentService.insert(
|
||||
{
|
||||
"id": get_uuid(),
|
||||
"file_id": id,
|
||||
"document_id": doc.id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@manager.route('/files/link-to-datasets', methods=['POST']) # noqa: F821
|
||||
@manager.route("/files/link-to-datasets", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("file_ids", "kb_ids")
|
||||
async def convert():
|
||||
@@ -162,9 +166,7 @@ async def convert():
|
||||
# soon as the background task is scheduled.
|
||||
loop = asyncio.get_running_loop()
|
||||
future = loop.run_in_executor(None, _convert_files, all_file_ids, kb_ids, user_id)
|
||||
future.add_done_callback(
|
||||
lambda f: logging.error("_convert_files failed: %s", f.exception()) if f.exception() else None
|
||||
)
|
||||
future.add_done_callback(lambda f: logging.error("_convert_files failed: %s", f.exception()) if f.exception() else None)
|
||||
logger.info(
|
||||
"user_id=%s resource_type=file_to_dataset_link resource_id=batch action=schedule_convert result=scheduled file_ids=%s kb_ids=%s",
|
||||
user_id,
|
||||
|
||||
@@ -69,11 +69,11 @@ async def create_or_upload(tenant_id: str = None):
|
||||
form = await request.form
|
||||
pf_id = form.get("parent_id")
|
||||
files = await request.files
|
||||
if 'file' not in files:
|
||||
if "file" not in files:
|
||||
return get_error_argument_result("No file part!")
|
||||
file_objs = files.getlist('file')
|
||||
file_objs = files.getlist("file")
|
||||
for file_obj in file_objs:
|
||||
if file_obj.filename == '':
|
||||
if file_obj.filename == "":
|
||||
return get_error_argument_result("No file selected!")
|
||||
|
||||
success, result = await file_api_service.upload_file(tenant_id, pf_id, file_objs)
|
||||
@@ -86,9 +86,7 @@ async def create_or_upload(tenant_id: str = None):
|
||||
if err is not None:
|
||||
return get_error_argument_result(err)
|
||||
|
||||
success, result = await file_api_service.create_folder(
|
||||
tenant_id, req["name"], req.get("parent_id"), req.get("type")
|
||||
)
|
||||
success, result = await file_api_service.create_folder(tenant_id, req["name"], req.get("parent_id"), req.get("type"))
|
||||
if success:
|
||||
return get_result(data=result)
|
||||
else:
|
||||
@@ -198,9 +196,7 @@ async def delete(tenant_id: str = None):
|
||||
errors = result.get("errors", [])
|
||||
return get_json_result(
|
||||
code=RetCode.DATA_ERROR,
|
||||
message=f"Partially deleted {success_count} files with {len(errors)} errors"
|
||||
if success_count > 0
|
||||
else f"Deleted files failed with {len(errors)} errors",
|
||||
message=f"Partially deleted {success_count} files with {len(errors)} errors" if success_count > 0 else f"Deleted files failed with {len(errors)} errors",
|
||||
data=result,
|
||||
)
|
||||
return get_error_data_result(message=result)
|
||||
@@ -209,7 +205,6 @@ async def delete(tenant_id: str = None):
|
||||
return get_error_data_result(message="Internal server error")
|
||||
|
||||
|
||||
|
||||
@manager.route("/files/move", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
@@ -254,9 +249,7 @@ async def move(tenant_id: str = None):
|
||||
return get_error_argument_result(err)
|
||||
|
||||
try:
|
||||
success, result = await file_api_service.move_files(
|
||||
tenant_id, req["src_file_ids"], req.get("dest_file_id"), req.get("new_name")
|
||||
)
|
||||
success, result = await file_api_service.move_files(tenant_id, req["src_file_ids"], req.get("dest_file_id"), req.get("new_name"))
|
||||
if success:
|
||||
return get_result(data=result)
|
||||
else:
|
||||
|
||||
@@ -35,12 +35,7 @@ async def create_memory():
|
||||
req = await get_request_json()
|
||||
t_parsed = time.perf_counter() if timing_enabled else None
|
||||
try:
|
||||
memory_info = {
|
||||
"name": req["name"],
|
||||
"memory_type": req["memory_type"],
|
||||
"embd_id": req["embd_id"],
|
||||
"llm_id": req["llm_id"]
|
||||
}
|
||||
memory_info = {"name": req["name"], "memory_type": req["memory_type"], "embd_id": req["embd_id"], "llm_id": req["llm_id"]}
|
||||
success, res = await memory_api_service.create_memory(memory_info)
|
||||
if timing_enabled:
|
||||
logging.info(
|
||||
@@ -84,10 +79,26 @@ async def create_memory():
|
||||
@login_required
|
||||
async def update_memory(memory_id):
|
||||
req = await get_request_json()
|
||||
new_settings = {k: req[k] for k in [
|
||||
"name", "permissions", "llm_id", "embd_id", "memory_type", "memory_size", "forgetting_policy", "temperature",
|
||||
"avatar", "description", "system_prompt", "user_prompt", "tenant_llm_id", "tenant_embd_id"
|
||||
] if k in req}
|
||||
new_settings = {
|
||||
k: req[k]
|
||||
for k in [
|
||||
"name",
|
||||
"permissions",
|
||||
"llm_id",
|
||||
"embd_id",
|
||||
"memory_type",
|
||||
"memory_size",
|
||||
"forgetting_policy",
|
||||
"temperature",
|
||||
"avatar",
|
||||
"description",
|
||||
"system_prompt",
|
||||
"user_prompt",
|
||||
"tenant_llm_id",
|
||||
"tenant_embd_id",
|
||||
]
|
||||
if k in req
|
||||
}
|
||||
try:
|
||||
success, res = await memory_api_service.update_memory(memory_id, new_settings)
|
||||
if success:
|
||||
@@ -122,9 +133,7 @@ async def delete_memory(memory_id):
|
||||
@manager.route("/memories", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
async def list_memory():
|
||||
filter_params = {
|
||||
k: request.args.get(k) for k in ["memory_type", "tenant_id", "owner_ids", "storage_type"] if k in request.args
|
||||
}
|
||||
filter_params = {k: request.args.get(k) for k in ["memory_type", "tenant_id", "owner_ids", "storage_type"] if k in request.args}
|
||||
keywords = request.args.get("keywords")
|
||||
page = int(request.args.get("page", 1))
|
||||
page_size = validate_rest_api_page_size(int(request.args.get("page_size", 50)))
|
||||
@@ -155,16 +164,14 @@ async def get_memory_config(memory_id):
|
||||
async def get_memory_messages(memory_id):
|
||||
args = request.args
|
||||
agent_ids = args.getlist("agent_id")
|
||||
if len(agent_ids) == 1 and ',' in agent_ids[0]:
|
||||
agent_ids = agent_ids[0].split(',')
|
||||
if len(agent_ids) == 1 and "," in agent_ids[0]:
|
||||
agent_ids = agent_ids[0].split(",")
|
||||
keywords = args.get("keywords", "")
|
||||
keywords = keywords.strip()
|
||||
page = int(args.get("page", 1))
|
||||
page_size = validate_rest_api_page_size(int(args.get("page_size", 50)))
|
||||
try:
|
||||
res = await memory_api_service.get_memory_messages(
|
||||
memory_id, agent_ids, keywords, page, page_size
|
||||
)
|
||||
res = await memory_api_service.get_memory_messages(memory_id, agent_ids, keywords, page, page_size)
|
||||
return get_json_result(message=True, data=res)
|
||||
except NotFoundException as not_found_exception:
|
||||
logging.error(not_found_exception)
|
||||
@@ -174,7 +181,7 @@ async def get_memory_messages(memory_id):
|
||||
return get_json_result(code=RetCode.SERVER_ERROR, message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/messages", methods=["POST"]) # noqa: F821
|
||||
@manager.route("/messages", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("memory_id", "agent_id", "session_id", "user_input", "agent_response")
|
||||
async def add_message():
|
||||
@@ -206,7 +213,7 @@ async def add_message():
|
||||
return get_json_result(message="Some messages failed to add. Detail:" + msg, code=RetCode.SERVER_ERROR)
|
||||
|
||||
|
||||
@manager.route("/messages/<memory_id>:<message_id>", methods=["DELETE"]) # noqa: F821
|
||||
@manager.route("/messages/<memory_id>:<message_id>", methods=["DELETE"]) # noqa: F821
|
||||
@login_required
|
||||
async def forget_message(memory_id: str, message_id: int):
|
||||
try:
|
||||
@@ -220,7 +227,7 @@ async def forget_message(memory_id: str, message_id: int):
|
||||
return get_json_result(code=RetCode.SERVER_ERROR, message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/messages/<memory_id>:<message_id>", methods=["PUT"]) # noqa: F821
|
||||
@manager.route("/messages/<memory_id>:<message_id>", methods=["PUT"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("status")
|
||||
async def update_message(memory_id: str, message_id: int):
|
||||
@@ -243,13 +250,13 @@ async def update_message(memory_id: str, message_id: int):
|
||||
return get_json_result(code=RetCode.SERVER_ERROR, message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/messages/search", methods=["GET"]) # noqa: F821
|
||||
@manager.route("/messages/search", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
async def search_message():
|
||||
args = request.args
|
||||
memory_ids = args.getlist("memory_id")
|
||||
if len(memory_ids) == 1 and ',' in memory_ids[0]:
|
||||
memory_ids = memory_ids[0].split(',')
|
||||
if len(memory_ids) == 1 and "," in memory_ids[0]:
|
||||
memory_ids = memory_ids[0].split(",")
|
||||
query = args.get("query")
|
||||
similarity_threshold = float(args.get("similarity_threshold", 0.2))
|
||||
keywords_similarity_weight = float(args.get("keywords_similarity_weight", 0.7))
|
||||
@@ -258,28 +265,19 @@ async def search_message():
|
||||
session_id = args.get("session_id", "")
|
||||
user_id = args.get("user_id", "")
|
||||
|
||||
filter_dict = {
|
||||
"memory_id": memory_ids,
|
||||
"agent_id": agent_id,
|
||||
"session_id": session_id,
|
||||
"user_id": user_id
|
||||
}
|
||||
params = {
|
||||
"query": query,
|
||||
"similarity_threshold": similarity_threshold,
|
||||
"keywords_similarity_weight": keywords_similarity_weight,
|
||||
"top_n": top_n
|
||||
}
|
||||
filter_dict = {"memory_id": memory_ids, "agent_id": agent_id, "session_id": session_id, "user_id": user_id}
|
||||
params = {"query": query, "similarity_threshold": similarity_threshold, "keywords_similarity_weight": keywords_similarity_weight, "top_n": top_n}
|
||||
res = await memory_api_service.search_message(filter_dict, params)
|
||||
return get_json_result(message=True, data=res)
|
||||
|
||||
@manager.route("/messages", methods=["GET"]) # noqa: F821
|
||||
|
||||
@manager.route("/messages", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
async def get_messages():
|
||||
args = request.args
|
||||
memory_ids = args.getlist("memory_id")
|
||||
if len(memory_ids) == 1 and ',' in memory_ids[0]:
|
||||
memory_ids = memory_ids[0].split(',')
|
||||
if len(memory_ids) == 1 and "," in memory_ids[0]:
|
||||
memory_ids = memory_ids[0].split(",")
|
||||
agent_id = args.get("agent_id", "")
|
||||
session_id = args.get("session_id", "")
|
||||
limit = int(args.get("limit", 10))
|
||||
@@ -293,7 +291,7 @@ async def get_messages():
|
||||
return get_json_result(code=RetCode.SERVER_ERROR, message="Internal server error")
|
||||
|
||||
|
||||
@manager.route("/messages/<memory_id>:<message_id>/content", methods=["GET"]) # noqa: F821
|
||||
@manager.route("/messages/<memory_id>:<message_id>/content", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
async def get_message_content(memory_id: str, message_id: int):
|
||||
try:
|
||||
|
||||
@@ -188,9 +188,7 @@ async def set_default_models(tenant_id: str):
|
||||
model_type = data["model_type"]
|
||||
|
||||
try:
|
||||
success, msg = models_api_service.set_tenant_default_models(
|
||||
tenant_id, model_provider, model_instance, model_name, model_type
|
||||
)
|
||||
success, msg = models_api_service.set_tenant_default_models(tenant_id, model_provider, model_instance, model_name, model_type)
|
||||
if success:
|
||||
logging.info(f"success: {success}, msg: {msg}")
|
||||
return get_result(message=msg)
|
||||
|
||||
@@ -54,6 +54,7 @@ def _validate_llm_id(llm_id, tenant_id, llm_setting=None):
|
||||
import logging
|
||||
from api.utils.reference_metadata_utils import enrich_chunks_with_document_metadata
|
||||
|
||||
|
||||
def _build_reference_chunks(reference, include_metadata=False, metadata_fields=None):
|
||||
chunks = chunks_format(reference)
|
||||
if not include_metadata:
|
||||
@@ -195,6 +196,7 @@ async def _stream_chat_completion_sse(
|
||||
yield f"data:{json.dumps(response, ensure_ascii=False)}\n\n"
|
||||
yield "data:[DONE]\n\n"
|
||||
|
||||
|
||||
def _normalize_message_content(content):
|
||||
"""Convert OpenAI message content to a string for the dialog layer.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from api.utils.api_utils import get_json_result
|
||||
from agent.plugin import GlobalPluginManager
|
||||
|
||||
|
||||
@manager.route('/plugin/tools', methods=['GET']) # noqa: F821
|
||||
@manager.route("/plugin/tools", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def llm_tools() -> Response:
|
||||
tools = GlobalPluginManager.get_llm_tools()
|
||||
|
||||
@@ -618,9 +618,7 @@ def list_instance_models(tenant_id: str = None, provider_id_or_name: str = None,
|
||||
"""
|
||||
supported_only = request.args.get("supported", "").lower() == "true"
|
||||
try:
|
||||
success, result = provider_api_service.list_instance_models(
|
||||
tenant_id, provider_id_or_name, instance_id_or_name, supported_only
|
||||
)
|
||||
success, result = provider_api_service.list_instance_models(tenant_id, provider_id_or_name, instance_id_or_name, supported_only)
|
||||
if success:
|
||||
return get_result(data=result)
|
||||
else:
|
||||
@@ -680,9 +678,7 @@ async def update_instance_models(tenant_id: str, provider_id_or_name: str, insta
|
||||
model_name = data["model_name"]
|
||||
model_type = data["model_type"]
|
||||
try:
|
||||
success, msg = provider_api_service.update_instance_models(
|
||||
tenant_id, provider_id_or_name, instance_id_or_name, model_name, model_type
|
||||
)
|
||||
success, msg = provider_api_service.update_instance_models(tenant_id, provider_id_or_name, instance_id_or_name, model_name, model_type)
|
||||
if success:
|
||||
return get_result(message=msg)
|
||||
else:
|
||||
@@ -755,9 +751,7 @@ async def add_model_to_instance(tenant_id: str, provider_id_or_name: str, instan
|
||||
extra = data.get("extra", {})
|
||||
|
||||
try:
|
||||
success, result = provider_api_service.add_model_to_instance(
|
||||
tenant_id, provider_id_or_name, instance_id_or_name, model_name, model_type, max_tokens, extra
|
||||
)
|
||||
success, result = provider_api_service.add_model_to_instance(tenant_id, provider_id_or_name, instance_id_or_name, model_name, model_type, max_tokens, extra)
|
||||
if success:
|
||||
return get_result(message=result)
|
||||
else:
|
||||
@@ -827,9 +821,7 @@ async def enable_or_disable_model(tenant_id: str = None, provider_id_or_name: st
|
||||
return get_error_argument_result(message="status must be 'active' or 'inactive'")
|
||||
|
||||
try:
|
||||
success, msg = provider_api_service.update_model_status(
|
||||
tenant_id, provider_id_or_name, instance_id_or_name, model_name, status
|
||||
)
|
||||
success, msg = provider_api_service.update_model_status(tenant_id, provider_id_or_name, instance_id_or_name, model_name, status)
|
||||
if success:
|
||||
return get_result(message=msg)
|
||||
else:
|
||||
@@ -904,15 +896,14 @@ async def chat_to_model(tenant_id: str = None, provider_id_or_name: str = None,
|
||||
thinking = data.get("thinking", False)
|
||||
|
||||
try:
|
||||
success, result = await provider_api_service.chat_to_model(
|
||||
tenant_id, provider_id_or_name, instance_id_or_name, model_name, message, stream, thinking
|
||||
)
|
||||
success, result = await provider_api_service.chat_to_model(tenant_id, provider_id_or_name, instance_id_or_name, model_name, message, stream, thinking)
|
||||
if not success:
|
||||
return get_error_data_result(message=result)
|
||||
|
||||
if stream and isinstance(result, dict) and result.get("type") == "stream":
|
||||
# Streaming response using SSE
|
||||
from quart import Response
|
||||
|
||||
llm = result["llm"]
|
||||
|
||||
async def generate():
|
||||
@@ -925,10 +916,14 @@ async def chat_to_model(tenant_id: str = None, provider_id_or_name: str = None,
|
||||
yield f"data: [MESSAGE]{chunk}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
return Response(generate(), mimetype="text/event-stream", headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
})
|
||||
return Response(
|
||||
generate(),
|
||||
mimetype="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
)
|
||||
|
||||
# Non-streaming response
|
||||
return get_result(data=result)
|
||||
|
||||
@@ -92,7 +92,7 @@ def list_searches():
|
||||
search_apps = [s for s in search_apps if s["tenant_id"] in owner_ids]
|
||||
total = len(search_apps)
|
||||
if page_number and items_per_page:
|
||||
search_apps = search_apps[(page_number - 1) * items_per_page: page_number * items_per_page]
|
||||
search_apps = search_apps[(page_number - 1) * items_per_page : page_number * items_per_page]
|
||||
return get_json_result(data={"search_apps": search_apps, "total": total})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
@@ -151,9 +151,7 @@ async def update(search_id):
|
||||
return get_data_error_result(message="search_config must be a JSON object")
|
||||
req["search_config"] = {**current_config, **new_config}
|
||||
logging.debug(
|
||||
"Search update weight: search_id=%s user_id=%s "
|
||||
"incoming_vector_similarity_weight=%s stored_vector_similarity_weight=%s "
|
||||
"stored_full_text_weight=%s",
|
||||
"Search update weight: search_id=%s user_id=%s incoming_vector_similarity_weight=%s stored_vector_similarity_weight=%s stored_full_text_weight=%s",
|
||||
search_id,
|
||||
current_user.id,
|
||||
new_config.get("vector_similarity_weight"),
|
||||
@@ -212,8 +210,7 @@ async def completion(search_id):
|
||||
|
||||
search_config = search_app.get("search_config", {})
|
||||
logging.debug(
|
||||
"Search completion loaded weight: search_id=%s user_id=%s "
|
||||
"stored_vector_similarity_weight=%s stored_full_text_weight=%s",
|
||||
"Search completion loaded weight: search_id=%s user_id=%s stored_vector_similarity_weight=%s stored_full_text_weight=%s",
|
||||
search_id,
|
||||
uid,
|
||||
search_config.get("vector_similarity_weight", 0.3),
|
||||
@@ -229,10 +226,14 @@ async def completion(search_id):
|
||||
async for ans in async_ask(req["question"], kb_ids, uid, search_config=search_config, search_id=search_id):
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": ans}, ensure_ascii=False) + "\n\n"
|
||||
except Exception as ex:
|
||||
yield "data:" + json.dumps(
|
||||
{"code": 500, "message": str(ex), "data": {"answer": "**ERROR**: " + str(ex), "reference": []}},
|
||||
ensure_ascii=False,
|
||||
) + "\n\n"
|
||||
yield (
|
||||
"data:"
|
||||
+ json.dumps(
|
||||
{"code": 500, "message": str(ex), "data": {"answer": "**ERROR**: " + str(ex), "reference": []}},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
+ "\n\n"
|
||||
)
|
||||
yield "data:" + json.dumps({"code": 0, "message": "", "data": True}, ensure_ascii=False) + "\n\n"
|
||||
|
||||
resp = Response(stream(), mimetype="text/event-stream")
|
||||
|
||||
@@ -20,7 +20,8 @@ from api.db.services.user_service import UserTenantService
|
||||
from api.utils.api_utils import get_data_error_result, get_json_result, server_error_response
|
||||
from api.apps import login_required, current_user
|
||||
|
||||
@manager.route('/system/stats', methods=['GET']) # noqa: F821
|
||||
|
||||
@manager.route("/system/stats", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def stats():
|
||||
try:
|
||||
@@ -29,15 +30,10 @@ def stats():
|
||||
return get_data_error_result(message="Tenant not found!")
|
||||
objs = API4ConversationService.stats(
|
||||
tenants[0].tenant_id,
|
||||
request.args.get(
|
||||
"from_date",
|
||||
(datetime.now() -
|
||||
timedelta(
|
||||
days=7)).strftime("%Y-%m-%d 00:00:00")),
|
||||
request.args.get(
|
||||
"to_date",
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
||||
"agent" if "canvas_id" in request.args else None)
|
||||
request.args.get("from_date", (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d 00:00:00")),
|
||||
request.args.get("to_date", datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
||||
"agent" if "canvas_id" in request.args else None,
|
||||
)
|
||||
|
||||
res = {"pv": [], "uv": [], "speed": [], "tokens": [], "round": [], "thumb_up": []}
|
||||
|
||||
@@ -45,8 +41,8 @@ def stats():
|
||||
dt = obj["dt"]
|
||||
res["pv"].append((dt, obj["pv"]))
|
||||
res["uv"].append((dt, obj["uv"]))
|
||||
res["speed"].append((dt, float(obj["tokens"]) / (float(obj["duration"]) + 0.1))) # +0.1 to avoid division by zero
|
||||
res["tokens"].append((dt, float(obj["tokens"]) / 1000.0)) # convert to thousands
|
||||
res["speed"].append((dt, float(obj["tokens"]) / (float(obj["duration"]) + 0.1))) # +0.1 to avoid division by zero
|
||||
res["tokens"].append((dt, float(obj["tokens"]) / 1000.0)) # convert to thousands
|
||||
res["round"].append((dt, obj["round"]))
|
||||
res["thumb_up"].append((dt, obj["thumb_up"]))
|
||||
|
||||
|
||||
@@ -34,10 +34,12 @@ from common.log_utils import get_log_levels, set_log_level
|
||||
from common import settings
|
||||
from rag.utils.redis_conn import REDIS_CONN
|
||||
|
||||
|
||||
@manager.route("/system/ping", methods=["GET"]) # noqa: F821
|
||||
async def ping():
|
||||
return "pong", 200
|
||||
|
||||
|
||||
@manager.route("/system/version", methods=["GET"]) # noqa: F821
|
||||
def version():
|
||||
"""
|
||||
@@ -196,13 +198,7 @@ def oceanbase_status():
|
||||
status_info = get_oceanbase_status()
|
||||
return get_json_result(data=status_info)
|
||||
except Exception as e:
|
||||
return get_json_result(
|
||||
data={
|
||||
"status": "error",
|
||||
"message": f"Failed to get OceanBase status: {str(e)}"
|
||||
},
|
||||
code=500
|
||||
)
|
||||
return get_json_result(data={"status": "error", "message": f"Failed to get OceanBase status: {str(e)}"}, code=500)
|
||||
|
||||
|
||||
@manager.route("/system/config", methods=["GET"]) # noqa: F821
|
||||
@@ -222,16 +218,20 @@ def get_config():
|
||||
type: integer 0 means disabled, 1 means enabled
|
||||
description: Whether user registration is enabled
|
||||
"""
|
||||
return get_json_result(data={
|
||||
"registerEnabled": settings.REGISTER_ENABLED,
|
||||
"disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN,
|
||||
})
|
||||
return get_json_result(
|
||||
data={
|
||||
"registerEnabled": settings.REGISTER_ENABLED,
|
||||
"disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@manager.route("/system/healthz", methods=["GET"]) # noqa: F821
|
||||
def healthz():
|
||||
result, all_ok = run_health_checks()
|
||||
return jsonify(result), (200 if all_ok else 500)
|
||||
|
||||
|
||||
@manager.route("/system/tokens", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
def token_list():
|
||||
@@ -409,6 +409,7 @@ async def set_logger_level():
|
||||
description: Log level updated successfully
|
||||
"""
|
||||
from quart import request
|
||||
|
||||
data = await request.get_json()
|
||||
if not data or "pkg_name" not in data or "level" not in data:
|
||||
return get_data_error_result(message="pkg_name and level are required")
|
||||
|
||||
@@ -30,8 +30,7 @@ from rag.utils.redis_conn import REDIS_CONN
|
||||
@manager.route("/tasks/<task_id>/cancel", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def cancel_task(task_id):
|
||||
"""Cancel a running task.
|
||||
"""
|
||||
"""Cancel a running task."""
|
||||
return await _cancel_task(task_id)
|
||||
|
||||
|
||||
@@ -77,11 +76,7 @@ async def _cancel_task(task_id):
|
||||
TaskService.model.update(
|
||||
progress_msg=TaskService.model.progress_msg + cancel_msg,
|
||||
progress=-1,
|
||||
).where(
|
||||
(TaskService.model.id == task_id)
|
||||
& (TaskService.model.progress >= 0)
|
||||
& (TaskService.model.progress < 1)
|
||||
).execute()
|
||||
).where((TaskService.model.id == task_id) & (TaskService.model.progress >= 0) & (TaskService.model.progress < 1)).execute()
|
||||
except Exception as e:
|
||||
logging.warning("Failed to update task %s progress after cancellation: %s", task_id, str(e))
|
||||
|
||||
@@ -89,6 +84,7 @@ async def _cancel_task(task_id):
|
||||
# cancelled so that the UI reflects the state correctly.
|
||||
try:
|
||||
from api.db.services.document_service import DocumentService
|
||||
|
||||
doc_id = task.doc_id
|
||||
if doc_id and doc_id not in (CANVAS_DEBUG_DOC_ID, GRAPH_RAPTOR_FAKE_DOC_ID):
|
||||
_, doc = DocumentService.get_by_id(doc_id)
|
||||
|
||||
@@ -82,9 +82,7 @@ async def create(tenant_id):
|
||||
return get_data_error_result(message=f"{invite_user_email} is already in the team.")
|
||||
if user_tenant_role == UserTenantRole.OWNER:
|
||||
return get_data_error_result(message=f"{invite_user_email} is the owner of the team.")
|
||||
return get_data_error_result(
|
||||
message=f"{invite_user_email} is in the team, but the role: {user_tenant_role} is invalid."
|
||||
)
|
||||
return get_data_error_result(message=f"{invite_user_email} is in the team, but the role: {user_tenant_role} is invalid.")
|
||||
|
||||
UserTenantService.save(
|
||||
id=get_uuid(),
|
||||
|
||||
@@ -42,7 +42,6 @@ class CanvasReplicaService:
|
||||
LOCK_RETRY_ATTEMPTS = 3
|
||||
LOCK_RETRY_SLEEP_SECS = 0.2
|
||||
|
||||
|
||||
@classmethod
|
||||
def normalize_dsl(cls, dsl):
|
||||
"""Normalize DSL to a JSON-serializable dict. Raise ValueError on invalid input."""
|
||||
@@ -61,17 +60,14 @@ class CanvasReplicaService:
|
||||
except Exception as e:
|
||||
raise ValueError("DSL is not JSON-serializable.") from e
|
||||
|
||||
|
||||
@classmethod
|
||||
def _replica_key(cls, canvas_id: str, tenant_id: str, runtime_user_id: str) -> str:
|
||||
return f"{cls.REPLICA_KEY_PREFIX}:{canvas_id}:{tenant_id}:{runtime_user_id}"
|
||||
|
||||
|
||||
@classmethod
|
||||
def _lock_key(cls, canvas_id: str, tenant_id: str, runtime_user_id: str) -> str:
|
||||
return f"{cls.LOCK_KEY_PREFIX}:{canvas_id}:{tenant_id}:{runtime_user_id}"
|
||||
|
||||
|
||||
@classmethod
|
||||
def _read_payload(cls, replica_key: str):
|
||||
"""Read replica payload from Redis; return None on missing/invalid content."""
|
||||
@@ -88,14 +84,12 @@ class CanvasReplicaService:
|
||||
logging.warning("Failed to parse canvas replica %s: %s", replica_key, e)
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def _write_payload(cls, replica_key: str, payload: dict):
|
||||
"""Write payload and refresh TTL."""
|
||||
payload["updated_at"] = int(time.time())
|
||||
REDIS_CONN.set_obj(replica_key, payload, cls.TTL_SECS)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _build_payload(
|
||||
cls,
|
||||
@@ -116,7 +110,6 @@ class CanvasReplicaService:
|
||||
"updated_at": int(time.time()),
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def create_if_absent(
|
||||
cls,
|
||||
@@ -136,7 +129,6 @@ class CanvasReplicaService:
|
||||
cls._write_payload(replica_key, payload)
|
||||
return payload
|
||||
|
||||
|
||||
@classmethod
|
||||
def bootstrap(
|
||||
cls,
|
||||
@@ -157,14 +149,12 @@ class CanvasReplicaService:
|
||||
title=title,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_for_run(cls, canvas_id: str, tenant_id: str, runtime_user_id: str):
|
||||
"""Load current runtime replica used by /completions."""
|
||||
replica_key = cls._replica_key(canvas_id, str(tenant_id), str(runtime_user_id))
|
||||
return cls._read_payload(replica_key)
|
||||
|
||||
|
||||
@classmethod
|
||||
def replace_for_set(
|
||||
cls,
|
||||
@@ -203,7 +193,6 @@ class CanvasReplicaService:
|
||||
except Exception:
|
||||
logging.exception("Failed to release canvas replica lock: %s", lock_key)
|
||||
|
||||
|
||||
@classmethod
|
||||
def _acquire_lock_with_retry(cls, lock_key: str):
|
||||
"""Acquire distributed lock with bounded retries; return lock object or None."""
|
||||
@@ -219,7 +208,6 @@ class CanvasReplicaService:
|
||||
time.sleep(cls.LOCK_RETRY_SLEEP_SECS + random.uniform(0, 0.1))
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def commit_after_run(
|
||||
cls,
|
||||
|
||||
@@ -48,15 +48,15 @@ async def upload_file(tenant_id: str, pf_id: str, file_objs: list):
|
||||
|
||||
file_res = []
|
||||
for file_obj in file_objs:
|
||||
MAX_FILE_NUM_PER_USER = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
|
||||
MAX_FILE_NUM_PER_USER = int(os.environ.get("MAX_FILE_NUM_PER_USER", 0))
|
||||
if 0 < MAX_FILE_NUM_PER_USER <= await thread_pool_exec(DocumentService.get_doc_count, tenant_id):
|
||||
return False, "Exceed the maximum file number of a free user!"
|
||||
|
||||
if not file_obj.filename:
|
||||
file_obj_names = [pf_folder.name, file_obj.filename]
|
||||
else:
|
||||
full_path = '/' + file_obj.filename
|
||||
file_obj_names = full_path.split('/')
|
||||
full_path = "/" + file_obj.filename
|
||||
file_obj_names = full_path.split("/")
|
||||
file_len = len(file_obj_names)
|
||||
|
||||
file_id_list = await thread_pool_exec(FileService.get_id_list_by_id, pf_id, file_obj_names, 1, [pf_id])
|
||||
@@ -66,25 +66,19 @@ async def upload_file(tenant_id: str, pf_id: str, file_objs: list):
|
||||
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 1])
|
||||
if not e:
|
||||
return False, "Folder not found!"
|
||||
last_folder = await thread_pool_exec(
|
||||
FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names, len_id_list, tenant_id, tenant_id
|
||||
)
|
||||
last_folder = await thread_pool_exec(FileService.create_folder, file, file_id_list[len_id_list - 1], file_obj_names, len_id_list, tenant_id, tenant_id)
|
||||
else:
|
||||
e, file = await thread_pool_exec(FileService.get_by_id, file_id_list[len_id_list - 2])
|
||||
if not e:
|
||||
return False, "Folder not found!"
|
||||
last_folder = await thread_pool_exec(
|
||||
FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names, len_id_list, tenant_id, tenant_id
|
||||
)
|
||||
last_folder = await thread_pool_exec(FileService.create_folder, file, file_id_list[len_id_list - 2], file_obj_names, len_id_list, tenant_id, tenant_id)
|
||||
|
||||
filetype = filename_type(file_obj_names[file_len - 1])
|
||||
location = file_obj_names[file_len - 1]
|
||||
while await thread_pool_exec(settings.STORAGE_IMPL.obj_exist, last_folder.id, location):
|
||||
location += "_"
|
||||
blob = await thread_pool_exec(file_obj.read)
|
||||
filename = await thread_pool_exec(
|
||||
duplicate_name, FileService.query, name=file_obj_names[file_len - 1], parent_id=last_folder.id
|
||||
)
|
||||
filename = await thread_pool_exec(duplicate_name, FileService.query, name=file_obj_names[file_len - 1], parent_id=last_folder.id)
|
||||
await thread_pool_exec(settings.STORAGE_IMPL.put, last_folder.id, location, blob)
|
||||
file_data = {
|
||||
"id": get_uuid(),
|
||||
@@ -126,16 +120,18 @@ async def create_folder(tenant_id: str, name: str, pf_id: str = None, file_type:
|
||||
else:
|
||||
ft = FileType.VIRTUAL.value
|
||||
|
||||
file = FileService.insert({
|
||||
"id": get_uuid(),
|
||||
"parent_id": pf_id,
|
||||
"tenant_id": tenant_id,
|
||||
"created_by": tenant_id,
|
||||
"name": name,
|
||||
"location": "",
|
||||
"size": 0,
|
||||
"type": ft,
|
||||
})
|
||||
file = FileService.insert(
|
||||
{
|
||||
"id": get_uuid(),
|
||||
"parent_id": pf_id,
|
||||
"tenant_id": tenant_id,
|
||||
"created_by": tenant_id,
|
||||
"name": name,
|
||||
"location": "",
|
||||
"size": 0,
|
||||
"type": ft,
|
||||
}
|
||||
)
|
||||
return True, file.to_json()
|
||||
|
||||
|
||||
@@ -173,7 +169,6 @@ def list_files(tenant_id: str, args: dict):
|
||||
return True, {"total": total, "files": files, "parent_folder": parent_folder.to_json()}
|
||||
|
||||
|
||||
|
||||
def get_parent_folder(file_id: str, user_id: str = None):
|
||||
"""
|
||||
Get parent folder of a file with permission check.
|
||||
@@ -235,9 +230,9 @@ async def delete_files(uid: str, file_ids: list, auth_header: str = ""):
|
||||
try:
|
||||
import requests
|
||||
|
||||
host = getattr(settings, 'HOST_IP', '127.0.0.1')
|
||||
host = getattr(settings, "HOST_IP", "127.0.0.1")
|
||||
# Go service runs on port+4 (9384 by default)
|
||||
port = getattr(settings, 'HOST_PORT', 9380) + 4
|
||||
port = getattr(settings, "HOST_PORT", 9380) + 4
|
||||
service_url = f"http://{host}:{port}"
|
||||
|
||||
# List all spaces and find the one matching the name
|
||||
@@ -270,9 +265,9 @@ async def delete_files(uid: str, file_ids: list, auth_header: str = ""):
|
||||
from urllib.parse import quote
|
||||
|
||||
# Construct service URL from settings
|
||||
host = getattr(settings, 'HOST_IP', '127.0.0.1')
|
||||
host = getattr(settings, "HOST_IP", "127.0.0.1")
|
||||
# Go service runs on port+4 (9384 by default)
|
||||
port = getattr(settings, 'HOST_PORT', 9380) + 4
|
||||
port = getattr(settings, "HOST_PORT", 9380) + 4
|
||||
service_url = f"http://{host}:{port}"
|
||||
|
||||
# Get space UUID from space name
|
||||
@@ -289,37 +284,24 @@ async def delete_files(uid: str, file_ids: list, auth_header: str = ""):
|
||||
try:
|
||||
data = response.json()
|
||||
if data.get("code") == 0:
|
||||
logging.info(
|
||||
f"Successfully deleted skill index: space={space_name}, skill={skill_name}, "
|
||||
f"status={response.status_code}, code=0"
|
||||
)
|
||||
logging.info(f"Successfully deleted skill index: space={space_name}, skill={skill_name}, status={response.status_code}, code=0")
|
||||
return True
|
||||
else:
|
||||
app_code = data.get("code", "unknown")
|
||||
app_msg = data.get("message", "no message")
|
||||
logging.error(
|
||||
f"Failed to delete skill index: space={space_name}, skill={skill_name}, "
|
||||
f"status={response.status_code}, app_code={app_code}, app_msg={app_msg}, "
|
||||
f"response={response.text}"
|
||||
f"Failed to delete skill index: space={space_name}, skill={skill_name}, status={response.status_code}, app_code={app_code}, app_msg={app_msg}, response={response.text}"
|
||||
)
|
||||
return False
|
||||
except ValueError as json_err:
|
||||
# JSON decode error - treat as failure
|
||||
logging.error(
|
||||
f"Failed to parse delete response JSON: space={space_name}, skill={skill_name}, "
|
||||
f"error={json_err}, raw_response={response.text}"
|
||||
)
|
||||
logging.error(f"Failed to parse delete response JSON: space={space_name}, skill={skill_name}, error={json_err}, raw_response={response.text}")
|
||||
return False
|
||||
else:
|
||||
logging.error(
|
||||
f"Failed to delete skill index: space={space_name}, skill={skill_name}, "
|
||||
f"status={response.status_code}, response={response.text}"
|
||||
)
|
||||
logging.error(f"Failed to delete skill index: space={space_name}, skill={skill_name}, status={response.status_code}, response={response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Exception deleting skill index: space={space_name}, skill={skill_name}, error={e}"
|
||||
)
|
||||
logging.error(f"Exception deleting skill index: space={space_name}, skill={skill_name}, error={e}")
|
||||
return False
|
||||
|
||||
def _delete_single_file(file) -> int:
|
||||
@@ -408,18 +390,12 @@ async def delete_files(uid: str, file_ids: list, auth_header: str = ""):
|
||||
logging.info(f"Deleting skill index for skill '{folder.name}' in space '{current_space_name}'")
|
||||
index_deleted = _delete_skill_index(tenant_id, current_space_name, folder.name, auth_header)
|
||||
if not index_deleted:
|
||||
logging.error(
|
||||
f"Aborting folder deletion due to index deletion failure: "
|
||||
f"folder={folder.name}, space={current_space_name}"
|
||||
)
|
||||
errors.append(
|
||||
f"Failed to delete skill index for folder '{folder.name}' in space '{current_space_name}'. "
|
||||
f"Folder deletion aborted to prevent orphaned indexes."
|
||||
)
|
||||
logging.error(f"Aborting folder deletion due to index deletion failure: folder={folder.name}, space={current_space_name}")
|
||||
errors.append(f"Failed to delete skill index for folder '{folder.name}' in space '{current_space_name}'. Folder deletion aborted to prevent orphaned indexes.")
|
||||
return deleted
|
||||
sub_files = FileService.list_all_files_by_parent_id(folder.id)
|
||||
logging.info(f"Folder '{folder.name}': found {len(sub_files)} children to delete")
|
||||
|
||||
|
||||
for sub_file in sub_files:
|
||||
if sub_file.type == FileType.FOLDER.value:
|
||||
deleted += _delete_folder_recursive(sub_file, tenant_id)
|
||||
@@ -432,16 +408,16 @@ async def delete_files(uid: str, file_ids: list, auth_header: str = ""):
|
||||
errors.append(f"Failed to delete folder record {folder.id}: {e}")
|
||||
else:
|
||||
deleted += 1
|
||||
|
||||
|
||||
try:
|
||||
if hasattr(settings.STORAGE_IMPL, 'remove_bucket'):
|
||||
if hasattr(settings.STORAGE_IMPL, "remove_bucket"):
|
||||
logging.info(f"Removing storage bucket for folder '{folder.name}' (id={folder.id})")
|
||||
settings.STORAGE_IMPL.remove_bucket(folder.id)
|
||||
else:
|
||||
logging.debug(f"Storage implementation does not support remove_bucket, skipping for folder '{folder.name}'")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove storage bucket for folder '{folder.name}' (id={folder.id}): {e}")
|
||||
|
||||
|
||||
return deleted
|
||||
|
||||
def _rm_sync():
|
||||
@@ -513,8 +489,7 @@ async def move_files(uid: str, src_file_ids: list, dest_file_id: str = None, new
|
||||
|
||||
if new_name:
|
||||
file = files_dict[src_file_ids[0]]
|
||||
if file.type != FileType.FOLDER.value and \
|
||||
pathlib.Path(new_name.lower()).suffix != pathlib.Path(file.name.lower()).suffix:
|
||||
if file.type != FileType.FOLDER.value and pathlib.Path(new_name.lower()).suffix != pathlib.Path(file.name.lower()).suffix:
|
||||
return False, "The extension of file can't be changed"
|
||||
target_parent_id = dest_folder.id if dest_folder else file.parent_id
|
||||
for f in FileService.query(name=new_name, parent_id=target_parent_id):
|
||||
@@ -541,16 +516,18 @@ async def move_files(uid: str, src_file_ids: list, dest_file_id: str = None, new
|
||||
if existing_folder:
|
||||
new_folder = existing_folder[0]
|
||||
else:
|
||||
new_folder = FileService.insert({
|
||||
"id": get_uuid(),
|
||||
"parent_id": dest_folder_entry.id,
|
||||
"tenant_id": source_file_entry.tenant_id,
|
||||
"created_by": source_file_entry.tenant_id,
|
||||
"name": effective_name,
|
||||
"location": "",
|
||||
"size": 0,
|
||||
"type": FileType.FOLDER.value,
|
||||
})
|
||||
new_folder = FileService.insert(
|
||||
{
|
||||
"id": get_uuid(),
|
||||
"parent_id": dest_folder_entry.id,
|
||||
"tenant_id": source_file_entry.tenant_id,
|
||||
"created_by": source_file_entry.tenant_id,
|
||||
"name": effective_name,
|
||||
"location": "",
|
||||
"size": 0,
|
||||
"type": FileType.FOLDER.value,
|
||||
}
|
||||
)
|
||||
|
||||
sub_files = FileService.list_all_files_by_parent_id(source_file_entry.id)
|
||||
for sub_file in sub_files:
|
||||
@@ -569,8 +546,10 @@ async def move_files(uid: str, src_file_ids: list, dest_file_id: str = None, new
|
||||
new_location += "_"
|
||||
try:
|
||||
moved = settings.STORAGE_IMPL.move(
|
||||
source_file_entry.parent_id, source_file_entry.location,
|
||||
dest_folder_entry.id, new_location,
|
||||
source_file_entry.parent_id,
|
||||
source_file_entry.location,
|
||||
dest_folder_entry.id,
|
||||
new_location,
|
||||
)
|
||||
except Exception as storage_err:
|
||||
raise RuntimeError(f"Move file failed at storage layer: {str(storage_err)}")
|
||||
|
||||
@@ -98,13 +98,7 @@ async def create_memory(memory_info: dict):
|
||||
if invalid_type:
|
||||
raise ArgumentException(f"Memory type '{invalid_type}' is not supported.")
|
||||
memory_type = list(memory_type)
|
||||
success, res = MemoryService.create_memory(
|
||||
tenant_id=current_user.id,
|
||||
name=memory_name,
|
||||
memory_type=memory_type,
|
||||
embd_id=memory_info["embd_id"],
|
||||
llm_id=memory_info["llm_id"]
|
||||
)
|
||||
success, res = MemoryService.create_memory(tenant_id=current_user.id, name=memory_name, memory_type=memory_type, embd_id=memory_info["embd_id"], llm_id=memory_info["llm_id"])
|
||||
if success:
|
||||
return True, format_ret_data_from_memory(res)
|
||||
else:
|
||||
@@ -243,7 +237,7 @@ async def delete_memory(memory_id):
|
||||
return True
|
||||
|
||||
|
||||
async def list_memory(filter_params: dict, keywords: str, page: int=1, page_size: int = 50):
|
||||
async def list_memory(filter_params: dict, keywords: str, page: int = 1, page_size: int = 50):
|
||||
"""
|
||||
:param filter_params: {
|
||||
"memory_type": list[str],
|
||||
@@ -268,9 +262,7 @@ async def list_memory(filter_params: dict, keywords: str, page: int=1, page_size
|
||||
|
||||
memory_list, count = MemoryService.get_by_filter(filter_dict, keywords, page, page_size)
|
||||
[memory.update({"memory_type": get_memory_type_human(memory["memory_type"])}) for memory in memory_list]
|
||||
return {
|
||||
"memory_list": memory_list, "total_count": count
|
||||
}
|
||||
return {"memory_list": memory_list, "total_count": count}
|
||||
|
||||
|
||||
async def get_memory_config(memory_id):
|
||||
@@ -280,10 +272,9 @@ async def get_memory_config(memory_id):
|
||||
return format_ret_data_from_memory(memory)
|
||||
|
||||
|
||||
async def get_memory_messages(memory_id, agent_ids: list[str], keywords: str, page: int=1, page_size: int = 50):
|
||||
async def get_memory_messages(memory_id, agent_ids: list[str], keywords: str, page: int = 1, page_size: int = 50):
|
||||
memory = _require_memory_access(memory_id)
|
||||
messages = MessageService.list_message(
|
||||
memory.tenant_id, memory_id, agent_ids, keywords, page, page_size)
|
||||
messages = MessageService.list_message(memory.tenant_id, memory_id, agent_ids, keywords, page, page_size)
|
||||
agent_name_mapping = {}
|
||||
extract_task_mapping = {}
|
||||
if messages["message_list"]:
|
||||
@@ -291,7 +282,7 @@ async def get_memory_messages(memory_id, agent_ids: list[str], keywords: str, pa
|
||||
agent_name_mapping = {agent["id"]: agent["title"] for agent in agent_list}
|
||||
task_list = TaskService.get_tasks_progress_by_doc_ids([memory_id])
|
||||
if task_list:
|
||||
task_list.sort(key=lambda t: t["create_time"]) # asc, use newer when exist more than one task
|
||||
task_list.sort(key=lambda t: t["create_time"]) # asc, use newer when exist more than one task
|
||||
for task in task_list:
|
||||
# the 'digest' field carries the source_id when a task is created, so use 'digest' as key
|
||||
extract_task_mapping.update({int(task["digest"]): task})
|
||||
@@ -324,10 +315,7 @@ async def forget_message(memory_id: str, message_id: int):
|
||||
memory = _require_memory_access(memory_id)
|
||||
|
||||
forget_time = timestamp_to_date(current_timestamp())
|
||||
update_succeed = MessageService.update_message(
|
||||
{"memory_id": memory_id, "message_id": int(message_id)},
|
||||
{"forget_at": forget_time},
|
||||
memory.tenant_id, memory_id)
|
||||
update_succeed = MessageService.update_message({"memory_id": memory_id, "message_id": int(message_id)}, {"forget_at": forget_time}, memory.tenant_id, memory_id)
|
||||
if update_succeed:
|
||||
return True
|
||||
raise Exception(f"Failed to forget message '{message_id}' in memory '{memory_id}'.")
|
||||
@@ -336,10 +324,7 @@ async def forget_message(memory_id: str, message_id: int):
|
||||
async def update_message_status(memory_id: str, message_id: int, status: bool):
|
||||
memory = _require_memory_access(memory_id)
|
||||
|
||||
update_succeed = MessageService.update_message(
|
||||
{"memory_id": memory_id, "message_id": int(message_id)},
|
||||
{"status": status},
|
||||
memory.tenant_id, memory_id)
|
||||
update_succeed = MessageService.update_message({"memory_id": memory_id, "message_id": int(message_id)}, {"status": status}, memory.tenant_id, memory_id)
|
||||
if update_succeed:
|
||||
return True
|
||||
raise Exception(f"Failed to set status for message '{message_id}' in memory '{memory_id}'.")
|
||||
@@ -383,13 +368,7 @@ async def get_messages(memory_ids: list[str], agent_id: str = "", session_id: st
|
||||
return []
|
||||
uids = [memory.tenant_id for memory in memory_list]
|
||||
accessible_memory_ids = [memory.id for memory in memory_list]
|
||||
res = MessageService.get_recent_messages(
|
||||
uids,
|
||||
accessible_memory_ids,
|
||||
agent_id,
|
||||
session_id,
|
||||
limit
|
||||
)
|
||||
res = MessageService.get_recent_messages(uids, accessible_memory_ids, agent_id, session_id, limit)
|
||||
return res
|
||||
|
||||
|
||||
|
||||
@@ -98,11 +98,7 @@ def _get_model_info(tenant_id: str, default_model: str, model_type: str):
|
||||
# Special case: TEI Builtin embedding model
|
||||
compose_profiles = os.getenv("COMPOSE_PROFILES", "")
|
||||
tei_model = os.getenv("TEI_MODEL", "")
|
||||
if (model_type == "embedding"
|
||||
and "tei-" in compose_profiles
|
||||
and tei_model
|
||||
and model_name == tei_model
|
||||
and (not provider_name or provider_name == "Builtin")):
|
||||
if model_type == "embedding" and "tei-" in compose_profiles and tei_model and model_name == tei_model and (not provider_name or provider_name == "Builtin"):
|
||||
return {
|
||||
"model_provider": "Builtin",
|
||||
"model_instance": "default",
|
||||
@@ -124,9 +120,7 @@ def _get_model_info(tenant_id: str, default_model: str, model_type: str):
|
||||
return None
|
||||
|
||||
# Check if model is enabled (no TenantModel record or status != inactive means enabled)
|
||||
model_entity = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(
|
||||
provider_obj.id, instance_obj.id, model_type, model_name
|
||||
)
|
||||
model_entity = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(provider_obj.id, instance_obj.id, model_type, model_name)
|
||||
enable = model_entity is None or model_entity.status == ActiveStatusEnum.ACTIVE.value
|
||||
|
||||
if not enable:
|
||||
@@ -134,12 +128,12 @@ def _get_model_info(tenant_id: str, default_model: str, model_type: str):
|
||||
|
||||
if model_entity:
|
||||
return {
|
||||
"model_provider": provider_name,
|
||||
"model_instance": instance_name,
|
||||
"model_name": model_name,
|
||||
"model_type": model_type,
|
||||
"enable": enable,
|
||||
}
|
||||
"model_provider": provider_name,
|
||||
"model_instance": instance_name,
|
||||
"model_name": model_name,
|
||||
"model_type": model_type,
|
||||
"enable": enable,
|
||||
}
|
||||
|
||||
# Check if model is in the LLM factory info
|
||||
factory_info = [f for f in (FACTORY_LLM_INFOS or []) if f["name"] == provider_name]
|
||||
@@ -185,12 +179,7 @@ def _check_model_available(tenant_id: str, provider_name: str, instance_name: st
|
||||
return True, None
|
||||
|
||||
compose_profiles = os.getenv("COMPOSE_PROFILES", "")
|
||||
is_tei_builtin_embedding = (
|
||||
model_type == LLMType.EMBEDDING.value
|
||||
and "tei-" in compose_profiles
|
||||
and model_name == os.getenv("TEI_MODEL", "")
|
||||
and (provider_name == "Builtin" or not provider_name)
|
||||
)
|
||||
is_tei_builtin_embedding = model_type == LLMType.EMBEDDING.value and "tei-" in compose_profiles and model_name == os.getenv("TEI_MODEL", "") and (provider_name == "Builtin" or not provider_name)
|
||||
if is_tei_builtin_embedding:
|
||||
return True, None
|
||||
|
||||
@@ -210,9 +199,7 @@ def _check_model_available(tenant_id: str, provider_name: str, instance_name: st
|
||||
return False, f"Provider '{provider_name}' not found in factory info"
|
||||
model_type = MODEL_TAG_TO_TYPE.get(model_type, model_type)
|
||||
# Check if model is disabled
|
||||
model_entity = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(
|
||||
provider_obj.id, instance_obj.id, model_type, model_name
|
||||
)
|
||||
model_entity = TenantModelService.get_by_provider_id_and_instance_id_and_model_type_and_model_name(provider_obj.id, instance_obj.id, model_type, model_name)
|
||||
if model_entity:
|
||||
if model_entity.status != ActiveStatusEnum.ACTIVE.value:
|
||||
return False, f"Model '{model_name}' isn't available"
|
||||
@@ -297,7 +284,7 @@ def set_tenant_default_models(tenant_id: str, model_provider: str, model_instanc
|
||||
return True, "success"
|
||||
|
||||
|
||||
def list_tenant_added_models(tenant_id: str, model_type_filter: str=None):
|
||||
def list_tenant_added_models(tenant_id: str, model_type_filter: str = None):
|
||||
"""
|
||||
List all added models for a tenant.
|
||||
|
||||
@@ -369,14 +356,16 @@ def list_tenant_added_models(tenant_id: str, model_type_filter: str=None):
|
||||
if not model_types:
|
||||
continue
|
||||
|
||||
added_models.append({
|
||||
"model_type": model_types,
|
||||
"name": llm["llm_name"],
|
||||
"provider_id": factory_instance.provider_id,
|
||||
"provider_name": provider_info_map[factory_instance.provider_id].provider_name if provider_info_map.get(factory_instance.provider_id) else "",
|
||||
"instance_id": factory_instance.id,
|
||||
"instance_name": factory_instance.instance_name
|
||||
})
|
||||
added_models.append(
|
||||
{
|
||||
"model_type": model_types,
|
||||
"name": llm["llm_name"],
|
||||
"provider_id": factory_instance.provider_id,
|
||||
"provider_name": provider_info_map[factory_instance.provider_id].provider_name if provider_info_map.get(factory_instance.provider_id) else "",
|
||||
"instance_id": factory_instance.id,
|
||||
"instance_name": factory_instance.instance_name,
|
||||
}
|
||||
)
|
||||
|
||||
manual_added_model_record_keys = list(set(model_record_map.keys()) - set(model_key_in_factory))
|
||||
if manual_added_model_record_keys:
|
||||
@@ -390,33 +379,34 @@ def list_tenant_added_models(tenant_id: str, model_type_filter: str=None):
|
||||
if not model_types:
|
||||
continue
|
||||
|
||||
added_models.append({
|
||||
"model_type": model_types,
|
||||
"name": model_name,
|
||||
"provider_id": provider_id,
|
||||
"provider_name": provider_info_map[provider_id].provider_name if provider_info_map.get(provider_id) else "",
|
||||
"instance_id": instance_id,
|
||||
"instance_name": instance_info_map[instance_id].instance_name if instance_info_map.get(instance_id) else ""
|
||||
})
|
||||
added_models.append(
|
||||
{
|
||||
"model_type": model_types,
|
||||
"name": model_name,
|
||||
"provider_id": provider_id,
|
||||
"provider_name": provider_info_map[provider_id].provider_name if provider_info_map.get(provider_id) else "",
|
||||
"instance_id": instance_id,
|
||||
"instance_name": instance_info_map[instance_id].instance_name if instance_info_map.get(instance_id) else "",
|
||||
}
|
||||
)
|
||||
|
||||
# Add TEI Builtin embedding model if configured
|
||||
compose_profiles = os.getenv("COMPOSE_PROFILES", "")
|
||||
tei_model = os.getenv("TEI_MODEL", "")
|
||||
if "tei-" in compose_profiles and tei_model:
|
||||
if not model_type_filter or model_type_filter == "embedding":
|
||||
tei_already_added = any(
|
||||
m["provider_name"] == "Builtin" and m["name"] == tei_model
|
||||
for m in added_models
|
||||
)
|
||||
tei_already_added = any(m["provider_name"] == "Builtin" and m["name"] == tei_model for m in added_models)
|
||||
if not tei_already_added:
|
||||
added_models.append({
|
||||
"model_type": ["embedding"],
|
||||
"name": tei_model,
|
||||
"provider_id": "",
|
||||
"provider_name": "Builtin",
|
||||
"instance_id": "",
|
||||
"instance_name": "default",
|
||||
})
|
||||
added_models.append(
|
||||
{
|
||||
"model_type": ["embedding"],
|
||||
"name": tei_model,
|
||||
"provider_id": "",
|
||||
"provider_name": "Builtin",
|
||||
"instance_id": "",
|
||||
"instance_name": "default",
|
||||
}
|
||||
)
|
||||
|
||||
added_models.sort(key=lambda x: (factory_rank_mapping.get(x["provider_name"]), x["provider_name"], x["instance_name"]))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user