nikeshn commited on
Commit
caea6f0
·
verified ·
1 Parent(s): 27a578e

Upload 2 files

Browse files
Files changed (1) hide show
  1. src/config.py +143 -117
src/config.py CHANGED
@@ -1,127 +1,153 @@
1
- """Environment-backed configuration for LibBee."""
2
-
3
- from functools import lru_cache
4
- from pathlib import Path
5
- from typing import Literal
6
-
7
- from pydantic import field_validator
8
- from pydantic_settings import BaseSettings, SettingsConfigDict
9
-
10
-
11
- LIBBEE_VERSION = "4.1.0"
12
-
13
-
14
- class Settings(BaseSettings):
15
- model_config = SettingsConfigDict(case_sensitive=False, env_file=".env", env_file_encoding="utf-8", extra="ignore")
16
-
17
- openai_api_key: str = ""
18
- anthropic_api_key: str = ""
19
  primo_api_key: str = ""
20
- contact_email: str = "libse@ku.ac.ae"
21
-
22
- # The Worker sends this secret in X-LibBee-Origin-Token. Production
23
- # protected routes fail closed when the value is missing.
24
- origin_token: str = ""
25
- environment: Literal["development", "production", "test"] = "production"
26
- allowed_cors_origins: str = "http://localhost:8080,http://127.0.0.1:5500"
27
-
28
- admin_password: str = ""
29
- session_secret: str = "change-me"
30
- maintenance_mode: bool = False
31
- maintenance_message: str = "LibBee is receiving an update. Please try again shortly."
32
- max_results: int = 5
33
- max_question_chars: int = 2000
34
- max_history_messages: int = 12
35
- max_message_chars: int = 4000
 
36
  max_concurrent_agent_requests: int = 20
37
-
38
- # /data survives HF Space restarts when persistent storage is attached.
39
- runtime_dir: str = "/data/libbee_runtime"
40
- feedback_store_path: str = ""
41
- metrics_store_path: str = ""
42
- config_store_path: str = ""
43
- rag_index_dir: str = ""
44
- knowledge_dir: str = ""
45
-
46
- # The authenticated admin page receives only a short-lived signed token;
47
- # the permanent shared secret is never serialized into browser HTML.
48
- cloudflare_worker_url: str = ""
49
- cloudflare_admin_token: str = ""
50
-
51
- # HF talks to LibCal directly because some hosted environments cannot open
52
- # outbound connections to shared workers.dev hostnames.
53
- libcal_client_id: str = ""
54
- libcal_client_secret: str = ""
55
- libcal_events_client_id: str = ""
56
- libcal_events_client_secret: str = ""
57
-
58
- @field_validator("max_results")
59
- @classmethod
60
- def reasonable_max_results(cls, value: int) -> int:
61
- if not 1 <= value <= 20:
62
- raise ValueError("max_results must be between 1 and 20")
63
- return value
64
-
65
- @field_validator("max_question_chars", "max_message_chars")
66
- @classmethod
67
- def reasonable_text_limit(cls, value: int) -> int:
68
- if not 256 <= value <= 20_000:
69
- raise ValueError("text limits must be between 256 and 20,000")
70
- return value
71
-
72
- @field_validator("max_history_messages")
73
- @classmethod
74
- def reasonable_history_limit(cls, value: int) -> int:
75
- if not 0 <= value <= 50:
76
- raise ValueError("max_history_messages must be between 0 and 50")
 
 
 
 
 
 
 
 
 
 
 
77
  return value
78
 
79
- @field_validator("max_concurrent_agent_requests")
80
  @classmethod
81
- def reasonable_concurrency(cls, value: int) -> int:
82
- if not 1 <= value <= 100:
83
- raise ValueError("max_concurrent_agent_requests must be between 1 and 100")
84
  return value
85
 
86
  @property
87
- def cors_origins(self) -> list[str]:
88
- return [value.strip().rstrip("/") for value in self.allowed_cors_origins.split(",") if value.strip()]
89
-
90
- @property
 
 
 
 
 
 
 
 
 
 
 
 
91
  def runtime_path(self) -> Path:
