service-forge 0.1.18__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.

Files changed (83) hide show
  1. service_forge/api/deprecated_websocket_api.py +86 -0
  2. service_forge/api/deprecated_websocket_manager.py +425 -0
  3. service_forge/api/http_api.py +152 -0
  4. service_forge/api/http_api_doc.py +455 -0
  5. service_forge/api/kafka_api.py +126 -0
  6. service_forge/api/routers/feedback/feedback_router.py +148 -0
  7. service_forge/api/routers/service/service_router.py +127 -0
  8. service_forge/api/routers/websocket/websocket_manager.py +83 -0
  9. service_forge/api/routers/websocket/websocket_router.py +78 -0
  10. service_forge/api/task_manager.py +141 -0
  11. service_forge/current_service.py +14 -0
  12. service_forge/db/__init__.py +1 -0
  13. service_forge/db/database.py +237 -0
  14. service_forge/db/migrations/feedback_migration.py +154 -0
  15. service_forge/db/models/__init__.py +0 -0
  16. service_forge/db/models/feedback.py +33 -0
  17. service_forge/llm/__init__.py +67 -0
  18. service_forge/llm/llm.py +56 -0
  19. service_forge/model/__init__.py +0 -0
  20. service_forge/model/feedback.py +30 -0
  21. service_forge/model/websocket.py +13 -0
  22. service_forge/proto/foo_input.py +5 -0
  23. service_forge/service.py +280 -0
  24. service_forge/service_config.py +44 -0
  25. service_forge/sft/cli.py +91 -0
  26. service_forge/sft/cmd/config_command.py +67 -0
  27. service_forge/sft/cmd/deploy_service.py +123 -0
  28. service_forge/sft/cmd/list_tars.py +41 -0
  29. service_forge/sft/cmd/service_command.py +149 -0
  30. service_forge/sft/cmd/upload_service.py +36 -0
  31. service_forge/sft/config/injector.py +129 -0
  32. service_forge/sft/config/injector_default_files.py +131 -0
  33. service_forge/sft/config/sf_metadata.py +30 -0
  34. service_forge/sft/config/sft_config.py +200 -0
  35. service_forge/sft/file/__init__.py +0 -0
  36. service_forge/sft/file/ignore_pattern.py +80 -0
  37. service_forge/sft/file/sft_file_manager.py +107 -0
  38. service_forge/sft/kubernetes/kubernetes_manager.py +257 -0
  39. service_forge/sft/util/assert_util.py +25 -0
  40. service_forge/sft/util/logger.py +16 -0
  41. service_forge/sft/util/name_util.py +8 -0
  42. service_forge/sft/util/yaml_utils.py +57 -0
  43. service_forge/storage/__init__.py +5 -0
  44. service_forge/storage/feedback_storage.py +245 -0
  45. service_forge/utils/__init__.py +0 -0
  46. service_forge/utils/default_type_converter.py +12 -0
  47. service_forge/utils/register.py +39 -0
  48. service_forge/utils/type_converter.py +99 -0
  49. service_forge/utils/workflow_clone.py +124 -0
  50. service_forge/workflow/__init__.py +1 -0
  51. service_forge/workflow/context.py +14 -0
  52. service_forge/workflow/edge.py +24 -0
  53. service_forge/workflow/node.py +184 -0
  54. service_forge/workflow/nodes/__init__.py +8 -0
  55. service_forge/workflow/nodes/control/if_node.py +29 -0
  56. service_forge/workflow/nodes/control/switch_node.py +28 -0
  57. service_forge/workflow/nodes/input/console_input_node.py +26 -0
  58. service_forge/workflow/nodes/llm/query_llm_node.py +41 -0
  59. service_forge/workflow/nodes/nested/workflow_node.py +28 -0
  60. service_forge/workflow/nodes/output/kafka_output_node.py +27 -0
  61. service_forge/workflow/nodes/output/print_node.py +29 -0
  62. service_forge/workflow/nodes/test/if_console_input_node.py +33 -0
  63. service_forge/workflow/nodes/test/time_consuming_node.py +62 -0
  64. service_forge/workflow/port.py +89 -0
  65. service_forge/workflow/trigger.py +28 -0
  66. service_forge/workflow/triggers/__init__.py +6 -0
  67. service_forge/workflow/triggers/a2a_api_trigger.py +257 -0
  68. service_forge/workflow/triggers/fast_api_trigger.py +201 -0
  69. service_forge/workflow/triggers/kafka_api_trigger.py +47 -0
  70. service_forge/workflow/triggers/once_trigger.py +23 -0
  71. service_forge/workflow/triggers/period_trigger.py +29 -0
  72. service_forge/workflow/triggers/websocket_api_trigger.py +189 -0
  73. service_forge/workflow/workflow.py +227 -0
  74. service_forge/workflow/workflow_callback.py +141 -0
  75. service_forge/workflow/workflow_config.py +66 -0
  76. service_forge/workflow/workflow_event.py +15 -0
  77. service_forge/workflow/workflow_factory.py +246 -0
  78. service_forge/workflow/workflow_group.py +51 -0
  79. service_forge/workflow/workflow_type.py +52 -0
  80. service_forge-0.1.18.dist-info/METADATA +98 -0
  81. service_forge-0.1.18.dist-info/RECORD +83 -0
  82. service_forge-0.1.18.dist-info/WHEEL +4 -0
  83. service_forge-0.1.18.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,149 @@
