librechat-python-sdk 0.1.0__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.
examples/basic_chat.py ADDED
@@ -0,0 +1,39 @@
1
+ """
2
+ Basic Chat Example using LibreChat Python SDK
3
+ """
4
+
5
+ import os
6
+ from dotenv import load_dotenv
7
+ from librechat_client import LibreChatClient
8
+
9
+ def main():
10
+ load_dotenv()
11
+
12
+ base_url = os.getenv("LIBRECHAT_BASE_URL", "https://chat.example.com")
13
+ refresh_token = os.getenv("LIBRECHAT_REFRESH_TOKEN")
14
+ connect_sid = os.getenv("LIBRECHAT_CONNECT_SID")
15
+
16
+ if not refresh_token or refresh_token == "YOUR_REFRESH_TOKEN_HERE":
17
+ print("[!] Please configure LIBRECHAT_REFRESH_TOKEN in your .env file.")
18
+ return
19
+
20
+ # Initialize client
21
+ client = LibreChatClient(
22
+ base_url=base_url,
23
+ refresh_token=refresh_token,
24
+ connect_sid=connect_sid
25
+ )
26
+
27
+ print("[+] Sending request to LibreChat...")
28
+
29
+ # Send a prompt to the agent endpoint
30
+ # The client automatically handles JWT refresh & token rotation
31
+ response = client.send_agent_message(
32
+ prompt="Explain the difference between GitOps and traditional CI/CD in 3 bullet points."
33
+ )
34
+
35
+ print("\n=== LibreChat Response ===")
36
+ print(response.get("text", response))
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,99 @@
1
+ """
2
+ Continuous Folder Monitor Daemon Example
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import time
8
+ import logging
9
+ from pathlib import Path
10
+ from dotenv import load_dotenv
11
+
12
+ # Ensure SDK root is in python path
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
14
+
15
+ from librechat_client import LibreChatClient
16
+ from tracker import FileStateTracker
17
+
18
+ try:
19
+ from watchdog.observers import Observer
20
+ from watchdog.events import FileSystemEventHandler
21
+ HAS_WATCHDOG = True
22
+ except ImportError:
23
+ HAS_WATCHDOG = False
24
+
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format="%(asctime)s [%(levelname)s] (FileMonitor) %(message)s"
28
+ )
29
+ logger = logging.getLogger("FileMonitor")
30
+
31
+ def run_monitor():
32
+ load_dotenv()
33
+
34
+ watch_dir = Path(os.getenv("WATCH_DIR", "./watch_folder")).resolve()
35
+ output_dir = Path(os.getenv("OUTPUT_DIR", "./output_folder")).resolve()
36
+
37
+ watch_dir.mkdir(parents=True, exist_ok=True)
38
+ output_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ base_url = os.getenv("LIBRECHAT_BASE_URL", "https://chat.example.com")
41
+ refresh_token = os.getenv("LIBRECHAT_REFRESH_TOKEN")
42
+ connect_sid = os.getenv("LIBRECHAT_CONNECT_SID")
43
+
44
+ if not refresh_token or refresh_token == "YOUR_REFRESH_TOKEN_HERE":
45
+ logger.error("LIBRECHAT_REFRESH_TOKEN is not set in .env!")
46
+ return
47
+
48
+ client = LibreChatClient(
49
+ base_url=base_url,
50
+ refresh_token=refresh_token,
51
+ connect_sid=connect_sid
52
+ )
53
+ tracker = FileStateTracker()
54
+
55
+ logger.info(f"Monitoring folder: {watch_dir}")
56
+
57
+ def process_file(filepath: Path):
58
+ if not filepath.is_file() or filepath.name.startswith(".") or tracker.is_processed(filepath):
59
+ return
60
+ logger.info(f"Processing new file: {filepath.name}")
61
+ try:
62
+ content = filepath.read_text(encoding="utf-8")
63
+ response = client.send_agent_message(prompt=f"Summarize:\n\n{content}")
64
+
65
+ output_file = output_dir / f"{filepath.stem}_summary_{int(time.time())}.md"
66
+ output_file.write_text(str(response.get("text", response)), encoding="utf-8")
67
+
68
+ tracker.mark_processed(filepath, output_file)
69
+ logger.info(f"[✓] Saved response to: {output_file}")
70
+ except Exception as e:
71
+ logger.error(f"Failed to process '{filepath.name}': {e}")
72
+
73
+ # Initial scan for offline changes across reboots
74
+ for p in watch_dir.rglob("*"):
75
+ process_file(p)
76
+
77
+ if HAS_WATCHDOG:
78
+ class Handler(FileSystemEventHandler):
79
+ def on_created(self, event):
80
+ process_file(Path(event.src_path))
81
+ def on_modified(self, event):
82
+ process_file(Path(event.src_path))
83
+
84
+ observer = Observer()
85
+ observer.schedule(Handler(), str(watch_dir), recursive=True)
86
+ observer.start()
87
+
88
+ try:
89
+ while True:
90
+ time.sleep(30)
91
+ for p in watch_dir.rglob("*"):
92
+ process_file(p)
93
+ except KeyboardInterrupt:
94
+ logger.info("Monitor stopped.")
95
+ if HAS_WATCHDOG:
96
+ observer.stop()
97
+
98
+ if __name__ == "__main__":
99
+ run_monitor()
@@ -0,0 +1,69 @@
1
+ """
2
+ File state tracking module for long-running file monitoring.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import time
8
+ import hashlib
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Dict, Any, Optional
12
+
13
+ logger = logging.getLogger("LibreChatSDK.FileTracker")
14
+
15
+ class FileStateTracker:
16
+ """Tracks file modification timestamps and SHA256 hashes across reboots."""
17
+ def __init__(self, db_path: str = "processed_files.json"):
18
+ self.db_path = db_path
19
+ self.data: Dict[str, Dict[str, Any]] = {}
20
+ self.load()
21
+
22
+ def load(self):
23
+ if os.path.exists(self.db_path):
24
+ try:
25
+ with open(self.db_path, "r", encoding="utf-8") as f:
26
+ self.data = json.load(f)
27
+ except Exception as e:
28
+ logger.warning(f"Could not load state DB '{self.db_path}': {e}")
29
+ self.data = {}
30
+
31
+ def save(self):
32
+ try:
33
+ with open(self.db_path, "w", encoding="utf-8") as f:
34
+ json.dump(self.data, f, indent=2)
35
+ except Exception as e:
36
+ logger.error(f"Failed to save state DB '{self.db_path}': {e}")
37
+
38
+ @staticmethod
39
+ def compute_sha256(filepath: Path) -> str:
40
+ hasher = hashlib.sha256()
41
+ try:
42
+ with open(filepath, "rb") as f:
43
+ while chunk := f.read(65536):
44
+ hasher.update(chunk)
45
+ return hasher.hexdigest()
46
+ except Exception:
47
+ return ""
48
+
49
+ def is_processed(self, filepath: Path) -> bool:
50
+ key = str(filepath.resolve())
51
+ if key not in self.data:
52
+ return False
53
+
54
+ entry = self.data[key]
55
+ if entry.get("mtime") == filepath.stat().st_mtime:
56
+ return True
57
+
58
+ current_hash = self.compute_sha256(filepath)
59
+ return entry.get("hash") == current_hash and bool(current_hash)
60
+
61
+ def mark_processed(self, filepath: Path, output_path: Optional[Path] = None):
62
+ key = str(filepath.resolve())
63
+ self.data[key] = {
64
+ "mtime": filepath.stat().st_mtime,
65
+ "hash": self.compute_sha256(filepath),
66
+ "processed_at": time.strftime("%Y-%m-%d %H:%M:%S"),
67
+ "output_file": str(output_path) if output_path else None
68
+ }
69
+ self.save()
@@ -0,0 +1,116 @@
1
+ """
2
+ GitOps Infrastructure-as-Code (IaC) Automated Security Auditor
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ A DevOps automation daemon that monitors a repository directory for Kubernetes manifests,
5
+ Terraform files, or Dockerfiles, passes changes to LibreChat for AI-assisted security
6
+ and compliance auditing, and generates markdown audit reports.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import time
12
+ import logging
13
+ from pathlib import Path
14
+ from dotenv import load_dotenv
15
+
16
+ # Ensure parent directory is in python path
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
18
+
19
+ from librechat_client import LibreChatClient
20
+
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format="%(asctime)s [%(levelname)s] (GitOps-Auditor) %(message)s"
24
+ )
25
+ logger = logging.getLogger("GitOpsAuditor")
26
+
27
+ DEFAULT_IAC_EXTENSIONS = {".yaml", ".yml", ".tf", ".dockerfile", "dockerfile"}
28
+
29
+ GITOPS_SECURITY_PROMPT = """
30
+ You are an expert DevOps & DevSecOps engineer performing an automated security code review.
31
+ Analyze the following Infrastructure-as-Code (IaC) configuration for security risks, compliance violations, and best practices.
32
+
33
+ Focus on:
34
+ 1. Least Privilege & Access Control (e.g., non-root containers, open security groups 0.0.0.0/0).
35
+ 2. Resource Management (missing CPU/memory limits, quotas).
36
+ 3. Secret Exposure (hardcoded credentials, API keys, unencrypted storage).
37
+ 4. Container Security (floating image tags, privilege escalation).
38
+
39
+ Return your findings formatted as a clear Markdown report with risk ratings (HIGH, MEDIUM, LOW) and actionable fix recommendations.
40
+
41
+ File Content:
42
+ ```
43
+ {content}
44
+ ```
45
+ """
46
+
47
+ def run_iac_auditor():
48
+ load_dotenv()
49
+
50
+ watch_dir = Path(os.getenv("IAC_WATCH_DIR", "./iac_repository")).resolve()
51
+ reports_dir = Path(os.getenv("REPORTS_DIR", "./reports")).resolve()
52
+
53
+ watch_dir.mkdir(parents=True, exist_ok=True)
54
+ reports_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ base_url = os.getenv("LIBRECHAT_BASE_URL", "https://chat.example.com")
57
+ refresh_token = os.getenv("LIBRECHAT_REFRESH_TOKEN")
58
+ connect_sid = os.getenv("LIBRECHAT_CONNECT_SID")
59
+
60
+ if not refresh_token or refresh_token == "YOUR_REFRESH_TOKEN_HERE":
61
+ logger.error("LIBRECHAT_REFRESH_TOKEN is not configured in .env!")
62
+ return
63
+
64
+ client = LibreChatClient(
65
+ base_url=base_url,
66
+ refresh_token=refresh_token,
67
+ connect_sid=connect_sid
68
+ )
69
+
70
+ logger.info(f"GitOps Security Auditor running. Monitoring IaC folder: {watch_dir}")
71
+
72
+ processed_files = set()
73
+
74
+ while True:
75
+ for file_path in watch_dir.rglob("*"):
76
+ if not file_path.is_file() or file_path.name.startswith("."):
77
+ continue
78
+
79
+ is_iac_file = file_path.suffix.lower() in DEFAULT_IAC_EXTENSIONS or "dockerfile" in file_path.name.lower()
80
+ if not is_iac_file:
81
+ continue
82
+
83
+ file_key = f"{file_path.resolve()}:{file_path.stat().st_mtime}"
84
+ if file_key in processed_files:
85
+ continue
86
+
87
+ logger.info(f"--> Auditing IaC configuration file: {file_path.name}")
88
+ try:
89
+ content = file_path.read_text(encoding="utf-8")
90
+ prompt = GITOPS_SECURITY_PROMPT.format(content=content)
91
+
92
+ logger.info(f"Submitting '{file_path.name}' to LibreChat LLM endpoint...")
93
+ result = client.send_agent_message(prompt=prompt)
94
+
95
+ report_text = result.get("text") if isinstance(result, dict) else str(result)
96
+
97
+ report_path = reports_dir / f"audit_{file_path.stem}_{int(time.time())}.md"
98
+ report_content = (
99
+ f"# GitOps Security Audit Report\n"
100
+ f"- **Target File**: `{file_path.name}`\n"
101
+ f"- **Audited At**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
102
+ f"--- \n\n"
103
+ f"{report_text}\n"
104
+ )
105
+ report_path.write_text(report_content, encoding="utf-8")
106
+
107
+ logger.info(f"[✓] Audit Report generated: {report_path}")
108
+ processed_files.add(file_key)
109
+
110
+ except Exception as err:
111
+ logger.error(f"[x] Failed to audit file '{file_path.name}': {err}")
112
+
113
+ time.sleep(15)
114
+
115
+ if __name__ == "__main__":
116
+ run_iac_auditor()
@@ -0,0 +1,20 @@
1
+ """
2
+ LibreChat Python SDK
3
+ ~~~~~~~~~~~~~~~~~~~
4
+ A resilient Python client SDK for interacting with LibreChat backend APIs with
5
+ Refresh Token Rotation (RTR) and long-running process state persistence.
6
+ """
7
+
8
+ from .client import LibreChatClient
9
+ from .exceptions import LibreChatError, LibreChatAuthError, LibreChatAPIError, TokenExpiredError
10
+ from .utils import parse_jwt_payload, is_jwt_expired
11
+
12
+ __all__ = [
13
+ "LibreChatClient",
14
+ "LibreChatError",
15
+ "LibreChatAuthError",
16
+ "LibreChatAPIError",
17
+ "TokenExpiredError",
18
+ "parse_jwt_payload",
19
+ "is_jwt_expired",
20
+ ]
@@ -0,0 +1,203 @@
1
+ """
2
+ Core LibreChat API Client with Refresh Token Rotation (RTR) and Session Persistence.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import time
8
+ import logging
9
+ from typing import Any, Dict, Optional, Union
10
+ import requests
11
+
12
+ from .utils import is_jwt_expired
13
+ from .exceptions import LibreChatAuthError, LibreChatAPIError
14
+
15
+ logger = logging.getLogger("LibreChatSDK.Client")
16
+
17
+ DEFAULT_STATE_FILE = "session_tokens.json"
18
+
19
+ class LibreChatClient:
20
+ """
21
+ An enterprise-grade, resilient Python client for LibreChat web applications.
22
+
23
+ Features:
24
+ - Automatic Refresh Token Rotation (RTR)
25
+ - Proactive JWT access token expiration monitoring
26
+ - Multi-day session persistence across laptop sleep & system reboots
27
+ - Exponential backoff retry logic for network drops
28
+ """
29
+ def __init__(
30
+ self,
31
+ base_url: str,
32
+ refresh_token: str,
33
+ connect_sid: Optional[str] = None,
34
+ token_provider: str = "librechat",
35
+ state_file: str = DEFAULT_STATE_FILE,
36
+ verify_ssl: bool = True
37
+ ):
38
+ self.base_url = base_url.rstrip("/")
39
+ self.verify_ssl = verify_ssl
40
+ self.state_file = state_file
41
+ self.access_token: Optional[str] = None
42
+ self.refresh_token = refresh_token
43
+ self.connect_sid = connect_sid
44
+ self.token_provider = token_provider
45
+
46
+ self.session = requests.Session()
47
+
48
+ # Load rotated tokens from disk if state file exists
49
+ self._load_state()
50
+
51
+ # Standard browser headers to ensure CORS & reverse proxy compatibility
52
+ self.session.headers.update({
53
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
54
+ "Origin": self.base_url,
55
+ "Referer": f"{self.base_url}/",
56
+ "Accept": "application/json, text/plain, */*"
57
+ })
58
+
59
+ self._sync_cookies()
60
+
61
+ def _sync_cookies(self):
62
+ """Bind authentication cookies to the session."""
63
+ self.session.cookies.set("refreshToken", self.refresh_token)
64
+ self.session.cookies.set("token_provider", self.token_provider)
65
+ if self.connect_sid:
66
+ self.session.cookies.set("connect.sid", self.connect_sid)
67
+
68
+ def _load_state(self):
69
+ """Restore active rotated refresh token from persistent storage."""
70
+ if os.path.exists(self.state_file):
71
+ try:
72
+ with open(self.state_file, "r", encoding="utf-8") as f:
73
+ state = json.load(f)
74
+ if state.get("refresh_token"):
75
+ self.refresh_token = state["refresh_token"]
76
+ logger.info("Restored latest rotated refresh token from state file.")
77
+ if state.get("access_token"):
78
+ self.access_token = state["access_token"]
79
+ if state.get("connect_sid"):
80
+ self.connect_sid = state["connect_sid"]
81
+ except Exception as e:
82
+ logger.warning(f"Could not read state file '{self.state_file}': {e}")
83
+
84
+ def _save_state(self):
85
+ """Save updated tokens after a successful Refresh Token Rotation."""
86
+ state = {
87
+ "refresh_token": self.refresh_token,
88
+ "access_token": self.access_token,
89
+ "connect_sid": self.connect_sid,
90
+ "updated_at": time.strftime("%Y-%m-%d %H:%M:%S")
91
+ }
92
+ try:
93
+ with open(self.state_file, "w", encoding="utf-8") as f:
94
+ json.dump(state, f, indent=2)
95
+ logger.debug(f"Saved token state to {self.state_file}")
96
+ except Exception as e:
97
+ logger.error(f"Failed to write state file '{self.state_file}': {e}")
98
+
99
+ def refresh_access_token(self, max_retries: int = 5) -> bool:
100
+ """
101
+ Request a fresh JWT access token via POST /api/auth/refresh.
102
+ Handles Refresh Token Rotation (RTR) seamlessly.
103
+ """
104
+ refresh_url = f"{self.base_url}/api/auth/refresh"
105
+ self._sync_cookies()
106
+
107
+ backoff = 2
108
+ for attempt in range(1, max_retries + 1):
109
+ logger.info(f"Refresing JWT access token (Attempt {attempt}/{max_retries})...")
110
+ try:
111
+ response = self.session.post(
112
+ refresh_url,
113
+ json={},
114
+ verify=self.verify_ssl,
115
+ timeout=15
116
+ )
117
+
118
+ # Fallback to GET if method is restricted by custom proxy
119
+ if response.status_code in (404, 405):
120
+ response = self.session.get(refresh_url, verify=self.verify_ssl, timeout=15)
121
+
122
+ if response.status_code == 200:
123
+ data = response.json()
124
+ new_token = data.get("token") or data.get("accessToken")
125
+ if new_token:
126
+ self.access_token = new_token
127
+ logger.info("Successfully acquired new access token.")
128
+
129
+ # Handle Refresh Token Rotation
130
+ if "refreshToken" in response.cookies:
131
+ rotated_token = response.cookies["refreshToken"]
132
+ if rotated_token != self.refresh_token:
133
+ logger.info("Detected new rotated refreshToken from server.")
134
+ self.refresh_token = rotated_token
135
+
136
+ self._save_state()
137
+ return True
138
+ else:
139
+ logger.error(f"Token refresh payload missing token field: {data}")
140
+ else:
141
+ logger.error(f"Refresh failed with HTTP {response.status_code}: {response.text}")
142
+ except requests.exceptions.RequestException as e:
143
+ logger.warning(f"Network error during token refresh: {e}. Retrying in {backoff}s...")
144
+ time.sleep(backoff)
145
+ backoff = min(backoff * 2, 60)
146
+
147
+ return False
148
+
149
+ def request(self, method: str, endpoint: str, max_retries: int = 3, **kwargs) -> requests.Response:
150
+ """
151
+ Execute an HTTP request against LibreChat with automatic proactive token check & 401 recovery.
152
+ """
153
+ url = f"{self.base_url}{endpoint}" if endpoint.startswith("/") else f"{self.base_url}/{endpoint}"
154
+
155
+ # Proactively check token expiry
156
+ if not self.access_token or is_jwt_expired(self.access_token):
157
+ logger.info("Access token missing or near expiration. Executing refresh...")
158
+ if not self.refresh_access_token():
159
+ raise LibreChatAuthError("Failed to refresh access token using stored refresh token.")
160
+
161
+ headers = kwargs.pop("headers", {})
162
+ headers["Authorization"] = f"Bearer {self.access_token}"
163
+
164
+ backoff = 2
165
+ for attempt in range(1, max_retries + 1):
166
+ try:
167
+ response = self.session.request(method, url, headers=headers, verify=self.verify_ssl, **kwargs)
168
+
169
+ # Handle HTTP 401 Unauthorized by attempting token rotation retry
170
+ if response.status_code == 401:
171
+ logger.warning("HTTP 401 Unauthorized encountered. Attempting emergency token refresh...")
172
+ if self.refresh_access_token():
173
+ headers["Authorization"] = f"Bearer {self.access_token}"
174
+ response = self.session.request(method, url, headers=headers, verify=self.verify_ssl, **kwargs)
175
+ else:
176
+ raise LibreChatAuthError("HTTP 401 Unauthorized and token refresh failed.")
177
+
178
+ if response.status_code >= 400:
179
+ raise LibreChatAPIError(
180
+ f"LibreChat API returned HTTP {response.status_code}",
181
+ status_code=response.status_code,
182
+ response_text=response.text
183
+ )
184
+
185
+ return response
186
+ except requests.exceptions.RequestException as e:
187
+ logger.warning(f"Network request attempt {attempt}/{max_retries} failed: {e}. Retrying in {backoff}s...")
188
+ time.sleep(backoff)
189
+ backoff = min(backoff * 2, 30)
190
+
191
+ raise LibreChatAPIError(f"Network request to {url} failed after {max_retries} retries.", status_code=500)
192
+
193
+ def send_agent_message(self, prompt: str, agent_id: Optional[str] = None, conversation_id: Optional[str] = None) -> Dict[str, Any]:
194
+ """
195
+ Send a prompt to the LibreChat Agent endpoint (/api/agents/chat/agents).
196
+ """
197
+ payload = {
198
+ "text": prompt,
199
+ "agent_id": agent_id,
200
+ "conversationId": conversation_id
201
+ }
202
+ response = self.request("POST", "/api/agents/chat/agents", json=payload)
203
+ return response.json()
@@ -0,0 +1,22 @@
1
+ """
2
+ Custom Exception Classes for LibreChat SDK
3
+ """
4
+
5
+ class LibreChatError(Exception):
6
+ """Base exception for all LibreChat SDK errors."""
7
+ pass
8
+
9
+ class LibreChatAuthError(LibreChatError):
10
+ """Raised when authentication or token refresh fails."""
11
+ pass
12
+
13
+ class LibreChatAPIError(LibreChatError):
14
+ """Raised when API endpoints return error status codes."""
15
+ def __init__(self, message: str, status_code: int, response_text: str = ""):
16
+ super().__init__(message)
17
+ self.status_code = status_code
18
+ self.response_text = response_text
19
+
20
+ class TokenExpiredError(LibreChatAuthError):
21
+ """Raised when a token has expired and cannot be auto-refreshed."""
22
+ pass
@@ -0,0 +1,38 @@
1
+ """
2
+ Utility functions for JWT parsing and token lifecycle management.
3
+ """
4
+
5
+ import base64
6
+ import json
7
+ import time
8
+ import logging
9
+ from typing import Optional, Dict, Any
10
+
11
+ logger = logging.getLogger("LibreChatSDK.Utils")
12
+
13
+ def parse_jwt_payload(token: str) -> Optional[Dict[str, Any]]:
14
+ """
15
+ Decodes an unencrypted JWT payload without requiring external crypto dependencies.
16
+ """
17
+ try:
18
+ parts = token.split(".")
19
+ if len(parts) != 3:
20
+ return None
21
+ payload_b64 = parts[1]
22
+ # Pad string if necessary
23
+ payload_b64 += "=" * (-len(payload_b64) % 4)
24
+ payload_bytes = base64.urlsafe_b64decode(payload_b64)
25
+ return json.loads(payload_bytes.decode("utf-8"))
26
+ except Exception as e:
27
+ logger.debug(f"Failed to parse JWT payload: {e}")
28
+ return None
29
+
30
+ def is_jwt_expired(token: str, buffer_seconds: int = 60) -> bool:
31
+ """
32
+ Check if a JWT access token is expired or within buffer_seconds of expiration.
33
+ """
34
+ payload = parse_jwt_payload(token)
35
+ if not payload or "exp" not in payload:
36
+ return True
37
+ exp_timestamp = payload["exp"]
38
+ return (time.time() + buffer_seconds) >= exp_timestamp
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: librechat-python-sdk
3
+ Version: 0.1.0
4
+ Summary: A resilient, auto-refreshing Python client SDK for LibreChat API endpoints featuring Refresh Token Rotation (RTR) and multi-day state persistence.
5
+ Author: Open Source Developer
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
11
+ Classifier: Topic :: System :: Monitoring
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: requests>=2.28.0
15
+ Requires-Dist: python-dotenv>=1.0.0
16
+ Provides-Extra: devops
17
+ Requires-Dist: watchdog>=3.0.0; extra == "devops"
18
+ Dynamic: license-file
19
+
20
+ # LibreChat Python SDK (`librechat-python-sdk`)
21
+
22
+ [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/downloads/)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
24
+
25
+ An enterprise-grade, resilient Python client SDK for interacting with **[LibreChat](https://github.com/danny-avila/LibreChat)** backend APIs.
26
+
27
+ Built specifically for **long-running daemons, multi-day background processes, and DevOps/GitOps automation pipelines**, this SDK handles LibreChat's **Refresh Token Rotation (RTR)**, short-lived JWT expirations, network disconnects during system sleep, and session state persistence across reboots.
28
+
29
+ ---
30
+
31
+ ## Key Features
32
+
33
+ - 🔄 **Refresh Token Rotation (RTR) Support**: Automatically captures newly rotated refresh tokens returned in `Set-Cookie` headers and updates active sessions.
34
+ - 💾 **Multi-Day State Persistence**: Saves updated session state to `session_tokens.json` on disk so long-running scripts survive laptop sleep/wake cycles and system reboots.
35
+ - ⏱️ **Proactive Expiration Checks**: Decodes JWT access token expiration timestamps (`exp`) and preemptively refreshes tokens before sending API calls.
36
+ - ⚡ **Network Fault Tolerance**: Built-in exponential backoff retries to handle intermittent Wi-Fi drops, sleep mode transitions, and network glitches.
37
+ - 🛡️ **DevOps & GitOps Ready**: Out-of-the-box examples for automated IaC security auditing, continuous folder watching, and CI/CD log analysis.
38
+
39
+ ---
40
+
41
+ ## Architecture & Token Lifecycle
42
+
43
+ LibreChat uses short-lived JWT access tokens (**15 minutes**) paired with long-lived refresh tokens (**7+ days**). Whenever a token refresh occurs, LibreChat revokes the old refresh token and issues a new one.
44
+
45
+ ```mermaid
46
+ sequenceDiagram
47
+ participant PythonScript as Python Script / Daemon
48
+ participant SDK as LibreChatClient SDK
49
+ participant Disk as Local State File (session_tokens.json)
50
+ participant Server as LibreChat Backend
51
+
52
+ PythonScript->>SDK: send_agent_message(prompt)
53
+ SDK->>SDK: Check JWT Expiration (exp timestamp)
54
+ alt Access Token Expired or Missing
55
+ SDK->>Server: POST /api/auth/refresh (with refreshToken cookie)
56
+ Server-->>SDK: 200 OK + New Access Token + New Rotated RefreshToken
57
+ SDK->>Disk: Persist new tokens to session_tokens.json
58
+ end
59
+ SDK->>Server: POST /api/agents/chat/agents (Bearer Access Token)
60
+ Server-->>SDK: HTTP 200 OK (Agent Response Payload)
61
+ SDK-->>PythonScript: Return JSON Response
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Installation
67
+
68
+ Clone the repository and install in editable mode:
69
+
70
+ ```bash
71
+ git clone https://github.com/your-username/librechat-python-sdk.git
72
+ cd librechat-python-sdk
73
+ pip install -e .
74
+ ```
75
+
76
+ To include optional DevOps folder monitoring dependencies:
77
+
78
+ ```bash
79
+ pip install -e .[devops]
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Quickstart
85
+
86
+ ### 1. Environment Setup
87
+
88
+ Create a `.env` file from the provided template:
89
+
90
+ ```bash
91
+ cp .env.example .env
92
+ ```
93
+
94
+ Fill in your LibreChat endpoint and browser session cookie:
95
+
96
+ ```env
97
+ LIBRECHAT_BASE_URL=https://chat.example.com
98
+ LIBRECHAT_REFRESH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
99
+ LIBRECHAT_CONNECT_SID=s%3A...
100
+ ```
101
+
102
+ ### 2. Basic Usage
103
+
104
+ ```python
105
+ import os
106
+ from dotenv import load_dotenv
107
+ from librechat_client import LibreChatClient
108
+
109
+ load_dotenv()
110
+
111
+ # Initialize resilient client
112
+ client = LibreChatClient(
113
+ base_url=os.getenv("LIBRECHAT_BASE_URL"),
114
+ refresh_token=os.getenv("LIBRECHAT_REFRESH_TOKEN"),
115
+ connect_sid=os.getenv("LIBRECHAT_CONNECT_SID")
116
+ )
117
+
118
+ # Send a prompt to the agent endpoint
119
+ response = client.send_agent_message(
120
+ prompt="Explain the benefits of Infrastructure-as-Code in DevSecOps."
121
+ )
122
+
123
+ print(response.get("text"))
124
+ ```
125
+
126
+ ---
127
+
128
+ ## DevOps & GitOps Showcase Examples
129
+
130
+ The [`examples/`](examples/) directory includes real-world automation implementations:
131
+
132
+ 1. **[GitOps IaC Security Auditor](examples/gitops_iac_auditor/)**: Monitors a repository directory for Kubernetes YAMLs, Terraform files, and Dockerfiles, submitting changes to LibreChat for automated security compliance auditing.
133
+ 2. **[Multi-Day File Monitor Daemon](examples/file_monitor/)**: A background folder watcher service with state tracking (`processed_files.json`) that survives system reboots and long idle periods.
134
+ 3. **[Basic Chat Example](examples/basic_chat.py)**: Minimal 15-line quickstart script.
135
+
136
+ ---
137
+
138
+ ## Project Structure
139
+
140
+ ```
141
+ librechat-python-sdk/
142
+ ├── .env.example # Environment template
143
+ ├── .gitignore # Production gitignore rules
144
+ ├── LICENSE # MIT License
145
+ ├── pyproject.toml # Python packaging metadata
146
+ ├── README.md # Project documentation
147
+ ├── requirements.txt # Package dependencies
148
+ ├── librechat_client/
149
+ │ ├── __init__.py # SDK package exports
150
+ │ ├── client.py # Core LibreChat API client with RTR & retry logic
151
+ │ ├── exceptions.py # Custom exception hierarchy
152
+ │ └── utils.py # JWT decoding & helper utilities
153
+ └── examples/
154
+ ├── basic_chat.py # Minimal usage script
155
+ ├── devops_iac_auditor/ # GitOps security auditor daemon example
156
+ └── file_monitor/ # Resilient folder monitor daemon example
157
+ ```
158
+
159
+ ---
160
+
161
+ ## License
162
+
163
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,13 @@
1
+ examples/basic_chat.py,sha256=I2IFznP7nHvNiOQAo1RehHiRPrI8k7Q8N2b_aaqUQbM,1130
2
+ examples/file_monitor/main.py,sha256=dfBq0HMnEGDSsJt3hi8gr8AM-LL8TBEZ23HD3ZNKvlk,3149
3
+ examples/file_monitor/tracker.py,sha256=ZSxx-BnCW4nkWLrSOs4G1-h5aOoXlSm6ikVNOPNe0tA,2267
4
+ examples/gitops_iac_auditor/main.py,sha256=UmfwTbt6o30kgnQb9Hi4r_f-IF4IRivj4tlMZPs3jMk,4466
5
+ librechat_client/__init__.py,sha256=hI82UsN6f0v7LW6R05Qgvx7yzcE6ZP-2OAmbKhCLz5g,572
6
+ librechat_client/client.py,sha256=AWIN7E8Ud5UgZy2rI2zRyHc9okN-aD9Y343YwiSQVcM,8971
7
+ librechat_client/exceptions.py,sha256=25IqxyOB_tjAKQSoVdRYlZdgoMbKa3mFiYBXKtz8f2Q,691
8
+ librechat_client/utils.py,sha256=U2ggP19MEZTfVEf0gbaI6ZnlgD24z_Okyk0mfwYfrD0,1183
9
+ librechat_python_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=M3cR1PlJHIOVkEtaebK5lK0EPJyFLrmstKLGJB-RTg0,1107
10
+ librechat_python_sdk-0.1.0.dist-info/METADATA,sha256=oPiTZl9t-mSaafcbO8_-x5L2yJBPxn7hw3gqyl1ckbg,6162
11
+ librechat_python_sdk-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
12
+ librechat_python_sdk-0.1.0.dist-info/top_level.txt,sha256=akSfwEbhgwTpJ9RLutUa6XcBSQHcgvmrseHM3N7cNak,26
13
+ librechat_python_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
20
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+ examples
2
+ librechat_client