johnbridges commited on
Commit
db550a4
·
1 Parent(s): a3a1b05
Files changed (2) hide show
  1. appsettings.json +58 -0
  2. config.py +224 -27
appsettings.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Debug",
5
+ "Microsoft": "Debug",
6
+ "Microsoft.Hosting.Lifetime": "Debug"
7
+ }
8
+ },
9
+ "OpenAIApiKey": ".env",
10
+ "EmailEncryptKey": ".env",
11
+ "LocalSystemUrl": {
12
+ "ExternalUrl": "https://asmonitorsrv.readyforquantum.com",
13
+ "IPAddress": "",
14
+ "RabbitHostName": "devrabbitmq.readyforquantum.com",
15
+ "RabbitPort": 63785,
16
+ "RabbitInstanceName": "ASSrv-LLMService",
17
+ "RabbitUserName": "usercommonxf1",
18
+ "RabbitPassword": ".env",
19
+ "RabbitVHost": "/vhostuser",
20
+ "UseTls": true
21
+ },
22
+ "RedisUrl": "redis.readyforquantum.com:46379",
23
+ "RedisSecret": ".env",
24
+ "ServiceID": "monitor",
25
+ "StartThisTestLLM": true,
26
+ "LlmNoInitMessage": false,
27
+ "ServiceAuthKey": ".env",
28
+ "LlmModelPath": "/home/mahadeva/code/models/",
29
+ "LlmModelFileName": "xLAM-2-3b-fc-r-q8_0.gguf",
30
+ "LlmContextFileName": "context-xlam-2.gguf",
31
+ "LlmSystemPrompt": "system_prompt_xlam_2_run",
32
+ "LlmPromptMode": " -if -sp -no-cnv --simple-io ",
33
+ "LlmVersion": "xlam_2",
34
+ "LlmCtxSize": 12000,
35
+ "LlmOpenAICtxSize": 32000,
36
+ "LlmCtxRatio": 6,
37
+ "LlmTemp": "0.3",
38
+ "LlmThreads": 4,
39
+ "LlmSystemPromptTimeout": 120,
40
+ "LlmUserPromptTimeout": 1200,
41
+ "LlmSessionIdleTimeout": 1440,
42
+ "LlmGptModel": "gpt-4.1-mini",
43
+ "LlmHFModelID": "qwen/qwen3-4b-fp8",
44
+ "LlmHFKey": ".env",
45
+ "LlmHFUrl": "https://api.novita.ai/v3/openai/chat/completions",
46
+ "LlmHFModelVersion": "qwen_3",
47
+ "LlmUseHF": true,
48
+ "LlmNoThink" : true,
49
+ "AudioServiceUrl": "https://devtranscribe.readyforquantum.com",
50
+ "AudioServiceOutputDir": "/home/audioservice/app/files",
51
+ "IsStream": false,
52
+ "REDIS_PASSWORD" :".env",
53
+ "RabbitPassword": ".env",
54
+ "RabbitRoutingKey": "execute.api",
55
+ "RabbitExhangeType": "direct",
56
+ "UseTls" : true,
57
+ "EnableAgentFlow" : true
58
+ }
config.py CHANGED
@@ -1,55 +1,252 @@
1
  # app/config.py
 
2
  import os
3
  from functools import lru_cache
 
4
 
5
- # v2-first import, fallback to v1
6
  try:
7
- from pydantic_settings import BaseSettings, SettingsConfigDict # pydantic v2
8
- from pydantic import AnyUrl, Field
9
  IS_V2 = True
10
- except Exception:
11
- from pydantic import BaseSettings, AnyUrl # pydantic v1
12
- from pydantic import Field
 
13
  IS_V2 = False
14
 
15
- class Settings(BaseSettings):
16
- # Read from env: set this in your Space Secrets as AMQP_URL
17
- AMQP_URL: AnyUrl = Field(..., description="amqps://user:pass@host:5671/%2F?heartbeat=30")
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  RABBIT_INSTANCE_NAME: str = "prod"
20
  RABBIT_EXCHANGE_TYPE: str = "topic"
21
  RABBIT_ROUTING_KEY: str = ""
22
  RABBIT_PREFETCH: int = 1