92
  requested = Path(self.runtime_dir)
93
- if requested.parent.exists() and requested.parent.is_dir():
94
- return requested
95
- return Path("/tmp/libbee_runtime")
96
-
97
- @property
98
- def feedback_path(self) -> Path:
99
- return Path(self.feedback_store_path) if self.feedback_store_path else self.runtime_path / "feedback.jsonl"
100
-
101
- @property
102
- def metrics_path(self) -> Path:
103
- return Path(self.metrics_store_path) if self.metrics_store_path else self.runtime_path / "metrics.json"
104
-
105
- @property
106
- def config_path(self) -> Path:
107
- return Path(self.config_store_path) if self.config_store_path else self.runtime_path / "runtime_config.json"
108
-
109
- @property
110
- def rag_cache_dir(self) -> Path:
111
- return Path(self.rag_index_dir) if self.rag_index_dir else self.runtime_path / "rag_cache"
112
-
113
- @property
114
- def kb_dir(self) -> Path:
115
- if self.knowledge_dir:
116
- return Path(self.knowledge_dir)
117
- if Path("/app/data/knowledge").exists():
118
- return Path("/app/data/knowledge")
119
- return Path("data/knowledge")
120
-
121
-
122
- @lru_cache
123
- def get_settings() -> Settings:
124
- settings = Settings()
125
- settings.runtime_path.mkdir(parents=True, exist_ok=True)
126
- settings.rag_cache_dir.mkdir(parents=True, exist_ok=True)
127
- return settings
 
 
 
1
+ """Environment-backed configuration for LibBee."""
2
+
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ from pydantic import field_validator
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+
11
+ LIBBEE_VERSION = "4.1.1"
12
+
13
+
14
+ class Settings(BaseSettings):
15
+ model_config = SettingsConfigDict(case_sensitive=False, env_file=".env", env_file_encoding="utf-8", extra="ignore")
16
+
17
+ openai_api_key: str = ""
18
+ anthropic_api_key: str = ""
19
  primo_api_key: str = ""
20
+ openalex_api_key: str = ""
21
+ contact_email: str = "libse@ku.ac.ae"
22
+
23
+ # The Worker sends this secret in X-LibBee-Origin-Token. Production
24
+ # protected routes fail closed when the value is missing.
25
+ origin_token: str = ""
26
+ environment: Literal["development", "production", "test"] = "production"
27
+ allowed_cors_origins: str = "http://localhost:8080,http://127.0.0.1:5500"
28
+
29
+ admin_password: str = ""
30
+ session_secret: str = "change-me"
31
+ maintenance_mode: bool = False
32
+ maintenance_message: str = "LibBee is receiving an update. Please try again shortly."
33
+ max_results: int = 5
34
+ max_question_chars: int = 2000
35
+ max_history_messages: int = 12
36
+ max_message_chars: int = 4000
37
  max_concurrent_agent_requests: int = 20