1
+ from typing import List, Optional
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from kubernetes import client, config
6
+ from kubernetes.client.rest import ApiException
7
+
8
+ from service_forge.sft.config.sft_config import sft_config
9
+ from service_forge.sft.util.logger import log_error, log_info, log_success, log_warning
10
+ from service_forge.sft.kubernetes.kubernetes_manager import KubernetesManager
11
+
12
+ def list_services() -> None:
13
+ namespace = sft_config.k8s_namespace
14
+ kubernetes_manager = KubernetesManager()
15
+ services = kubernetes_manager.get_services_in_namespace(namespace)
16
+
17
+ if not services:
18
+ log_warning(f"No services starting with 'sf-' found in namespace '{namespace}'")
19
+ return
20
+
21
+ console = Console()
22
+ table = Table(title=f"Services in namespace '{namespace}' (sf-*)", show_header=True, header_style="bold magenta")
23
+ table.add_column("Name", style="cyan", no_wrap=True)
24
+ table.add_column("Type", style="green")
25
+ table.add_column("Port", style="yellow")
26
+ table.add_column("Target Port", style="yellow")
27
+
28
+ for service_name in sorted(services):
29
+ details = kubernetes_manager.get_service_details(namespace, service_name)
30
+ table.add_row(
31
+ details.name,
32
+ details.type or "-",
33
+ str(details.port or "-"),
34
+ str(details.target_port or "-")
35
+ )
36
+
37
+ console.print(table)
38
+ log_info(f"Found {len(services)} service(s)")
39
+
40
+ def show_logs(
41
+ namespace: str,
42
+ pod_name: str,
43
+ container_name: str,
44
+ tail: int,
45
+ follow: bool,
46
+ previous: bool
47
+ ) -> None:
48
+ kubernetes_manager = KubernetesManager()
49
+ try:
50
+ if not follow:
51
+ log_info(f"Fetching logs from pod '{pod_name}' (container: {container_name})...")
52
+
53
+ logs = kubernetes_manager.get_pod_logs(
54
+ namespace=namespace,
55
+ pod_name=pod_name,
56
+ container_name=container_name,
57
+ tail=tail,
58
+ follow=follow,
59
+ previous=previous
60
+ )
61
+
62
+ if follow:
63
+ try:
64
+ for line in logs:
65
+ log_info(line, end="")
66
+ except KeyboardInterrupt:
67
+ log_warning("Log streaming interrupted")
68
+ raise typer.Exit(0)
69
+ else:
70
+ if logs:
71
+ log_info(logs)
72
+ else:
73
+ log_warning(f"No logs available for pod '{pod_name}' container '{container_name}'")
74
+
75
+ except ApiException as e:
76
+ if e.status == 404:
77
+ log_error(f"Pod '{pod_name}' or container '{container_name}' not found")
78
+ elif e.status == 400:
79
+ log_error(f"Bad request: {e.reason}")
80
+ if "previous" in str(e.body).lower():
81
+ log_info("Note: 'previous' flag only works for stopped containers")
82
+ else:
83
+ log_error(f"Failed to get logs: {e.reason}")
84
+ if e.body:
85
+ log_error(f"Error details: {e.body}")
86
+ except Exception as e:
87
+ log_error(f"Failed to get logs: {e}")
88
+
89
+
90
+ def show_service_logs(
91
+ service_name: str,
92
+ container: Optional[str] = None,
93
+ tail: int = 100,
94
+ follow: bool = False,
95
+ previous: bool = False
96
+ ) -> None:
97
+ namespace = sft_config.k8s_namespace
98
+ kubernetes_manager = KubernetesManager()
99
+
100
+ if not service_name.startswith("sf-"):
101
+ log_warning(f"Service name '{service_name}' does not start with 'sf-'. Proceeding anyway...")
102
+
103
+ services = kubernetes_manager.get_services_in_namespace(namespace)
104
+ if service_name not in services:
105
+ log_error(f"Service '{service_name}' not found in namespace '{namespace}'")
106
+ log_info(f"Available services: {', '.join(services) if services else 'None'}")
107
+ raise typer.Exit(1)
108
+
109
+ pod_names = kubernetes_manager.get_pods_for_service(namespace, service_name)
110
+ if not pod_names:
111
+ log_error(f"No pods found for service '{service_name}' in namespace '{namespace}'")
112
+ raise typer.Exit(1)
113
+
114
+ log_info(f"Found {len(pod_names)} pod(s) for service '{service_name}'")
115
+
116
+ for pod_name in pod_names:
117
+ containers = kubernetes_manager.get_pod_containers(namespace, pod_name)
118
+
119
+ if not containers:
120
+ log_warning(f"No containers found in pod '{pod_name}'")
121
+ continue
122
+
123
+ if container and container not in containers:
124
+ log_error(f"Container '{container}' not found in pod '{pod_name}'")
125
+ log_info(f"Available containers: {', '.join(containers)}")
126
+ continue
127
+
128
+ target_containers = [container] if container else containers
129
+
130
+ for container_name in target_containers:
131
+ show_logs(namespace, pod_name, container_name, tail, follow, previous)
132
+
133
+ def delete_service(service_name: str, force: bool = False) -> None:
134
+ namespace = sft_config.k8s_namespace
135
+
136
+ if not service_name.startswith("sf-"):
137
+ log_error(f"Service name '{service_name}' does not start with 'sf-'")
138
+ raise typer.Exit(1)
139
+
140
+ kubernetes_manager = KubernetesManager()
141
+
142
+ services = kubernetes_manager.get_services_in_namespace(namespace)
143
+ if service_name not in services:
144
+ log_warning(f"Service '{service_name}' not found in namespace '{namespace}'")
145
+ log_info(f"Available services: {', '.join(services) if services else 'None'}")
146
+
147
+ log_info(f"Deleting service '{service_name}' from namespace '{namespace}'...")
148
+ kubernetes_manager.delete_service(namespace, service_name, force)
149
+
@@ -0,0 +1,36 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from service_forge.sft.util.logger import log_error, log_success
4
+ from service_forge.sft.file.sft_file_manager import sft_file_manager
5
+ from service_forge.sft.util.assert_util import assert_dir_exists, assert_file_exists
6
+ from service_forge.sft.config.sf_metadata import load_metadata
7
+
8
+ def upload_service(project_path: str) -> None:
9
+ project_dir = Path(project_path).resolve()
10
+ assert_dir_exists(project_dir)
11
+
12
+ metadata_path = project_dir / "sf-meta.yaml"
13
+ assert_file_exists(metadata_path)
14
+
15
+ try:
16
+ meta_data = load_metadata(metadata_path)
17
+ except Exception as e:
18
+ log_error(f"Failed to read sf-meta.yaml: {e}")
19
+ raise typer.Exit(1)
20
+
21
+ try:
22
+ tar_file = sft_file_manager.create_tar(project_dir, meta_data.name, meta_data.version)
23
+ except Exception as e:
24
+ log_error(f"Failed to create tar file: {e}")
25
+ raise typer.Exit(1)
26
+
27
+ log_success(f"Packaging successful: {tar_file}")
28
+
29
+ # upload to the service
30
+ try:
31
+ sft_file_manager.upload_tar(tar_file)
32
+ except Exception as e:
33
+ log_error(f"Failed to upload tar file: {e}")
34
+ raise typer.Exit(1)
35
+
36
+ log_success(f"Upload successful: {tar_file}")
@@ -0,0 +1,129 @@
1
+ import yaml
2
+ from pathlib import Path
3
+ from service_forge.sft.util.logger import log_info, log_error
4
+ from service_forge.sft.config.injector_default_files import *
5
+ from service_forge.sft.config.sf_metadata import load_metadata
6
+ from service_forge.sft.config.sft_config import sft_config
7
+ from service_forge.service_config import ServiceConfig, ServiceFeedbackConfig
8
+ from service_forge.sft.util.name_util import get_service_name
9
+ from service_forge.sft.util.yaml_utils import load_sf_metadata_as_string
10
+
11
+ class Injector:
12
+ def __init__(self, project_dir: Path):
13
+ self.project_dir = project_dir
14
+ self.deployment_yaml_path = project_dir / "deployment.yaml"
15
+ self.metadata_path = project_dir / "sf-meta.yaml"
16
+ self.ingress_yaml_path = project_dir / "ingress.yaml"
17
+ self.dockerfile_path = project_dir / "Dockerfile"
18
+ self.pyproject_toml_path = project_dir / "pyproject.toml"
19
+ self.metadata = load_metadata(self.metadata_path)
20
+ self.name = self.metadata.name
21
+ self.version = self.metadata.version
22
+ self.namespace = sft_config.k8s_namespace
23
+
24
+ try:
25
+ self.sf_metadata_string = load_sf_metadata_as_string(self.metadata_path)
26
+ except Exception as e:
27
+ log_error(f"Failed to load sf-metadata as string: {e}")
28
+ self.sf_metadata_string = ""
29
+
30
+ @property
31
+ def service_name(self) -> str:
32
+ return get_service_name(self.name, self.version)
33
+
34
+ def inject_deployment(self) -> None:
35
+ single_line_metadata = self.sf_metadata_string.replace('\n', '\\n').replace('"', '\\"')
36
+
37
+ envs = {
38
+ "DEEPSEEK_API_KEY": sft_config.deepseek_api_key,
39
+ "DEEPSEEK_BASE_URL": sft_config.deepseek_base_url,
40
+ }
41
+
42
+ for env in self.metadata.env:
43
+ envs[env['name']] = env['value']
44
+
45
+ env_str = ""
46
+ for key, value in envs.items():
47
+ env_str += f" - name: {key}\n value: {value}\n"
48
+
49
+ deployment_yaml = DEFAULT_DEPLOYMENT_YAML.format(
50
+ service_name=self.service_name,
51
+ name=self.name,
52
+ version=self.version,
53
+ namespace=self.namespace,
54
+ sf_metadata=f'"{single_line_metadata}"',
55
+ env=env_str,
56
+ )
57
+ with open(self.deployment_yaml_path, "w") as f:
58
+ f.write(deployment_yaml)
59
+ print("deployment_yaml_path: ", self.deployment_yaml_path)
60
+
61
+ def inject_service_config(self) -> None:
62
+ service_config_path = self.project_dir / Path(self.metadata.service_config)
63
+
64
+ config = ServiceConfig.from_yaml_file(service_config_path)
65
+
66
+ config.http_port = sft_config.inject_http_port
67
+ config.kafka_host = sft_config.inject_kafka_host
68
+ config.kafka_port = sft_config.inject_kafka_port
69
+ if config.databases is not None:
70
+ for database in config.databases:
71
+ if database.postgres_host is not None:
72
+ database.postgres_host = sft_config.inject_postgres_host
73
+ database.postgres_port = sft_config.inject_postgres_port
74
+ database.postgres_user = sft_config.inject_postgres_user
75
+ database.postgres_password = sft_config.inject_postgres_password
76
+ database.postgres_db = self.service_name
77
+ if database.mongo_host is not None:
78
+ database.mongo_host = sft_config.inject_mongo_host
79
+ database.mongo_port = sft_config.inject_mongo_port
80
+ database.mongo_user = sft_config.inject_mongo_user
81
+ database.mongo_password = sft_config.inject_mongo_password
82
+ database.mongo_db = sft_config.inject_mongo_db
83
+ if database.redis_host is not None:
84
+ database.redis_host = sft_config.inject_redis_host
85
+ database.redis_port = sft_config.inject_redis_port
86
+ database.redis_password = sft_config.inject_redis_password
87
+ if config.feedback is not None:
88
+ config.feedback.api_url = sft_config.inject_feedback_api_url
89
+ config.feedback.api_timeout = sft_config.inject_feedback_api_timeout
90
+ else:
91
+ config.feedback = ServiceFeedbackConfig(
92
+ api_url=sft_config.inject_feedback_api_url,
93
+ api_timeout=sft_config.inject_feedback_api_timeout,
94
+ )
95
+
96
+ with open(service_config_path, "w", encoding="utf-8") as f:
97
+ yaml.dump(config.model_dump(), f, allow_unicode=True, indent=2)
98
+
99
+ def inject_ingress(self) -> None:
100
+ ingress_yaml = DEFAULT_TRAEFIK_INGRESS_YAML.format(
101
+ name=self.name,
102
+ version=self.version.replace(".", "-"),
103
+ namespace=self.namespace,
104
+ )
105
+ with open(self.ingress_yaml_path, "w") as f:
106
+ f.write(ingress_yaml)
107
+ print("ingress_yaml_path: ", self.ingress_yaml_path)
108
+
109
+ def inject_dockerfile(self) -> None:
110
+ dockerfile = DEFAULT_DOCKERFILE
111
+ with open(self.dockerfile_path, "w") as f:
112
+ f.write(dockerfile)
113
+ print("dockerfile_path: ", self.dockerfile_path)
114
+
115
+ def inject_pyproject_toml(self) -> None:
116
+ pyproject_toml = DEFAULT_PYPROJECT_TOML
117
+ with open(self.pyproject_toml_path, "r") as f:
118
+ existing_pyproject_toml = f.read()
119
+ if pyproject_toml.strip() not in existing_pyproject_toml.strip():
120
+ with open(self.pyproject_toml_path, "a") as f:
121
+ f.write(pyproject_toml)
122
+ print("pyproject_toml_path: ", self.pyproject_toml_path)
123
+
124
+ def inject(self) -> None:
125
+ self.inject_deployment()
126
+ self.inject_service_config()
127
+ self.inject_ingress()
128
+ self.inject_dockerfile()
129
+ self.inject_pyproject_toml()
@@ -0,0 +1,131 @@
1
+ DEFAULT_DEPLOYMENT_YAML = """
2
+ apiVersion: apps/v1
3
+ kind: Deployment
4
+ metadata:
5
+ name: {service_name}
6
+ namespace: {namespace}
7
+ spec:
8
+ replicas: 1
9
+ selector:
10
+ matchLabels:
11
+ app: {service_name}
12
+ template:
13
+ metadata:
14
+ labels:
15
+ app: {service_name}
16
+ spec:
17
+ imagePullSecrets:
18
+ - name: aliyun-regcred
19
+ containers:
20
+ - name: {service_name}
21
+ image: crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci/sf-{name}:{version}
22
+ imagePullPolicy: Always
23
+ ports:
24
+ - containerPort: 8000
25
+ env:
26
+ {env}
27
+
28
+ ---
29
+ apiVersion: v1
30
+ kind: Service
31
+ metadata:
32
+ name: {service_name}
33
+ namespace: {namespace}
34
+ annotations:
35
+ metadata: {sf_metadata}
36
+ spec:
37
+ type: ClusterIP
38
+ selector:
39
+ app: {service_name}
40
+ ports:
41
+ - protocol: TCP
42
+ port: 80
43
+ targetPort: 8000
44
+ """
45
+
46
+ DEFAULT_TRAEFIK_INGRESS_YAML = """
47
+ apiVersion: traefik.io/v1alpha1
48
+ kind: Middleware
49
+ metadata:
50
+ name: strip-prefix-sf-{name}-{version}v
51
+ namespace: {namespace}
52
+ spec:
53
+ stripPrefix:
54
+ prefixes:
55
+ - /api/v1/{name}-{version}
56
+
57
+ ---
58
+
59
+ apiVersion: traefik.io/v1alpha1
60
+ kind: IngressRoute
61
+ metadata:
62
+ name: sf-{name}-{version}v
63
+ namespace: {namespace}
64
+ spec:
65
+ entryPoints:
66
+ - web
67
+ routes:
68
+ - match: PathPrefix(`/api/v1/{name}-{version}/openapi.json`)
69
+ kind: Rule
70
+ services:
71
+ - name: sf-{name}-{version}v
72
+ namespace: {namespace}
73
+ port: 80
74
+ middlewares:
75
+ - name: strip-prefix-sf-{name}-{version}v
76
+ namespace: {namespace}
77
+ - name: cors
78
+ namespace: {namespace}
79
+
80
+ - match: PathPrefix(`/api/v1/{name}-{version}/docs`)
81
+ kind: Rule
82
+ services:
83
+ - name: sf-{name}-{version}v
84
+ namespace: {namespace}
85
+ port: 80
86
+ middlewares:
87
+ - name: strip-prefix-sf-{name}-{version}v
88
+ namespace: {namespace}
89
+ - name: cors
90
+ namespace: {namespace}
91
+
92
+ - match: PathPrefix(`/api/v1/{name}-{version}`)
93
+ kind: Rule
94
+ services:
95
+ - name: sf-{name}-{version}v
96
+ namespace: {namespace}
97
+ port: 80
98
+ middlewares:
99
+ - name: strip-prefix-sf-{name}-{version}v
100
+ namespace: {namespace}
101
+ - name: cors
102
+ namespace: {namespace}
103
+ - name: jwt-auth
104
+ namespace: {namespace}
105
+ """
106
+
107
+ DEFAULT_DOCKERFILE = """
108
+ FROM crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci/service-forge:latest
109
+
110
+ WORKDIR /app
111
+
112
+ COPY . ./service
113
+
114
+ WORKDIR /app
115
+ RUN uv sync
116
+
117
+ ENV PYTHONPATH=/app/service:/app:/app/src
118
+ ENV PATH="/app/.venv/bin:$PATH"
119
+
120
+ WORKDIR /app/service
121
+
122
+ RUN chmod +x start.sh
123
+
124
+ CMD ["./start.sh"]
125
+ """
126
+
127
+ DEFAULT_PYPROJECT_TOML = """
128
+
129
+ [tool.uv.sources]
130
+ service-forge = { workspace = true }
131
+ """
@@ -0,0 +1,30 @@
1
+ from omegaconf import OmegaConf
2
+
3
+ class SfMetadata:
4
+ def __init__(
5
+ self,
6
+ name: str,
7
+ version: str,
8
+ description: str,
9
+ service_config: str,
10
+ config_only: bool,
11
+ env: list[dict],
12
+ ) -> None:
13
+ self.name = name
14
+ self.version = version
15
+ self.description = description
16
+ self.service_config = service_config
17
+ self.config_only = config_only
18
+ self.env = env
19
+
20
+ def load_metadata(path: str) -> SfMetadata:
21
+ with open(path, 'r') as file:
22
+ data = OmegaConf.load(file)
23
+ return SfMetadata(
24
+ name=data.get('name'),
25
+ version=data.get('version'),
26
+ description=data.get('description'),
27
+ service_config=data.get('service_config'),
28
+ config_only=data.get('config_only'),
29
+ env=data.get('env', []),
30
+ )
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from omegaconf import OmegaConf
7
+
8
+ class SftConfig:
9
+ CONFIG_ROOT = Path.home() / ".sft"
10
+
11
+ # Configuration descriptions mapping
12
+ CONFIG_DESCRIPTIONS = {
13
+ "sft_file_root": "SFT file storage root directory",
14
+ "service_center_address": "Service center address",
15
+ "k8s_namespace": "Kubernetes namespace",
16
+ "registry_address": "Registry address",
17
+ "inject_http_port": "HTTP port for services",
18
+ "inject_kafka_host": "Kafka host for services",
19
+ "inject_kafka_port": "Kafka port for services",
20
+ "inject_postgres_host": "Postgres host for services",
21
+ "inject_postgres_port": "Postgres port for services",
22
+ "inject_postgres_user": "Postgres user for services",
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",
26
+ "deepseek_api_key": "DeepSeek API key",
27
+ "deepseek_base_url": "DeepSeek base URL",
28
+ }
29
+
30
+ def __init__(
31
+ self,
32
+ sft_file_root: str = "/tmp/sft",
33
+ service_center_address: str = "http://vps.shiweinan.com:37919/service_center",
34
+ k8s_namespace: str = "secondbrain",
35
+ registry_address: str = "crpi-cev6qq28wwgwwj0y.cn-beijing.personal.cr.aliyuncs.com/nexthci",
36
+
37
+ inject_http_port: int = 8000,
38
+
39
+ inject_kafka_host: str = "localhost",
40
+ inject_kafka_port: int = 9092,
41
+
42
+ inject_postgres_host: str = "second-brain-postgres-postgresql",
43
+ inject_postgres_port: int = 5432,
44
+ inject_postgres_user: str = "postgres",
45
+ inject_postgres_password: str = "gnBGWg7aL4",
46
+
47
+ inject_mongo_host: str = "mongo-mongodb",
48
+ inject_mongo_port: int = 27017,
49
+ inject_mongo_user: str = "secondbrain",
50
+ inject_mongo_password: str = "secondbrain",
51
+ inject_mongo_db: str = "secondbrain",
52
+
53
+ inject_redis_host: str = "redis-master",
54
+ inject_redis_port: int = 6379,
55
+ inject_redis_password: str = "rDdM2Y2gX9",
56
+
57
+ inject_feedback_api_url: str = "http://vps.shiweinan.com:37919/api/v1/feedback",
58
+ inject_feedback_api_timeout: int = 5,
59
+
60
+ deepseek_api_key: str = "82c9df22-f6ed-411e-90d7-c5255376b7ca",
61
+ deepseek_base_url: str = "https://ark.cn-beijing.volces.com/api/v3",
62
+ ):
63
+ self.sft_file_root = sft_file_root
64
+ self.service_center_address = service_center_address
65
+ self.k8s_namespace = k8s_namespace
66
+ self.registry_address = registry_address
67
+
68
+ self.inject_http_port = inject_http_port
69
+
70
+ self.inject_kafka_host = inject_kafka_host
71
+ self.inject_kafka_port = inject_kafka_port
72
+
73
+ self.inject_postgres_host = inject_postgres_host
74
+ self.inject_postgres_port = inject_postgres_port
75
+ self.inject_postgres_user = inject_postgres_user
76
+ self.inject_postgres_password = inject_postgres_password
77
+
78
+ self.inject_mongo_host = inject_mongo_host
79
+ self.inject_mongo_port = inject_mongo_port
80
+ self.inject_mongo_user = inject_mongo_user
81
+ self.inject_mongo_password = inject_mongo_password
82
+ self.inject_mongo_db = inject_mongo_db
83
+
84
+ self.inject_redis_host = inject_redis_host
85
+ self.inject_redis_port = inject_redis_port
86
+ self.inject_redis_password = inject_redis_password
87
+
88
+ self.inject_feedback_api_url = inject_feedback_api_url
89
+ self.inject_feedback_api_timeout = inject_feedback_api_timeout
90
+
91
+ self.deepseek_api_key = deepseek_api_key
92
+ self.deepseek_base_url = deepseek_base_url
93
+
94
+ @property
95
+ def server_url(self) -> str:
96
+ return self.service_center_address
97
+
98
+ @property
99
+ def upload_timeout(self) -> int:
100
+ return 300 # 5 minutes default timeout
101
+
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
119
+
120
+ @property
121
+ def config_file_path(self) -> Path:
122
+ return self.CONFIG_ROOT / "config.yaml"
123
+
124
+ def ensure_config_dir(self) -> None:
125
+ self.CONFIG_ROOT.mkdir(parents=True, exist_ok=True)
126
+
127
+ def to_dict(self) -> dict:
128
+ config_keys = self.get_config_keys()
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
137
+
138
+ def from_dict(self, data: dict) -> None:
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:
145
+ if key in data:
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)
153
+
154
+ def save(self) -> None:
155
+ self.ensure_config_dir()
156
+ config_dict = self.to_dict()
157
+ OmegaConf.save(config_dict, self.config_file_path)
158
+
159
+ def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
160
+ return getattr(self, key, default)
161
+
162
+ def set(self, key: str, value: str, description: Optional[str] = None) -> None:
163
+ if key in ["config_root"]:
164
+ raise ValueError(f"{key} is read-only")
165
+ if hasattr(self, key):
166
+ setattr(self, key, value)
167
+ if description:
168
+ self.CONFIG_DESCRIPTIONS[key] = description
169
+ else:
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
178
+
179
+ def update(self, updates: dict) -> None:
180
+ for key, value in updates.items():
181
+ self.set(key, value)
182
+
183
+
184
+ def load_config() -> SftConfig:
185
+ # config = SftConfig()
186
+ config = SftConfig()
187
+ config_file = config.config_file_path
188
+
189
+ if config_file.exists():
190
+ try:
191
+ data = OmegaConf.load(config_file)
192
+ config = SftConfig(**OmegaConf.to_container(data, resolve=True))
193
+ except Exception as e:
194
+ ...
195
+
196
+ config.save()
197
+
198
+ return config
199
+
200
+ sft_config = load_config()
File without changes