Spaces:
Building
Building
Update config_provider.py
Browse files- config_provider.py +132 -84
config_provider.py
CHANGED
@@ -1,166 +1,214 @@
|
|
1 |
"""
|
2 |
-
Flare –
|
3 |
-
|
4 |
-
•
|
5 |
-
•
|
|
|
|
|
6 |
"""
|
7 |
|
8 |
from __future__ import annotations
|
9 |
|
10 |
import json
|
11 |
from pathlib import Path
|
12 |
-
from typing import Dict, List, Optional
|
13 |
|
14 |
from pydantic import BaseModel, Field, HttpUrl, ValidationError
|
15 |
from utils import log
|
16 |
|
17 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
class RetryConfig(BaseModel):
|
19 |
-
|
20 |
-
retry_count: int = 3
|
21 |
backoff_seconds: int = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
|
|
|
|
23 |
|
|
|
|
|
24 |
class ParameterConfig(BaseModel):
|
25 |
name: str
|
26 |
-
|
|
|
27 |
required: bool = True
|
|
|
|
|
28 |
invalid_prompt: Optional[str] = None
|
|
|
|
|
|
|
|
|
29 |
|
30 |
|
31 |
class IntentConfig(BaseModel):
|
32 |
name: str
|
33 |
-
|
|
|
|
|
|
|
|
|
34 |
parameters: List[ParameterConfig] = []
|
|
|
|
|
35 |
fallback_error_prompt: Optional[str] = None
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
class VersionConfig(BaseModel):
|
39 |
-
id: int
|
40 |
-
caption: str
|
|
|
41 |
general_prompt: str
|
42 |
-
|
43 |
-
intents: List[IntentConfig]
|
|
|
|
|
|
|
44 |
|
45 |
|
46 |
class ProjectConfig(BaseModel):
|
|
|
47 |
name: str
|
48 |
-
caption: str
|
|
|
|
|
49 |
versions: List[VersionConfig]
|
50 |
|
|
|
|
|
51 |
|
52 |
-
class APIAuthConfig(BaseModel):
|
53 |
-
token_endpoint: HttpUrl
|
54 |
-
refresh_endpoint: Optional[HttpUrl] = None
|
55 |
-
client_id: str
|
56 |
-
client_secret: str
|
57 |
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
-
|
60 |
-
|
61 |
-
url: HttpUrl
|
62 |
-
method: str = Field("GET", pattern=r"^(GET|POST|PUT|PATCH|DELETE)$")
|
63 |
-
timeout_seconds: int = 10
|
64 |
-
proxy: Optional[str] = None
|
65 |
-
auth: Optional[APIAuthConfig] = None
|
66 |
-
response_prompt: Optional[str] = None
|
67 |
-
retry: Optional[RetryConfig] = None
|
68 |
|
|
|
|
|
69 |
|
70 |
-
|
71 |
-
|
72 |
-
apis: Dict[str, APIConfig]
|
73 |
-
locale: str = "tr-TR"
|
74 |
-
retry: RetryConfig = RetryConfig()
|
75 |
|
76 |
|
77 |
-
#
|
78 |
class ConfigProvider:
|
79 |
-
|
80 |
_CONFIG_PATH = Path(__file__).parent / "service_config.jsonc"
|
81 |
|
82 |
@classmethod
|
83 |
def get(cls) -> ServiceConfig:
|
84 |
-
if cls.
|
85 |
-
cls.
|
86 |
-
|
|
|
87 |
|
88 |
-
#
|
89 |
@classmethod
|
90 |
-
def
|
91 |
log(f"📥 Loading service config from {cls._CONFIG_PATH.name} …")
|
92 |
raw = cls._CONFIG_PATH.read_text(encoding="utf-8")
|
93 |
json_str = cls._strip_jsonc(raw)
|
94 |
try:
|
95 |
data = json.loads(json_str)
|
96 |
cfg = ServiceConfig.model_validate(data)
|
97 |
-
log("✅ Service config loaded
|
98 |
return cfg
|
99 |
except (json.JSONDecodeError, ValidationError) as exc:
|
100 |
log(f"❌ Config validation error: {exc}")
|
101 |
raise
|
102 |
|
103 |
-
# -------- robust JSONC stripper ----------
|
104 |
@staticmethod
|
105 |
def _strip_jsonc(text: str) -> str:
|
106 |
-
"""Remove //
|
107 |
-
OUT,
|
108 |
-
state = OUT
|
109 |
-
res = []
|
110 |
-
|
111 |
-
i = 0
|
112 |
while i < len(text):
|
113 |
ch = text[i]
|
114 |
-
|
115 |
if state == OUT:
|
116 |
if ch == '"':
|
117 |
-
state =
|
118 |
-
res.append(ch)
|
119 |
elif ch == '/':
|
120 |
-
# Could be // or /* – peek next char
|
121 |
nxt = text[i + 1] if i + 1 < len(text) else ""
|
122 |
if nxt == '/':
|
123 |
-
state =
|
124 |
-
i += 1 # skip nxt in loop
|
125 |
elif nxt == '*':
|
126 |
-
state =
|
127 |
-
i += 1
|
128 |
else:
|
129 |
res.append(ch)
|
130 |
else:
|
131 |
res.append(ch)
|
132 |
-
|
133 |
-
elif state == IN_STR:
|
134 |
res.append(ch)
|
135 |
-
if ch == '\\'
|
136 |
-
state = ESC # escape next
|
137 |
-
elif ch == '"':
|
138 |
-
state = OUT
|
139 |
-
|
140 |
elif state == ESC:
|
141 |
res.append(ch)
|
142 |
-
state =
|
143 |
-
|
144 |
-
elif state == IN_SLASH:
|
145 |
if ch == '\n':
|
146 |
-
res.append(ch)
|
147 |
state = OUT
|
148 |
-
|
149 |
-
elif state == IN_BLOCK:
|
150 |
if ch == '*' and i + 1 < len(text) and text[i + 1] == '/':
|
151 |
-
i
|
152 |
-
state = OUT
|
153 |
-
|
154 |
i += 1
|
155 |
-
|
156 |
return ''.join(res)
|
157 |
-
|
158 |
-
|
159 |
-
# --------------- exports ---------------
|
160 |
-
RetryConfig = RetryConfig
|
161 |
-
ParameterConfig = ParameterConfig
|
162 |
-
IntentConfig = IntentConfig
|
163 |
-
VersionConfig = VersionConfig
|
164 |
-
APIConfig = APIConfig
|
165 |
-
ProjectConfig = ProjectConfig
|
166 |
-
ServiceConfig = ServiceConfig
|
|
|
1 |
"""
|
2 |
+
Flare – Configuration Loader (JSONC + Pydantic v2)
|
3 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
4 |
+
• Global, Project, Version, Intent, API tanımları
|
5 |
+
• JSONC yorum temizleme (string literal’lere dokunmaz)
|
6 |
+
• Alias’lar (version_number, max_attempts vb.)
|
7 |
+
• apis[] artık liste → runtime’da dict’e map’lenir
|
8 |
"""
|
9 |
|
10 |
from __future__ import annotations
|
11 |
|
12 |
import json
|
13 |
from pathlib import Path
|
14 |
+
from typing import Any, Dict, List, Optional
|
15 |
|
16 |
from pydantic import BaseModel, Field, HttpUrl, ValidationError
|
17 |
from utils import log
|
18 |
|
19 |
+
# ---------------- Global -----------------
|
20 |
+
class UserConfig(BaseModel):
|
21 |
+
username: str
|
22 |
+
password_hash: str
|
23 |
+
salt: str
|
24 |
+
|
25 |
+
|
26 |
+
class GlobalConfig(BaseModel):
|
27 |
+
work_mode: str = Field("hfcloud", pattern=r"^(hfcloud|cloud|on-premise)$")
|
28 |
+
cloud_token: Optional[str] = None
|
29 |
+
spark_endpoint: HttpUrl
|
30 |
+
users: List[UserConfig] = []
|
31 |
+
|
32 |
+
|
33 |
+
# ---------------- Retry / Proxy ----------
|
34 |
class RetryConfig(BaseModel):
|
35 |
+
retry_count: int = Field(3, alias="max_attempts")
|
|
|
36 |
backoff_seconds: int = 2
|
37 |
+
strategy: str = Field("static", pattern=r"^(static|exponential)$")
|
38 |
+
|
39 |
+
|
40 |
+
class ProxyConfig(BaseModel):
|
41 |
+
enabled: bool = True
|
42 |
+
url: HttpUrl
|
43 |
+
|
44 |
+
|
45 |
+
# ---------------- API & Auth -------------
|
46 |
+
class APIAuthConfig(BaseModel):
|
47 |
+
enabled: bool = False
|
48 |
+
token_endpoint: Optional[HttpUrl] = None
|
49 |
+
response_token_path: str = "access_token"
|
50 |
+
token_request_body: Dict[str, Any] = Field({}, alias="body_template")
|
51 |
+
token_refresh_endpoint: Optional[HttpUrl] = None
|
52 |
+
token_refresh_body: Dict[str, Any] = {}
|
53 |
+
|
54 |
+
class Config:
|
55 |
+
extra = "allow"
|
56 |
+
|
57 |
+
|
58 |
+
class APIConfig(BaseModel):
|
59 |
+
name: str
|
60 |
+
url: HttpUrl
|
61 |
+
method: str = Field("GET", pattern=r"^(GET|POST|PUT|PATCH|DELETE)$")
|
62 |
+
headers: Dict[str, Any] = {}
|
63 |
+
body_template: Dict[str, Any] = {}
|
64 |
+
timeout_seconds: int = 10
|
65 |
+
retry: RetryConfig = RetryConfig()
|
66 |
+
proxy: Optional[str | ProxyConfig] = None
|
67 |
+
auth: Optional[APIAuthConfig] = None
|
68 |
+
response_prompt: Optional[str] = None
|
69 |
|
70 |
+
class Config:
|
71 |
+
extra = "allow"
|
72 |
|
73 |
+
|
74 |
+
# ---------------- Intent / Param ---------
|
75 |
class ParameterConfig(BaseModel):
|
76 |
name: str
|
77 |
+
caption: Optional[str] = ""
|
78 |
+
type: str = Field(..., pattern=r"^(int|float|bool|str|string)$")
|
79 |
required: bool = True
|
80 |
+
extraction_prompt: Optional[str] = None
|
81 |
+
validation_regex: Optional[str] = None
|
82 |
invalid_prompt: Optional[str] = None
|
83 |
+
type_error_prompt: Optional[str] = None
|
84 |
+
|
85 |
+
def canonical_type(self) -> str:
|
86 |
+
return "str" if self.type == "string" else self.type
|
87 |
|
88 |
|
89 |
class IntentConfig(BaseModel):
|
90 |
name: str
|
91 |
+
caption: Optional[str] = ""
|
92 |
+
locale: str = "tr-TR"
|
93 |
+
dependencies: List[str] = []
|
94 |
+
examples: List[str] = []
|
95 |
+
detection_prompt: Optional[str] = None
|
96 |
parameters: List[ParameterConfig] = []
|
97 |
+
action: str
|
98 |
+
fallback_timeout_prompt: Optional[str] = None
|
99 |
fallback_error_prompt: Optional[str] = None
|
100 |
|
101 |
+
class Config:
|
102 |
+
extra = "allow"
|
103 |
+
|
104 |
+
|
105 |
+
# ---------------- Version / Project ------
|
106 |
+
class LLMConfig(BaseModel):
|
107 |
+
repo_id: str
|
108 |
+
generation_config: Dict[str, Any] = {}
|
109 |
+
use_fine_tune: bool = False
|
110 |
+
fine_tune_zip: str = ""
|
111 |
+
|
112 |
|
113 |
class VersionConfig(BaseModel):
|
114 |
+
id: int = Field(..., alias="version_number")
|
115 |
+
caption: Optional[str] = ""
|
116 |
+
published: bool = False
|
117 |
general_prompt: str
|
118 |
+
llm: LLMConfig
|
119 |
+
intents: List[IntentConfig]
|
120 |
+
|
121 |
+
class Config:
|
122 |
+
extra = "allow"
|
123 |
|
124 |
|
125 |
class ProjectConfig(BaseModel):
|
126 |
+
id: Optional[int] = None
|
127 |
name: str
|
128 |
+
caption: Optional[str] = ""
|
129 |
+
enabled: bool = True
|
130 |
+
last_version_number: Optional[int] = None
|
131 |
versions: List[VersionConfig]
|
132 |
|
133 |
+
class Config:
|
134 |
+
extra = "allow"
|
135 |
|
|
|
|
|
|
|
|
|
|
|
136 |
|
137 |
+
# ---------------- Service Config ---------
|
138 |
+
class ServiceConfig(BaseModel):
|
139 |
+
global_config: GlobalConfig = Field(..., alias="config")
|
140 |
+
projects: List[ProjectConfig]
|
141 |
+
apis: List[APIConfig]
|
142 |
|
143 |
+
# runtime helpers (skip validation)
|
144 |
+
_api_by_name: Dict[str, APIConfig] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
|
146 |
+
def build_index(self):
|
147 |
+
self._api_by_name = {a.name: a for a in self.apis}
|
148 |
|
149 |
+
def get_api(self, name: str) -> APIConfig | None:
|
150 |
+
return self._api_by_name.get(name)
|
|
|
|
|
|
|
151 |
|
152 |
|
153 |
+
# ---------------- Provider Singleton -----
|
154 |
class ConfigProvider:
|
155 |
+
_instance: Optional[ServiceConfig] = None
|
156 |
_CONFIG_PATH = Path(__file__).parent / "service_config.jsonc"
|
157 |
|
158 |
@classmethod
|
159 |
def get(cls) -> ServiceConfig:
|
160 |
+
if cls._instance is None:
|
161 |
+
cls._instance = cls._load()
|
162 |
+
cls._instance.build_index()
|
163 |
+
return cls._instance
|
164 |
|
165 |
+
# -------- Internal helpers ------------
|
166 |
@classmethod
|
167 |
+
def _load(cls) -> ServiceConfig:
|
168 |
log(f"📥 Loading service config from {cls._CONFIG_PATH.name} …")
|
169 |
raw = cls._CONFIG_PATH.read_text(encoding="utf-8")
|
170 |
json_str = cls._strip_jsonc(raw)
|
171 |
try:
|
172 |
data = json.loads(json_str)
|
173 |
cfg = ServiceConfig.model_validate(data)
|
174 |
+
log("✅ Service config loaded.")
|
175 |
return cfg
|
176 |
except (json.JSONDecodeError, ValidationError) as exc:
|
177 |
log(f"❌ Config validation error: {exc}")
|
178 |
raise
|
179 |
|
|
|
180 |
@staticmethod
|
181 |
def _strip_jsonc(text: str) -> str:
|
182 |
+
"""Remove // and /* */ comments (string-aware)."""
|
183 |
+
OUT, STR, ESC, SLASH, BLOCK = 0, 1, 2, 3, 4
|
184 |
+
state, res, i = OUT, [], 0
|
|
|
|
|
|
|
185 |
while i < len(text):
|
186 |
ch = text[i]
|
|
|
187 |
if state == OUT:
|
188 |
if ch == '"':
|
189 |
+
state, res = STR, res + [ch]
|
|
|
190 |
elif ch == '/':
|
|
|
191 |
nxt = text[i + 1] if i + 1 < len(text) else ""
|
192 |
if nxt == '/':
|
193 |
+
state, i = SLASH, i + 1
|
|
|
194 |
elif nxt == '*':
|
195 |
+
state, i = BLOCK, i + 1
|
|
|
196 |
else:
|
197 |
res.append(ch)
|
198 |
else:
|
199 |
res.append(ch)
|
200 |
+
elif state == STR:
|
|
|
201 |
res.append(ch)
|
202 |
+
state = ESC if ch == '\\' else (OUT if ch == '"' else STR)
|
|
|
|
|
|
|
|
|
203 |
elif state == ESC:
|
204 |
res.append(ch)
|
205 |
+
state = STR
|
206 |
+
elif state == SLASH:
|
|
|
207 |
if ch == '\n':
|
208 |
+
res.append(ch)
|
209 |
state = OUT
|
210 |
+
elif state == BLOCK:
|
|
|
211 |
if ch == '*' and i + 1 < len(text) and text[i + 1] == '/':
|
212 |
+
i, state = i + 1, OUT
|
|
|
|
|
213 |
i += 1
|
|
|
214 |
return ''.join(res)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|