librechat-python-sdk 0.1.0__tar.gz
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.
- librechat_python_sdk-0.1.0/LICENSE +21 -0
- librechat_python_sdk-0.1.0/PKG-INFO +163 -0
- librechat_python_sdk-0.1.0/README.md +144 -0
- librechat_python_sdk-0.1.0/examples/basic_chat.py +39 -0
- librechat_python_sdk-0.1.0/examples/file_monitor/main.py +99 -0
- librechat_python_sdk-0.1.0/examples/file_monitor/tracker.py +69 -0
- librechat_python_sdk-0.1.0/examples/gitops_iac_auditor/main.py +116 -0
- librechat_python_sdk-0.1.0/librechat_client/__init__.py +20 -0
- librechat_python_sdk-0.1.0/librechat_client/client.py +203 -0
- librechat_python_sdk-0.1.0/librechat_client/exceptions.py +22 -0
- librechat_python_sdk-0.1.0/librechat_client/utils.py +38 -0
- librechat_python_sdk-0.1.0/librechat_python_sdk.egg-info/PKG-INFO +163 -0
- librechat_python_sdk-0.1.0/librechat_python_sdk.egg-info/SOURCES.txt +16 -0
- librechat_python_sdk-0.1.0/librechat_python_sdk.egg-info/dependency_links.txt +1 -0
- librechat_python_sdk-0.1.0/librechat_python_sdk.egg-info/requires.txt +5 -0
- librechat_python_sdk-0.1.0/librechat_python_sdk.egg-info/top_level.txt +3 -0
- librechat_python_sdk-0.1.0/pyproject.toml +32 -0
- librechat_python_sdk-0.1.0/setup.cfg +4 -0
|
@@ -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,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
|
+
[](https://www.python.org/downloads/)
|
|
23
|
+
[](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,144 @@
|
|
|
1
|
+
# LibreChat Python SDK (`librechat-python-sdk`)
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
An enterprise-grade, resilient Python client SDK for interacting with **[LibreChat](https://github.com/danny-avila/LibreChat)** backend APIs.
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Key Features
|
|
13
|
+
|
|
14
|
+
- 🔄 **Refresh Token Rotation (RTR) Support**: Automatically captures newly rotated refresh tokens returned in `Set-Cookie` headers and updates active sessions.
|
|
15
|
+
- 💾 **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.
|
|
16
|
+
- ⏱️ **Proactive Expiration Checks**: Decodes JWT access token expiration timestamps (`exp`) and preemptively refreshes tokens before sending API calls.
|
|
17
|
+
- ⚡ **Network Fault Tolerance**: Built-in exponential backoff retries to handle intermittent Wi-Fi drops, sleep mode transitions, and network glitches.
|
|
18
|
+
- 🛡️ **DevOps & GitOps Ready**: Out-of-the-box examples for automated IaC security auditing, continuous folder watching, and CI/CD log analysis.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Architecture & Token Lifecycle
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
```mermaid
|
|
27
|
+
sequenceDiagram
|
|
28
|
+
participant PythonScript as Python Script / Daemon
|
|
29
|
+
participant SDK as LibreChatClient SDK
|
|
30
|
+
participant Disk as Local State File (session_tokens.json)
|
|
31
|
+
participant Server as LibreChat Backend
|
|
32
|
+
|
|
33
|
+
PythonScript->>SDK: send_agent_message(prompt)
|
|
34
|
+
SDK->>SDK: Check JWT Expiration (exp timestamp)
|
|
35
|
+
alt Access Token Expired or Missing
|
|
36
|
+
SDK->>Server: POST /api/auth/refresh (with refreshToken cookie)
|
|
37
|
+
Server-->>SDK: 200 OK + New Access Token + New Rotated RefreshToken
|
|
38
|
+
SDK->>Disk: Persist new tokens to session_tokens.json
|
|
39
|
+
end
|
|
40
|
+
SDK->>Server: POST /api/agents/chat/agents (Bearer Access Token)
|
|
41
|
+
Server-->>SDK: HTTP 200 OK (Agent Response Payload)
|
|
42
|
+
SDK-->>PythonScript: Return JSON Response
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
Clone the repository and install in editable mode:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/your-username/librechat-python-sdk.git
|
|
53
|
+
cd librechat-python-sdk
|
|
54
|
+
pip install -e .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To include optional DevOps folder monitoring dependencies:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .[devops]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
### 1. Environment Setup
|
|
68
|
+
|
|
69
|
+
Create a `.env` file from the provided template:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cp .env.example .env
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Fill in your LibreChat endpoint and browser session cookie:
|
|
76
|
+
|
|
77
|
+
```env
|
|
78
|
+
LIBRECHAT_BASE_URL=https://chat.example.com
|
|
79
|
+
LIBRECHAT_REFRESH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
80
|
+
LIBRECHAT_CONNECT_SID=s%3A...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Basic Usage
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import os
|
|
87
|
+
from dotenv import load_dotenv
|
|
88
|
+
from librechat_client import LibreChatClient
|
|
89
|
+
|
|
90
|
+
load_dotenv()
|
|
91
|
+
|
|
92
|
+
# Initialize resilient client
|
|
93
|
+
client = LibreChatClient(
|
|
94
|
+
base_url=os.getenv("LIBRECHAT_BASE_URL"),
|
|
95
|
+
refresh_token=os.getenv("LIBRECHAT_REFRESH_TOKEN"),
|
|
96
|
+
connect_sid=os.getenv("LIBRECHAT_CONNECT_SID")
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Send a prompt to the agent endpoint
|
|
100
|
+
response = client.send_agent_message(
|
|
101
|
+
prompt="Explain the benefits of Infrastructure-as-Code in DevSecOps."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print(response.get("text"))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## DevOps & GitOps Showcase Examples
|
|
110
|
+
|
|
111
|
+
The [`examples/`](examples/) directory includes real-world automation implementations:
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
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.
|
|
115
|
+
3. **[Basic Chat Example](examples/basic_chat.py)**: Minimal 15-line quickstart script.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Project Structure
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
librechat-python-sdk/
|
|
123
|
+
├── .env.example # Environment template
|
|
124
|
+
├── .gitignore # Production gitignore rules
|
|
125
|
+
├── LICENSE # MIT License
|
|
126
|
+
├── pyproject.toml # Python packaging metadata
|
|
127
|
+
├── README.md # Project documentation
|
|
128
|
+
├── requirements.txt # Package dependencies
|
|
129
|
+
├── librechat_client/
|
|
130
|
+
│ ├── __init__.py # SDK package exports
|
|
131
|
+
│ ├── client.py # Core LibreChat API client with RTR & retry logic
|
|
132
|
+
│ ├── exceptions.py # Custom exception hierarchy
|
|
133
|
+
│ └── utils.py # JWT decoding & helper utilities
|
|
134
|
+
└── examples/
|
|
135
|
+
├── basic_chat.py # Minimal usage script
|
|
136
|
+
├── devops_iac_auditor/ # GitOps security auditor daemon example
|
|
137
|
+
└── file_monitor/ # Resilient folder monitor daemon example
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -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
|
+
[](https://www.python.org/downloads/)
|
|
23
|
+
[](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,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
examples/basic_chat.py
|
|
5
|
+
examples/file_monitor/main.py
|
|
6
|
+
examples/file_monitor/tracker.py
|
|
7
|
+
examples/gitops_iac_auditor/main.py
|
|
8
|
+
librechat_client/__init__.py
|
|
9
|
+
librechat_client/client.py
|
|
10
|
+
librechat_client/exceptions.py
|
|
11
|
+
librechat_client/utils.py
|
|
12
|
+
librechat_python_sdk.egg-info/PKG-INFO
|
|
13
|
+
librechat_python_sdk.egg-info/SOURCES.txt
|
|
14
|
+
librechat_python_sdk.egg-info/dependency_links.txt
|
|
15
|
+
librechat_python_sdk.egg-info/requires.txt
|
|
16
|
+
librechat_python_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "librechat-python-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A resilient, auto-refreshing Python client SDK for LibreChat API endpoints featuring Refresh Token Rotation (RTR) and multi-day state persistence."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Open Source Developer" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Topic :: System :: Monitoring"
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"requests>=2.28.0",
|
|
23
|
+
"python-dotenv>=1.0.0"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
devops = [
|
|
28
|
+
"watchdog>=3.0.0"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["."]
|