23
 
 
 
 
 
 
24
  SERVICE_ID: str = "gradllm"
25
  USE_TLS: bool = True
26
 
27
- # Optional: prefix->type map, e.g. {"llmStartSession": "topic"}
28
- EXCHANGE_TYPES: dict[str, str] = {}
29
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  if IS_V2:
31
- # pydantic v2
32
- model_config = SettingsConfigDict(
33
- case_sensitive=True,
34
- env_file=".env", # for local dev; ignored on HF unless you commit it
35
- env_file_encoding="utf-8",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  else:
38
- # pydantic v1
39
  class Config:
40
  case_sensitive = True
41
- env_file = ".env"
42
- env_file_encoding = "utf-8"
43
 
44
  @lru_cache
45
  def get_settings() -> Settings:
46
- try:
47
- return Settings()
48
- except Exception as e:
49
- # Friendlier message when AMQP_URL is missing/invalid
50
- raise RuntimeError(
51
- "AMQP_URL is not set or invalid. In your Hugging Face Space, add a Secret "
52
- "named AMQP_URL (e.g. amqps://user:pass@host:5671/%2F?heartbeat=30)."
53
- ) from e
 
 
54
 
55
  settings = get_settings()
 
1
  # app/config.py
2
+ import json
3
  import os
4
  from functools import lru_cache
5
+ from urllib.parse import quote
6
 
7
+ # pydantic v2-first import, soft-fallback to v1
8
  try:
9
+ from pydantic_settings import BaseSettings, SettingsConfigDict # v2
10
+ from pydantic import BaseModel, Field
11
  IS_V2 = True
12
+ except Exception: # v1 fallback
13
+ from pydantic import BaseSettings as _BaseSettings, BaseModel, Field
14
+ class BaseSettings(_BaseSettings): # shim name
15
+ pass
16
  IS_V2 = False
17
 
 
 
 
18
 
19
+ APPSETTINGS_PATH = os.environ.get("APPSETTINGS_JSON", "appsettings.json")
20
+
21
+ def _load_json(path: str):
22
+ if not path or not os.path.exists(path):
23
+ return {}
24
+ try:
25
+ with open(path, "r", encoding="utf-8") as f:
26
+ return json.load(f)
27
+ except Exception:
28
+ return {}
29
+
30
+ def _env_or(raw_val: str | None, env_name: str | None):
31
+ """
32
+ If raw_val == ".env" -> fetch from env_name (or the obvious uppercase key).
33
+ Otherwise return raw_val.
34
+ """
35
+ if raw_val != ".env":
36
+ return raw_val
37
+ key = env_name or ""
38
+ # If an explicit env name isn't passed, try a sensible default
39
+ if not key:
40
+ return None
41
+ return os.environ.get(key)
42
+
43
+ def _bool(v, default=False):
44
+ if v is None:
45
+ return default
46
+ if isinstance(v, bool):
47
+ return v
48
+ s = str(v).strip().lower()
49
+ if s in {"1", "true", "yes", "y", "on"}:
50
+ return True
51
+ if s in {"0", "false", "no", "n", "off"}:
52
+ return False
53
+ return default
54
+
55
+
56
+ class LocalSystemUrl(BaseModel):
57
+ ExternalUrl: str | None = None
58
+ IPAddress: str | None = None
59
+ RabbitHostName: str | None = None
60
+ RabbitPort: int | None = None
61
+ RabbitInstanceName: str | None = None
62
+ RabbitUserName: str | None = None
63
+ RabbitPassword: str | None = None # might be ".env"
64
+ RabbitVHost: str | None = None
65
+ UseTls: bool | None = None
66
+
67
+
68
+ class _SettingsModel(BaseModel):
69
+ # Core (env can still override each)
70
+ AMQP_URL: str | None = None
71
+
72
+ # Rabbit defaults (match what you already had)
73
  RABBIT_INSTANCE_NAME: str = "prod"
74
  RABBIT_EXCHANGE_TYPE: str = "topic"
75
  RABBIT_ROUTING_KEY: str = ""
76
  RABBIT_PREFETCH: int = 1
77
 
78
+ # TLS controls (default to strict=False only because your broker uses custom certs)
79
+ RABBIT_TLS_VERIFY: bool = False
80
+ RABBIT_TLS_CHECK_HOSTNAME: bool = False
81
+ RABBIT_TLS_CA_FILE: str | None = None
82
+
83
  SERVICE_ID: str = "gradllm"
84
  USE_TLS: bool = True
85
 
86
+ EXCHANGE_TYPES: dict[str, str] = Field(default_factory=dict)
 
87
 
88
+ # appsettings.json fields that we may use to assemble AMQP_URL
89
+ LocalSystemUrl: LocalSystemUrl | None = None
90
+ ServiceID: str | None = None
91
+ RabbitRoutingKey: str | None = None
92
+ RabbitExhangeType: str | None = None # (note the spelling in your JSON)
93
+ UseTls: bool | None = None # top-level dup in your JSON
94
+
95
+ # misc passthroughs if you want them later
96
+ RedisUrl: str | None = None
97
+ RedisSecret: str | None = None
98
+
99
+ # pydantic config
100
  if IS_V2:
101
+ model_config = SettingsConfigDict(extra="ignore")
102
+ else:
103
+ class Config:
104
+ extra = "ignore"
105
+
106
+
107
+ def _build_amqp_url_from_local(local: LocalSystemUrl, top_level_use_tls: bool | None):
108
+ """
109
+ Build amqp(s)://user:pass@host:port/vhost?heartbeat=30
110
+ Uses LocalSystemUrl + `.env` indirections.
111
+ """
112
+ if not local or not local.RabbitHostName or not local.RabbitUserName:
113
+ return None
114
+
115
+ # determine TLS
116
+ use_tls = local.UseTls
117
+ if use_tls is None:
118
+ use_tls = top_level_use_tls
119
+ if use_tls is None:
120
+ use_tls = True # default secure
121
+
122
+ scheme = "amqps" if use_tls else "amqp"
123
+
124
+ # password indirection
125
+ # If RabbitPassword is ".env", read RABBIT_PASSWORD (conventional)
126
+ raw_pwd = local.RabbitPassword
127
+ pwd = raw_pwd if raw_pwd and raw_pwd != ".env" else os.environ.get("RABBIT_PASSWORD")
128
+ # fall back to standard names, just in case
129
+ if not pwd:
130
+ pwd = os.environ.get("RabbitPassword") or os.environ.get("RABBIT_PASS")
131
+
132
+ user = local.RabbitUserName
133
+ host = local.RabbitHostName
134
+ port = local.RabbitPort or (5671 if scheme == "amqps" else 5672)
135
+ vhost = local.RabbitVHost or "/"
136
+ vhost_enc = quote(vhost, safe="") # encode e.g. "/vhostuser" -> "%2Fvhostuser"
137
+
138
+ return f"{scheme}://{user}:{pwd}@{host}:{port}/{vhost_enc}?heartbeat=30"
139
+
140
+
141
+ def _merge_env_over_json(j: dict) -> _SettingsModel:
142
+ """
143
+ Precedence:
144
+ 1) Environment variables (HF Secrets)
145
+ 2) appsettings.json values
146
+ 3) built defaults
147
+ We also synthesize AMQP_URL from LocalSystemUrl if not set via env.
148
+ """
149
+ # Start with JSON
150
+ model = _SettingsModel(**j)
151
+
152
+ # Map top-level JSON keys to our fields when used
153
+ if model.ServiceID:
154
+ model.SERVICE_ID = model.ServiceID
155
+ if model.RabbitRoutingKey:
156
+ model.RABBIT_ROUTING_KEY = model.RabbitRoutingKey
157
+ if model.RabbitExhangeType:
158
+ model.RABBIT_EXCHANGE_TYPE = model.RabbitExhangeType
159
+ if model.UseTls is not None:
160
+ model.USE_TLS = _bool(model.UseTls, model.USE_TLS)
161
+
162
+ # If AMQP_URL not set, try to build from LocalSystemUrl
163
+ if not model.AMQP_URL and model.LocalSystemUrl:
164
+ built = _build_amqp_url_from_local(model.LocalSystemUrl, model.UseTls)
165
+ if built:
166
+ model.AMQP_URL = built
167
+
168
+ # Now overlay environment variables (HF Secrets)
169
+ env = os.environ
170
+
171
+ # Direct env override of AMQP_URL wins
172
+ model.AMQP_URL = env.get("AMQP_URL", model.AMQP_URL)
173
+
174
+ # Other rabbit knobs
175
+ model.RABBIT_INSTANCE_NAME = env.get("RABBIT_INSTANCE_NAME", model.RABBIT_INSTANCE_NAME)
176
+ model.RABBIT_EXCHANGE_TYPE = env.get("RABBIT_EXCHANGE_TYPE", model.RABBIT_EXCHANGE_TYPE)
177
+ model.RABBIT_ROUTING_KEY = env.get("RABBIT_ROUTING_KEY", model.RABBIT_ROUTING_KEY)
178
+ model.RABBIT_PREFETCH = int(env.get("RABBIT_PREFETCH", model.RABBIT_PREFETCH))
179
+
180
+ # TLS env overrides
181
+ if "RABBIT_TLS_VERIFY" in env:
182
+ model.RABBIT_TLS_VERIFY = _bool(env["RABBIT_TLS_VERIFY"], model.RABBIT_TLS_VERIFY)
183
+ if "RABBIT_TLS_CHECK_HOSTNAME" in env:
184
+ model.RABBIT_TLS_CHECK_HOSTNAME = _bool(env["RABBIT_TLS_CHECK_HOSTNAME"], model.RABBIT_TLS_CHECK_HOSTNAME)
185
+ model.RABBIT_TLS_CA_FILE = env.get("RABBIT_TLS_CA_FILE", model.RABBIT_TLS_CA_FILE)
186
+
187
+ # SERVICE_ID can be overridden (you renamed yours to gradllm)
188
+ model.SERVICE_ID = env.get("SERVICE_ID", model.SERVICE_ID)
189
+
190
+ # Optional EXCHANGE_TYPES as JSON string in env
191
+ et = env.get("EXCHANGE_TYPES")
192
+ if et:
193
+ try:
194
+ model.EXCHANGE_TYPES = json.loads(et)
195
+ except Exception:
196
+ pass
197
+
198
+ # Final sanity: must have AMQP_URL
199
+ if not model.AMQP_URL:
200
+ raise RuntimeError(
201
+ "AMQP_URL is not configured. Set it via:\n"
202
+ "- env secret AMQP_URL, or\n"
203
+ "- appsettings.json LocalSystemUrl (RabbitHostName/UserName/Password/VHost/UseTls)."
204
  )
205
+
206
+ return model
207
+
208
+
209
+ class Settings(BaseSettings):
210
+ """
211
+ Thin wrapper that exposes the merged model as attributes.
212
+ """
213
+ # required
214
+ AMQP_URL: str
215
+
216
+ # rabbit
217
+ RABBIT_INSTANCE_NAME: str = "prod"
218
+ RABBIT_EXCHANGE_TYPE: str = "topic"
219
+ RABBIT_ROUTING_KEY: str = ""
220
+ RABBIT_PREFETCH: int = 1
221
+
222
+ # TLS
223
+ RABBIT_TLS_VERIFY: bool = False
224
+ RABBIT_TLS_CHECK_HOSTNAME: bool = False
225
+ RABBIT_TLS_CA_FILE: str | None = None
226
+
227
+ SERVICE_ID: str = "gradllm"
228
+ USE_TLS: bool = True
229
+
230
+ EXCHANGE_TYPES: dict[str, str] = Field(default_factory=dict)
231
+
232
+ if IS_V2:
233
+ model_config = SettingsConfigDict(case_sensitive=True)
234
  else:
 
235
  class Config:
236
  case_sensitive = True
237
+
 
238
 
239
  @lru_cache
240
  def get_settings() -> Settings:
241
+ # 1) load json
242
+ j = _load_json(APPSETTINGS_PATH)
243
+
244
+ # 2) merge with env precedence + synthesize AMQP_URL if needed
245
+ merged = _merge_env_over_json(j)
246
+
247
+ # 3) project into the public Settings class
248
+ data = merged.model_dump() if hasattr(merged, "model_dump") else merged.dict()
249
+ return Settings(**data)
250
+
251
 
252
  settings = get_settings()