service-forge 0.1.11__py3-none-any.whl → 0.1.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of service-forge might be problematic. Click here for more details.
- service_forge/api/http_api.py +4 -0
- service_forge/api/routers/feedback/feedback_router.py +148 -0
- service_forge/api/routers/service/service_router.py +22 -32
- service_forge/current_service.py +14 -0
- service_forge/db/database.py +46 -32
- service_forge/db/migrations/feedback_migration.py +154 -0
- service_forge/db/models/__init__.py +0 -0
- service_forge/db/models/feedback.py +33 -0
- service_forge/llm/__init__.py +5 -0
- service_forge/model/feedback.py +30 -0
- service_forge/service.py +118 -126
- service_forge/service_config.py +42 -156
- service_forge/sft/cli.py +39 -0
- service_forge/sft/cmd/remote_deploy.py +160 -0
- service_forge/sft/cmd/remote_list_tars.py +111 -0
- service_forge/sft/config/injector.py +46 -24
- service_forge/sft/config/injector_default_files.py +1 -1
- service_forge/sft/config/sft_config.py +55 -8
- service_forge/storage/__init__.py +5 -0
- service_forge/storage/feedback_storage.py +245 -0
- service_forge/utils/default_type_converter.py +1 -1
- service_forge/utils/type_converter.py +5 -0
- service_forge/utils/workflow_clone.py +3 -2
- service_forge/workflow/node.py +8 -0
- service_forge/workflow/nodes/llm/query_llm_node.py +1 -1
- service_forge/workflow/trigger.py +4 -0
- service_forge/workflow/triggers/a2a_api_trigger.py +2 -0
- service_forge/workflow/triggers/fast_api_trigger.py +32 -0
- service_forge/workflow/triggers/kafka_api_trigger.py +3 -0
- service_forge/workflow/triggers/once_trigger.py +4 -1
- service_forge/workflow/triggers/period_trigger.py +4 -1
- service_forge/workflow/triggers/websocket_api_trigger.py +15 -11
- service_forge/workflow/workflow.py +74 -31
- service_forge/workflow/workflow_callback.py +3 -2
- service_forge/workflow/workflow_config.py +66 -0
- service_forge/workflow/workflow_factory.py +86 -85
- service_forge/workflow/workflow_group.py +33 -9
- {service_forge-0.1.11.dist-info → service_forge-0.1.24.dist-info}/METADATA +1 -1
- {service_forge-0.1.11.dist-info → service_forge-0.1.24.dist-info}/RECORD +41 -31
- service_forge/api/routers/service/__init__.py +0 -4
- {service_forge-0.1.11.dist-info → service_forge-0.1.24.dist-info}/WHEEL +0 -0
- {service_forge-0.1.11.dist-info → service_forge-0.1.24.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
6
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
7
|
+
|
|
8
|
+
def remote_deploy_tar(filename: str, service_center_url: str = None) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Remote deploy specified tar package from service-center
|
|
11
|
+
"""
|
|
12
|
+
# If URL is not provided, try to get it from configuration
|
|
13
|
+
if not service_center_url:
|
|
14
|
+
service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
|
|
15
|
+
|
|
16
|
+
# Ensure URL ends with /
|
|
17
|
+
if not service_center_url.endswith('/'):
|
|
18
|
+
service_center_url += '/'
|
|
19
|
+
|
|
20
|
+
api_url = f"{service_center_url}api/v1/services/deploy-from-tar"
|
|
21
|
+
|
|
22
|
+
log_info(f"Sending deployment request to {api_url}...")
|
|
23
|
+
log_info(f"Tar package to deploy: {filename}")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Prepare request data
|
|
27
|
+
data = {
|
|
28
|
+
"filename": filename
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Send POST request
|
|
32
|
+
response = requests.post(
|
|
33
|
+
api_url,
|
|
34
|
+
json=data,
|
|
35
|
+
headers={'Content-Type': 'application/json'},
|
|
36
|
+
timeout=300 # 5 minute timeout
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if response.status_code != 200:
|
|
40
|
+
log_error(f"Deployment request failed, status code: {response.status_code}")
|
|
41
|
+
try:
|
|
42
|
+
error_data = response.json()
|
|
43
|
+
log_error(f"Error message: {error_data.get('message', 'Unknown error')}")
|
|
44
|
+
if 'data' in error_data and error_data['data']:
|
|
45
|
+
log_error(f"Details: {json.dumps(error_data['data'], indent=2, ensure_ascii=False)}")
|
|
46
|
+
except:
|
|
47
|
+
log_error(f"Response content: {response.text}")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Parse response data
|
|
51
|
+
result = response.json()
|
|
52
|
+
|
|
53
|
+
if result.get('code') != 200:
|
|
54
|
+
log_error(f"Deployment failed: {result.get('message', 'Unknown error')}")
|
|
55
|
+
if 'data' in result and result['data']:
|
|
56
|
+
log_error(f"Details: {json.dumps(result['data'], indent=2, ensure_ascii=False)}")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Deployment successful
|
|
60
|
+
data = result.get('data', {})
|
|
61
|
+
service_name = data.get('service_name', 'Unknown')
|
|
62
|
+
version = data.get('version', 'Unknown')
|
|
63
|
+
deploy_output = data.get('deploy_output', '')
|
|
64
|
+
|
|
65
|
+
log_success(f"Successfully deployed service: {service_name} version: {version}")
|
|
66
|
+
|
|
67
|
+
if deploy_output:
|
|
68
|
+
log_info("Deployment output:")
|
|
69
|
+
print(deploy_output)
|
|
70
|
+
|
|
71
|
+
except requests.exceptions.Timeout:
|
|
72
|
+
log_error("Deployment request timed out (exceeded 5 minutes), please check service status or try again later")
|
|
73
|
+
except requests.exceptions.RequestException as e:
|
|
74
|
+
log_error(f"Request failed: {str(e)}")
|
|
75
|
+
log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
log_error(f"Exception occurred while deploying tar package: {str(e)}")
|
|
78
|
+
|
|
79
|
+
def remote_list_and_deploy(service_center_url: str = None) -> None:
|
|
80
|
+
"""
|
|
81
|
+
List remote tar packages first, then let user select which package to deploy
|
|
82
|
+
"""
|
|
83
|
+
# If URL is not provided, try to get it from configuration
|
|
84
|
+
if not service_center_url:
|
|
85
|
+
service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
|
|
86
|
+
|
|
87
|
+
# Ensure URL ends with /
|
|
88
|
+
if not service_center_url.endswith('/'):
|
|
89
|
+
service_center_url += '/'
|
|
90
|
+
|
|
91
|
+
api_url = f"{service_center_url}api/v1/services/tar-list"
|
|
92
|
+
|
|
93
|
+
log_info(f"Getting tar package list from {api_url}...")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# 发送GET请求获取tar包列表
|
|
97
|
+
response = requests.get(api_url, timeout=30)
|
|
98
|
+
|
|
99
|
+
if response.status_code != 200:
|
|
100
|
+
log_error(f"Failed to get tar package list, status code: {response.status_code}")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Parse response data
|
|
104
|
+
result = response.json()
|
|
105
|
+
|
|
106
|
+
if result.get('code') != 200:
|
|
107
|
+
log_error(f"Failed to get tar package list: {result.get('message', 'Unknown error')}")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
tar_files = result.get('data', [])
|
|
111
|
+
|
|
112
|
+
if not tar_files:
|
|
113
|
+
log_info("No tar packages found")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Display tar package list
|
|
117
|
+
log_info("Available tar package list:")
|
|
118
|
+
for i, tar_file in enumerate(tar_files, 1):
|
|
119
|
+
filename = tar_file.get('filename', '-')
|
|
120
|
+
service_name = tar_file.get('service_name', '-')
|
|
121
|
+
version = tar_file.get('version', '-')
|
|
122
|
+
deployed_status = "Deployed" if tar_file.get('deployed_status', False) else "Not Deployed"
|
|
123
|
+
|
|
124
|
+
print(f"{i}. {filename} (service: {service_name}, version: {version}, status: {deployed_status})")
|
|
125
|
+
|
|
126
|
+
# Let user choose
|
|
127
|
+
try:
|
|
128
|
+
choice = input("\nEnter the number of the tar package to deploy (enter 'q' to exit): ").strip()
|
|
129
|
+
|
|
130
|
+
if choice.lower() == 'q':
|
|
131
|
+
log_info("Deployment cancelled")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
index = int(choice) - 1
|
|
135
|
+
if 0 <= index < len(tar_files):
|
|
136
|
+
selected_tar = tar_files[index]
|
|
137
|
+
filename = selected_tar.get('filename')
|
|
138
|
+
|
|
139
|
+
if selected_tar.get('deployed_status', False):
|
|
140
|
+
log_warning(f"Tar package {filename} is already deployed, continue deployment?")
|
|
141
|
+
confirm = input("Enter 'y' to continue, any other key to cancel: ").strip().lower()
|
|
142
|
+
if confirm != 'y':
|
|
143
|
+
log_info("Deployment cancelled")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
log_info(f"Selected for deployment: {filename}")
|
|
147
|
+
remote_deploy_tar(filename, service_center_url)
|
|
148
|
+
else:
|
|
149
|
+
log_error("Invalid selection")
|
|
150
|
+
|
|
151
|
+
except ValueError:
|
|
152
|
+
log_error("Please enter a valid number")
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
log_info("\nDeployment cancelled")
|
|
155
|
+
|
|
156
|
+
except requests.exceptions.RequestException as e:
|
|
157
|
+
log_error(f"Request failed: {str(e)}")
|
|
158
|
+
log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
log_error(f"Exception occurred while getting tar package list: {str(e)}")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import requests
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
|
|
8
|
+
from service_forge.sft.config.sft_config import sft_config
|
|
9
|
+
|
|
10
|
+
def remote_list_tars(service_center_url: str = None) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Get remote tar package list and status from service-center
|
|
13
|
+
"""
|
|
14
|
+
# If URL is not provided, try to get it from configuration
|
|
15
|
+
if not service_center_url:
|
|
16
|
+
service_center_url = getattr(sft_config, 'service_center_address', 'http://localhost:5000')
|
|
17
|
+
|
|
18
|
+
# Ensure URL ends with /
|
|
19
|
+
if not service_center_url.endswith('/'):
|
|
20
|
+
service_center_url += '/'
|
|
21
|
+
|
|
22
|
+
api_url = f"{service_center_url}api/v1/services/tar-list"
|
|
23
|
+
|
|
24
|
+
log_info(f"Getting tar package list from {api_url}...")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# 发送GET请求
|
|
28
|
+
response = requests.get(api_url, timeout=30)
|
|
29
|
+
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
log_error(f"Failed to get tar package list, status code: {response.status_code}")
|
|
32
|
+
try:
|
|
33
|
+
error_data = response.json()
|
|
34
|
+
log_error(f"Error message: {error_data.get('message', 'Unknown error')}")
|
|
35
|
+
except:
|
|
36
|
+
log_error(f"Response content: {response.text}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Parse response data
|
|
40
|
+
result = response.json()
|
|
41
|
+
|
|
42
|
+
if result.get('code') != 200:
|
|
43
|
+
log_error(f"Failed to get tar package list: {result.get('message', 'Unknown error')}")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
tar_files = result.get('data', [])
|
|
47
|
+
|
|
48
|
+
if not tar_files:
|
|
49
|
+
log_info("No tar packages found")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Use rich table to display results
|
|
53
|
+
console = Console()
|
|
54
|
+
table = Table(title="Remote Server Tar Package List", show_header=True, header_style="bold magenta")
|
|
55
|
+
table.add_column("Filename", style="cyan", no_wrap=True)
|
|
56
|
+
table.add_column("Service Name", style="green", no_wrap=True)
|
|
57
|
+
table.add_column("Version", style="blue", no_wrap=True)
|
|
58
|
+
table.add_column("Size", justify="right", style="yellow")
|
|
59
|
+
table.add_column("Modified Time", style="dim")
|
|
60
|
+
table.add_column("Deploy Status", justify="center", style="bold")
|
|
61
|
+
|
|
62
|
+
for tar_file in tar_files:
|
|
63
|
+
# Format file size
|
|
64
|
+
size = _format_size(tar_file.get('file_size', 0))
|
|
65
|
+
|
|
66
|
+
# Format modification time
|
|
67
|
+
modified_time = _format_time(tar_file.get('modified_time', 0))
|
|
68
|
+
|
|
69
|
+
# Deployment status
|
|
70
|
+
deployed_status = "✅ Deployed" if tar_file.get('deployed_status', False) else "❌ Not Deployed"
|
|
71
|
+
status_style = "green" if tar_file.get('deployed_status', False) else "red"
|
|
72
|
+
|
|
73
|
+
table.add_row(
|
|
74
|
+
tar_file.get('filename', '-'),
|
|
75
|
+
tar_file.get('service_name', '-'),
|
|
76
|
+
tar_file.get('version', '-'),
|
|
77
|
+
size,
|
|
78
|
+
modified_time,
|
|
79
|
+
f"[{status_style}]{deployed_status}[/{status_style}]"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
console.print(table)
|
|
83
|
+
log_success(f"Found {len(tar_files)} tar packages in total")
|
|
84
|
+
|
|
85
|
+
except requests.exceptions.RequestException as e:
|
|
86
|
+
log_error(f"Request failed: {str(e)}")
|
|
87
|
+
log_info(f"Please check if service-center service is running normally and if the URL is correct: {service_center_url}")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
log_error(f"Exception occurred while getting tar package list: {str(e)}")
|
|
90
|
+
|
|
91
|
+
def _format_size(size_bytes: int) -> str:
|
|
92
|
+
"""Format file size"""
|
|
93
|
+
if size_bytes == 0:
|
|
94
|
+
return "0 B"
|
|
95
|
+
|
|
96
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
97
|
+
if size_bytes < 1024.0:
|
|
98
|
+
return f"{size_bytes:.2f} {unit}"
|
|
99
|
+
size_bytes /= 1024.0
|
|
100
|
+
return f"{size_bytes:.2f} TB"
|
|
101
|
+
|
|
102
|
+
def _format_time(timestamp: float) -> str:
|
|
103
|
+
"""Format timestamp"""
|
|
104
|
+
if timestamp == 0:
|
|
105
|
+
return "-"
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
from datetime import datetime
|
|
109
|
+
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
|
110
|
+
except:
|
|
111
|
+
return "-"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import yaml
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from service_forge.sft.util.logger import log_info, log_error
|
|
4
4
|
from service_forge.sft.config.injector_default_files import *
|
|
5
5
|
from service_forge.sft.config.sf_metadata import load_metadata
|
|
6
6
|
from service_forge.sft.config.sft_config import sft_config
|
|
7
|
-
from service_forge.service_config import ServiceConfig
|
|
7
|
+
from service_forge.service_config import ServiceConfig, ServiceFeedbackConfig
|
|
8
8
|
from service_forge.sft.util.name_util import get_service_name
|
|
9
9
|
from service_forge.sft.util.yaml_utils import load_sf_metadata_as_string
|
|
10
10
|
|
|
@@ -16,6 +16,7 @@ class Injector:
|
|
|
16
16
|
self.ingress_yaml_path = project_dir / "ingress.yaml"
|
|
17
17
|
self.dockerfile_path = project_dir / "Dockerfile"
|
|
18
18
|
self.pyproject_toml_path = project_dir / "pyproject.toml"
|
|
19
|
+
self.start_sh_path = project_dir / "start.sh"
|
|
19
20
|
self.metadata = load_metadata(self.metadata_path)
|
|
20
21
|
self.name = self.metadata.name
|
|
21
22
|
self.version = self.metadata.version
|
|
@@ -60,31 +61,41 @@ class Injector:
|
|
|
60
61
|
|
|
61
62
|
def inject_service_config(self) -> None:
|
|
62
63
|
service_config_path = self.project_dir / Path(self.metadata.service_config)
|
|
63
|
-
|
|
64
|
+
|
|
65
|
+
config = ServiceConfig.from_yaml_file(service_config_path)
|
|
64
66
|
|
|
65
67
|
config.http_port = sft_config.inject_http_port
|
|
66
68
|
config.kafka_host = sft_config.inject_kafka_host
|
|
67
69
|
config.kafka_port = sft_config.inject_kafka_port
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
database.postgres_host
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
database.mongo_host
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
database.redis_host
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
if config.databases is not None:
|
|
71
|
+
for database in config.databases:
|
|
72
|
+
if database.postgres_host is not None:
|
|
73
|
+
database.postgres_host = sft_config.inject_postgres_host
|
|
74
|
+
database.postgres_port = sft_config.inject_postgres_port
|
|
75
|
+
database.postgres_user = sft_config.inject_postgres_user
|
|
76
|
+
database.postgres_password = sft_config.inject_postgres_password
|
|
77
|
+
database.postgres_db = self.service_name
|
|
78
|
+
if database.mongo_host is not None:
|
|
79
|
+
database.mongo_host = sft_config.inject_mongo_host
|
|
80
|
+
database.mongo_port = sft_config.inject_mongo_port
|
|
81
|
+
database.mongo_user = sft_config.inject_mongo_user
|
|
82
|
+
database.mongo_password = sft_config.inject_mongo_password
|
|
83
|
+
database.mongo_db = sft_config.inject_mongo_db
|
|
84
|
+
if database.redis_host is not None:
|
|
85
|
+
database.redis_host = sft_config.inject_redis_host
|
|
86
|
+
database.redis_port = sft_config.inject_redis_port
|
|
87
|
+
database.redis_password = sft_config.inject_redis_password
|
|
88
|
+
if config.feedback is not None:
|
|
89
|
+
config.feedback.api_url = sft_config.inject_feedback_api_url
|
|
90
|
+
config.feedback.api_timeout = sft_config.inject_feedback_api_timeout
|
|
91
|
+
else:
|
|
92
|
+
config.feedback = ServiceFeedbackConfig(
|
|
93
|
+
api_url=sft_config.inject_feedback_api_url,
|
|
94
|
+
api_timeout=sft_config.inject_feedback_api_timeout,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with open(service_config_path, "w", encoding="utf-8") as f:
|
|
98
|
+
yaml.dump(config.model_dump(), f, allow_unicode=True, indent=2)
|
|
88
99
|
|
|
89
100
|
def inject_ingress(self) -> None:
|
|
90
101
|
ingress_yaml = DEFAULT_TRAEFIK_INGRESS_YAML.format(
|
|
@@ -111,9 +122,20 @@ class Injector:
|
|
|
111
122
|
f.write(pyproject_toml)
|
|
112
123
|
print("pyproject_toml_path: ", self.pyproject_toml_path)
|
|
113
124
|
|
|
125
|
+
def clear_start_sh(self) -> None:
|
|
126
|
+
if Path(self.start_sh_path).exists():
|
|
127
|
+
with open(self.start_sh_path, "rb") as f:
|
|
128
|
+
content = f.read()
|
|
129
|
+
content_str = content.decode("utf-8")
|
|
130
|
+
lines = content_str.splitlines()
|
|
131
|
+
new_content = "\n".join(lines) + ("\n" if content_str.endswith(('\n', '\r')) else "")
|
|
132
|
+
with open(self.start_sh_path, "w", encoding="utf-8", newline="\n") as f:
|
|
133
|
+
f.write(new_content)
|
|
134
|
+
|
|
114
135
|
def inject(self) -> None:
|
|
115
136
|
self.inject_deployment()
|
|
116
137
|
self.inject_service_config()
|
|
117
138
|
self.inject_ingress()
|
|
118
139
|
self.inject_dockerfile()
|
|
119
|
-
self.inject_pyproject_toml()
|
|
140
|
+
self.inject_pyproject_toml()
|
|
141
|
+
self.clear_start_sh()
|
|
@@ -21,6 +21,8 @@ class SftConfig:
|
|
|
21
21
|
"inject_postgres_port": "Postgres port for services",
|
|
22
22
|
"inject_postgres_user": "Postgres user for services",
|
|
23
23
|
"inject_postgres_password": "Postgres password for services",
|
|
24
|
+
"inject_feedback_api_url": "Feedback API URL for services",
|
|
25
|
+
"inject_feedback_api_timeout": "Feedback API timeout for services",
|
|
24
26
|
"deepseek_api_key": "DeepSeek API key",
|
|
25
27
|
"deepseek_base_url": "DeepSeek base URL",
|
|
26
28
|
}
|
|
@@ -52,6 +54,9 @@ class SftConfig:
|
|
|
52
54
|
inject_redis_port: int = 6379,
|
|
53
55
|
inject_redis_password: str = "rDdM2Y2gX9",
|
|
54
56
|
|
|
57
|
+
inject_feedback_api_url: str = "http://vps.shiweinan.com:37919/api/v1/feedback",
|
|
58
|
+
inject_feedback_api_timeout: int = 5,
|
|
59
|
+
|
|
55
60
|
deepseek_api_key: str = "82c9df22-f6ed-411e-90d7-c5255376b7ca",
|
|
56
61
|
deepseek_base_url: str = "https://ark.cn-beijing.volces.com/api/v3",
|
|
57
62
|
):
|
|
@@ -80,6 +85,9 @@ class SftConfig:
|
|
|
80
85
|
self.inject_redis_port = inject_redis_port
|
|
81
86
|
self.inject_redis_password = inject_redis_password
|
|
82
87
|
|
|
88
|
+
self.inject_feedback_api_url = inject_feedback_api_url
|
|
89
|
+
self.inject_feedback_api_timeout = inject_feedback_api_timeout
|
|
90
|
+
|
|
83
91
|
self.deepseek_api_key = deepseek_api_key
|
|
84
92
|
self.deepseek_base_url = deepseek_base_url
|
|
85
93
|
|
|
@@ -91,10 +99,23 @@ class SftConfig:
|
|
|
91
99
|
def upload_timeout(self) -> int:
|
|
92
100
|
return 300 # 5 minutes default timeout
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
sig = inspect.signature(
|
|
97
|
-
|
|
102
|
+
def get_config_keys(self) -> list[str]:
|
|
103
|
+
# Get initial configuration parameters from __init__ method
|
|
104
|
+
sig = inspect.signature(self.__class__.__init__)
|
|
105
|
+
init_keys = [param for param in sig.parameters.keys() if param != 'self']
|
|
106
|
+
|
|
107
|
+
# Get all instance attributes (including dynamically added configurations)
|
|
108
|
+
instance_keys = []
|
|
109
|
+
for attr_name in dir(self):
|
|
110
|
+
# Exclude special methods, private attributes, class attributes, and methods
|
|
111
|
+
if (not attr_name.startswith('_') and
|
|
112
|
+
not callable(getattr(self, attr_name)) and
|
|
113
|
+
attr_name not in ['CONFIG_ROOT', 'CONFIG_DESCRIPTIONS']):
|
|
114
|
+
instance_keys.append(attr_name)
|
|
115
|
+
|
|
116
|
+
# Merge and deduplicate, maintaining order (initial configs first, then dynamically added)
|
|
117
|
+
all_keys = list(dict.fromkeys(init_keys + instance_keys))
|
|
118
|
+
return all_keys
|
|
98
119
|
|
|
99
120
|
@property
|
|
100
121
|
def config_file_path(self) -> Path:
|
|
@@ -105,13 +126,30 @@ class SftConfig:
|
|
|
105
126
|
|
|
106
127
|
def to_dict(self) -> dict:
|
|
107
128
|
config_keys = self.get_config_keys()
|
|
108
|
-
|
|
129
|
+
result = {}
|
|
130
|
+
for key in config_keys:
|
|
131
|
+
value = getattr(self, key)
|
|
132
|
+
# Convert Path objects to strings for JSON serialization
|
|
133
|
+
if isinstance(value, Path):
|
|
134
|
+
value = str(value)
|
|
135
|
+
result[key] = value
|
|
136
|
+
return result
|
|
109
137
|
|
|
110
138
|
def from_dict(self, data: dict) -> None:
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
# Get initial configuration parameters from __init__ method
|
|
140
|
+
sig = inspect.signature(self.__class__.__init__)
|
|
141
|
+
init_keys = [param for param in sig.parameters.keys() if param != 'self']
|
|
142
|
+
|
|
143
|
+
# First, set all initial configuration parameters
|
|
144
|
+
for key in init_keys:
|
|
113
145
|
if key in data:
|
|
114
146
|
setattr(self, key, data[key])
|
|
147
|
+
|
|
148
|
+
# Then, handle any additional keys that might be dynamically added configurations
|
|
149
|
+
for key, value in data.items():
|
|
150
|
+
if key not in init_keys and key not in ['CONFIG_ROOT', 'CONFIG_DESCRIPTIONS']:
|
|
151
|
+
# This might be a dynamically added configuration
|
|
152
|
+
setattr(self, key, value)
|
|
115
153
|
|
|
116
154
|
def save(self) -> None:
|
|
117
155
|
self.ensure_config_dir()
|
|
@@ -121,13 +159,22 @@ class SftConfig:
|
|
|
121
159
|
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
122
160
|
return getattr(self, key, default)
|
|
123
161
|
|
|
124
|
-
def set(self, key: str, value: str) -> None:
|
|
162
|
+
def set(self, key: str, value: str, description: Optional[str] = None) -> None:
|
|
125
163
|
if key in ["config_root"]:
|
|
126
164
|
raise ValueError(f"{key} is read-only")
|
|
127
165
|
if hasattr(self, key):
|
|
128
166
|
setattr(self, key, value)
|
|
167
|
+
if description:
|
|
168
|
+
self.CONFIG_DESCRIPTIONS[key] = description
|
|
129
169
|
else:
|
|
130
170
|
raise ValueError(f"Unknown config key: {key}")
|
|
171
|
+
|
|
172
|
+
def add(self, key: str, value: str, description: Optional[str] = None) -> None:
|
|
173
|
+
if hasattr(self, key):
|
|
174
|
+
raise ValueError(f"{key} already exists")
|
|
175
|
+
setattr(self, key, value)
|
|
176
|
+
if description:
|
|
177
|
+
self.CONFIG_DESCRIPTIONS[key] = description
|
|
131
178
|
|
|
132
179
|
def update(self, updates: dict) -> None:
|
|
133
180
|
for key, value in updates.items():
|