38
+ # Includes the entire JSON envelope and UTF-8 history. The original 32 KiB
39
+ # ceiling rejected otherwise valid conversations under the model limits.
40
+ max_request_body_bytes: int = 524_288
41
+
42
+ # /data survives HF Space restarts when persistent storage is attached.
43
+ runtime_dir: str = "/data/libbee_runtime"
44
+ feedback_store_path: str = ""
45
+ metrics_store_path: str = ""
46
+ config_store_path: str = ""
47
+ rag_index_dir: str = ""
48
+ knowledge_dir: str = ""
49
+
50
+ # The authenticated admin page receives only a short-lived signed token;
51
+ # the permanent shared secret is never serialized into browser HTML.
52
+ cloudflare_worker_url: str = ""
53
+ cloudflare_admin_token: str = ""
54
+
55
+ # HF talks to LibCal directly because some hosted environments cannot open
56
+ # outbound connections to shared workers.dev hostnames.
57
+ libcal_client_id: str = ""
58
+ libcal_client_secret: str = ""
59
+ libcal_events_client_id: str = ""
60
+ libcal_events_client_secret: str = ""
61
+
62
+ @field_validator("max_results")
63
+ @classmethod
64
+ def reasonable_max_results(cls, value: int) -> int:
65
+ if not 1 <= value <= 20:
66
+ raise ValueError("max_results must be between 1 and 20")
67
+ return value
68
+
69
+ @field_validator("max_question_chars", "max_message_chars")
70
+ @classmethod
71
+ def reasonable_text_limit(cls, value: int, info) -> int:
72
+ ceiling = 2000 if info.field_name == "max_question_chars" else 4000
73
+ if not 256 <= value <= ceiling:
74
+ raise ValueError(f"{info.field_name} must be between 256 and {ceiling}")
75
+ return value
76
+
77
+ @field_validator("max_history_messages")
78
+ @classmethod
79
+ def reasonable_history_limit(cls, value: int) -> int:
80
+ if not 0 <= value <= 12:
81
+ raise ValueError("max_history_messages must be between 0 and 12")
82
+ return value
83
+
84
+ @field_validator("max_concurrent_agent_requests")
85
+ @classmethod
86
+ def reasonable_concurrency(cls, value: int) -> int:
87
+ if not 1 <= value <= 100:
88
+ raise ValueError("max_concurrent_agent_requests must be between 1 and 100")
89
  return value
90
 
91
+ @field_validator("max_request_body_bytes")
92
  @classmethod
93
+ def reasonable_body_limit(cls, value: int) -> int:
94
+ if not 16_384 <= value <= 2_097_152:
95
+ raise ValueError("max_request_body_bytes must be between 16 KiB and 2 MiB")
96
  return value
97
 
98
  @property
99
+ def runtime_defaults(self) -> dict:
100
+ """One source of defaults for admin, public config, and agent replies."""
101
+ return {
102
+ "max_results": self.max_results,
103
+ "maintenance_mode": self.maintenance_mode,
104
+ "welcome_message": "Hi! I'm LibBee, the Khalifa University Library AI Assistant.",
105
+ "custom_instructions": "",
106
+ "announcement": "",
107
+ "maintenance_message": self.maintenance_message,
108
+ }
109
+
110
+ @property
111
+ def cors_origins(self) -> list[str]:
112
+ return [value.strip().rstrip("/") for value in self.allowed_cors_origins.split(",") if value.strip()]
113
+
114
+ @property
115
  def runtime_path(self) -> Path:
116
  requested = Path(self.runtime_dir)
117
+ # Honour explicit paths, including nested directories not created yet.
118
+ # Only the absent default HF mount falls back to ephemeral local disk.
119
+ if self.runtime_dir == "/data/libbee_runtime" and not Path("/data").is_dir():
120
+ return Path("/tmp/libbee_runtime")
121
+ return requested
122
+
123
+ @property
124
+ def feedback_path(self) -> Path:
125
+ return Path(self.feedback_store_path) if self.feedback_store_path else self.runtime_path / "feedback.jsonl"
126
+
127
+ @property
128
+ def metrics_path(self) -> Path:
129
+ return Path(self.metrics_store_path) if self.metrics_store_path else self.runtime_path / "metrics.json"
130
+
131
+ @property
132
+ def config_path(self) -> Path:
133
+ return Path(self.config_store_path) if self.config_store_path else self.runtime_path / "runtime_config.json"
134
+
135
+ @property
136
+ def rag_cache_dir(self) -> Path:
137
+ return Path(self.rag_index_dir) if self.rag_index_dir else self.runtime_path / "rag_cache"
138
+
139
+ @property
140
+ def kb_dir(self) -> Path:
141
+ if self.knowledge_dir:
142
+ return Path(self.knowledge_dir)
143
+ if Path("/app/data/knowledge").exists():
144
+ return Path("/app/data/knowledge")
145
+ return Path("data/knowledge")
146
+
147
+
148
+ @lru_cache
149
+ def get_settings() -> Settings:
150
+ settings = Settings()
151
+ settings.runtime_path.mkdir(parents=True, exist_ok=True)
152
+ settings.rag_cache_dir.mkdir(parents=True, exist_ok=True)
153
+ return settings