Compare commits

...

2 Commits

Author SHA1 Message Date
Pavel
c740933782 Delete docsgpt_scanner.py 2025-03-30 11:45:33 +01:00
Pavel
6d3134c944 proxy for api-tool
-Only for api_tool for now, if this solution works well then implementation for other tools is required
- Need to check api keys creation with the current proxies
- Show connection string example at creation
- locale needs updates for other languages
2025-03-30 14:42:37 +04:00
20 changed files with 1005 additions and 8 deletions

View File

@@ -11,4 +11,7 @@ class AgentCreator:
agent_class = cls.agents.get(type.lower())
if not agent_class:
raise ValueError(f"No agent class found for type {type}")
config = kwargs.pop('config', None)
if isinstance(config, dict) and 'proxy_id' in config and 'proxy_id' not in kwargs:
kwargs['proxy_id'] = config['proxy_id']
return agent_class(*args, **kwargs)

View File

@@ -17,6 +17,7 @@ class BaseAgent:
api_key,
user_api_key=None,
decoded_token=None,
proxy_id=None,
):
self.endpoint = endpoint
self.llm = LLMCreator.create_llm(
@@ -30,6 +31,7 @@ class BaseAgent:
self.tools = []
self.tool_config = {}
self.tool_calls = []
self.proxy_id = proxy_id
def gen(self, *args, **kwargs) -> Generator[Dict, None, None]:
raise NotImplementedError('Method "gen" must be implemented in the child class')
@@ -41,6 +43,11 @@ class BaseAgent:
user_tools = user_tools_collection.find({"user": user, "status": True})
user_tools = list(user_tools)
tools_by_id = {str(tool["_id"]): tool for tool in user_tools}
if hasattr(self, 'proxy_id') and self.proxy_id:
for tool_id, tool in tools_by_id.items():
if 'config' not in tool:
tool['config'] = {}
tool['config']['proxy_id'] = self.proxy_id
return tools_by_id
def _build_tool_parameters(self, action):
@@ -126,6 +133,7 @@ class BaseAgent:
"method": tool_data["config"]["actions"][action_name]["method"],
"headers": headers,
"query_params": query_params,
"proxy_id": self.proxy_id,
}
if tool_data["name"] == "api_tool"
else tool_data["config"]

View File

@@ -18,9 +18,10 @@ class ClassicAgent(BaseAgent):
prompt="",
chat_history=None,
decoded_token=None,
proxy_id=None,
):
super().__init__(
endpoint, llm_name, gpt_model, api_key, user_api_key, decoded_token
endpoint, llm_name, gpt_model, api_key, user_api_key, decoded_token, proxy_id
)
self.user = decoded_token.get("sub")
self.prompt = prompt

View File

@@ -23,15 +23,43 @@ class APITool(Tool):
)
def _make_api_call(self, url, method, headers, query_params, body):
sanitized_headers = {}
for key, value in headers.items():
if isinstance(value, str):
sanitized_value = value.encode('latin-1', errors='ignore').decode('latin-1')
sanitized_headers[key] = sanitized_value
else:
sanitized_headers[key] = value
if query_params:
url = f"{url}?{requests.compat.urlencode(query_params)}"
if isinstance(body, dict):
body = json.dumps(body)
response = None
try:
print(f"Making API call: {method} {url} with body: {body}")
if body == "{}":
body = None
response = requests.request(method, url, headers=headers, data=body)
proxy_id = self.config.get("proxy_id", None)
request_kwargs = {
'method': method,
'url': url,
'headers': sanitized_headers,
'data': body
}
try:
if proxy_id:
from application.agents.tools.proxy_handler import apply_proxy_to_request
response = apply_proxy_to_request(
requests.request,
proxy_id=proxy_id,
**request_kwargs
)
else:
response = requests.request(**request_kwargs)
except ImportError:
response = requests.request(**request_kwargs)
response.raise_for_status()
content_type = response.headers.get(
"Content-Type", "application/json"

View File

@@ -0,0 +1,63 @@
import logging
import requests
from typing import Dict, Optional
from bson.objectid import ObjectId
from application.core.mongo_db import MongoDB
logger = logging.getLogger(__name__)
# Get MongoDB connection
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
proxies_collection = db["proxies"]
def get_proxy_config(proxy_id: str) -> Optional[Dict[str, str]]:
"""
Retrieve proxy configuration from the database.
Args:
proxy_id: The ID of the proxy configuration
Returns:
A dictionary with proxy configuration or None if not found
"""
if not proxy_id or proxy_id == "none":
return None
try:
if ObjectId.is_valid(proxy_id):
proxy_config = proxies_collection.find_one({"_id": ObjectId(proxy_id)})
if proxy_config and "connection" in proxy_config:
connection_str = proxy_config["connection"].strip()
if connection_str:
# Format proxy for requests library
return {
"http": connection_str,
"https": connection_str
}
return None
except Exception as e:
logger.error(f"Error retrieving proxy configuration: {e}")
return None
def apply_proxy_to_request(request_func, proxy_id=None, **kwargs):
"""
Apply proxy configuration to a requests function if available.
This is a minimal wrapper that doesn't change the function signature.
Args:
request_func: The requests function to call (e.g., requests.get, requests.post)
proxy_id: Optional proxy ID to use
**kwargs: Arguments to pass to the request function
Returns:
The response from the request
"""
if proxy_id:
proxy_config = get_proxy_config(proxy_id)
if proxy_config:
kwargs['proxies'] = proxy_config
logger.info(f"Using proxy for request")
return request_func(**kwargs)

View File

@@ -335,6 +335,9 @@ class Stream(Resource):
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"proxy_id": fields.String(
required=False, description="Proxy ID to use for API calls"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
@@ -376,6 +379,7 @@ class Stream(Resource):
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
proxy_id = data.get("proxy_id", None)
index = data.get("index", None)
chunks = int(data.get("chunks", 2))
@@ -386,6 +390,7 @@ class Stream(Resource):
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
prompt_id = data_key.get("prompt_id", "default")
proxy_id = data_key.get("proxy_id", None)
source = {"active_docs": data_key.get("source")}
retriever_name = data_key.get("retriever", retriever_name)
user_api_key = data["api_key"]
@@ -422,6 +427,7 @@ class Stream(Resource):
api_key=settings.API_KEY,
user_api_key=user_api_key,
prompt=prompt,
proxy_id=proxy_id,
chat_history=history,
decoded_token=decoded_token,
)
@@ -496,6 +502,9 @@ class Answer(Resource):
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"proxy_id": fields.String(
required=False, description="Proxy ID to use for API calls"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
@@ -527,6 +536,7 @@ class Answer(Resource):
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
proxy_id = data.get("proxy_id", None)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
@@ -535,6 +545,7 @@ class Answer(Resource):
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
prompt_id = data_key.get("prompt_id", "default")
proxy_id = data_key.get("proxy_id", None)
source = {"active_docs": data_key.get("source")}
retriever_name = data_key.get("retriever", retriever_name)
user_api_key = data["api_key"]
@@ -569,6 +580,7 @@ class Answer(Resource):
api_key=settings.API_KEY,
user_api_key=user_api_key,
prompt=prompt,
proxy_id=proxy_id,
chat_history=history,
decoded_token=decoded_token,
)

View File

@@ -27,6 +27,7 @@ db = mongo["docsgpt"]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
prompts_collection = db["prompts"]
proxies_collection = db["proxies"]
feedback_collection = db["feedback"]
api_key_collection = db["api_keys"]
token_usage_collection = db["token_usage"]
@@ -919,6 +920,183 @@ class UpdatePrompt(Resource):
return make_response(jsonify({"success": True}), 200)
@user_ns.route("/api/get_proxies")
class GetProxies(Resource):
@api.doc(description="Get all proxies for the user")
def get(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
try:
proxies = proxies_collection.find({"user": user})
list_proxies = [
{"id": "none", "name": "None", "type": "public"},
]
for proxy in proxies:
list_proxies.append(
{
"id": str(proxy["_id"]),
"name": proxy["name"],
"type": "private",
}
)
except Exception as err:
current_app.logger.error(f"Error retrieving proxies: {err}")
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify(list_proxies), 200)
@user_ns.route("/api/get_single_proxy")
class GetSingleProxy(Resource):
@api.doc(params={"id": "ID of the proxy"}, description="Get a single proxy by ID")
def get(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
proxy_id = request.args.get("id")
if not proxy_id:
return make_response(
jsonify({"success": False, "message": "ID is required"}), 400
)
try:
if proxy_id == "none":
return make_response(jsonify({"connection": ""}), 200)
proxy = proxies_collection.find_one(
{"_id": ObjectId(proxy_id), "user": user}
)
if not proxy:
return make_response(jsonify({"status": "not found"}), 404)
except Exception as err:
current_app.logger.error(f"Error retrieving proxy: {err}")
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"connection": proxy["connection"]}), 200)
@user_ns.route("/api/create_proxy")
class CreateProxy(Resource):
create_proxy_model = api.model(
"CreateProxyModel",
{
"connection": fields.String(
required=True, description="Connection string of the proxy"
),
"name": fields.String(required=True, description="Name of the proxy"),
},
)
@api.expect(create_proxy_model)
@api.doc(description="Create a new proxy")
def post(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
data = request.get_json()
required_fields = ["connection", "name"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
user = decoded_token.get("sub")
try:
resp = proxies_collection.insert_one(
{
"name": data["name"],
"connection": data["connection"],
"user": user,
}
)
new_id = str(resp.inserted_id)
except Exception as err:
current_app.logger.error(f"Error creating proxy: {err}")
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"id": new_id}), 200)
@user_ns.route("/api/delete_proxy")
class DeleteProxy(Resource):
delete_proxy_model = api.model(
"DeleteProxyModel",
{"id": fields.String(required=True, description="Proxy ID to delete")},
)
@api.expect(delete_proxy_model)
@api.doc(description="Delete a proxy by ID")
def post(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
data = request.get_json()
required_fields = ["id"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
# Don't allow deleting the 'none' proxy
if data["id"] == "none":
return make_response(jsonify({"success": False, "message": "Cannot delete default proxy"}), 400)
result = proxies_collection.delete_one({"_id": ObjectId(data["id"]), "user": user})
if result.deleted_count == 0:
return make_response(jsonify({"success": False, "message": "Proxy not found"}), 404)
except Exception as err:
current_app.logger.error(f"Error deleting proxy: {err}")
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"success": True}), 200)
@user_ns.route("/api/update_proxy")
class UpdateProxy(Resource):
update_proxy_model = api.model(
"UpdateProxyModel",
{
"id": fields.String(required=True, description="Proxy ID to update"),
"name": fields.String(required=True, description="New name of the proxy"),
"connection": fields.String(
required=True, description="New connection string of the proxy"
),
},
)
@api.expect(update_proxy_model)
@api.doc(description="Update an existing proxy")
def post(self):
decoded_token = request.decoded_token
if not decoded_token:
return make_response(jsonify({"success": False}), 401)
user = decoded_token.get("sub")
data = request.get_json()
required_fields = ["id", "name", "connection"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
# Don't allow updating the 'none' proxy
if data["id"] == "none":
return make_response(jsonify({"success": False, "message": "Cannot update default proxy"}), 400)
result = proxies_collection.update_one(
{"_id": ObjectId(data["id"]), "user": user},
{"$set": {"name": data["name"], "connection": data["connection"]}},
)
if result.modified_count == 0:
return make_response(jsonify({"success": False, "message": "Proxy not found"}), 404)
except Exception as err:
current_app.logger.error(f"Error updating proxy: {err}")
return make_response(jsonify({"success": False}), 400)
return make_response(jsonify({"success": True}), 200)
@user_ns.route("/api/get_api_keys")
class GetApiKeys(Resource):

View File

@@ -14,6 +14,7 @@ esutils==1.0.1
Flask==3.1.0
faiss-cpu==1.9.0.post1
flask-restx==1.3.0
gevent==24.11.1
google-genai==1.3.0
google-generativeai==0.8.3
gTTS==2.5.4
@@ -35,7 +36,7 @@ langchain-community==0.3.19
langchain-core==0.3.45
langchain-openai==0.3.8
langchain-text-splitters==0.3.6
langsmith==0.3.19
langsmith==0.3.15
lazy-object-proxy==1.10.0
lxml==5.3.1
markupsafe==3.0.2
@@ -65,7 +66,7 @@ py==1.11.0
pydantic==2.10.6
pydantic-core==2.27.2
pydantic-settings==2.7.1
pymongo==4.11.3
pymongo==4.10.1
pypdf==5.2.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1

View File

@@ -35,7 +35,7 @@ services:
worker:
build: ../application
command: celery -A application.app.celery worker -l INFO -B
command: celery -A application.app.celery worker -l INFO --pool=gevent -B
environment:
- API_KEY=$API_KEY
- EMBEDDINGS_KEY=$API_KEY

View File

@@ -13,6 +13,11 @@ const endpoints = {
DELETE_PROMPT: '/api/delete_prompt',
UPDATE_PROMPT: '/api/update_prompt',
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
PROXIES: '/api/get_proxies',
CREATE_PROXY: '/api/create_proxy',
DELETE_PROXY: '/api/delete_proxy',
UPDATE_PROXY: '/api/update_proxy',
SINGLE_PROXY: (id: string) => `/api/get_single_proxy?id=${id}`,
DELETE_PATH: (docPath: string) => `/api/delete_old?source_id=${docPath}`,
TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`,
MESSAGE_ANALYTICS: '/api/get_message_analytics',

View File

@@ -27,6 +27,16 @@ const userService = {
apiClient.post(endpoints.USER.UPDATE_PROMPT, data, token),
getSinglePrompt: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.SINGLE_PROMPT(id), token),
getProxies: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.PROXIES, token),
createProxy: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.CREATE_PROXY, data, token),
deleteProxy: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.DELETE_PROXY, data, token),
updateProxy: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.UPDATE_PROXY, data, token),
getSingleProxy: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.SINGLE_PROXY(id), token),
deletePath: (docPath: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.DELETE_PATH(docPath), token),
getTaskStatus: (task_id: string, token: string | null): Promise<any> =>

View File

@@ -11,6 +11,7 @@ export function handleFetchAnswer(
history: Array<any> = [],
conversationId: string | null,
promptId: string | null,
proxyId: string | null,
chunks: string,
token_limit: number,
): Promise<
@@ -44,6 +45,7 @@ export function handleFetchAnswer(
history: JSON.stringify(history),
conversation_id: conversationId,
prompt_id: promptId,
proxy_id: proxyId,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
@@ -82,6 +84,7 @@ export function handleFetchAnswerSteaming(
history: Array<any> = [],
conversationId: string | null,
promptId: string | null,
proxyId: string | null,
chunks: string,
token_limit: number,
onEvent: (event: MessageEvent) => void,
@@ -99,6 +102,7 @@ export function handleFetchAnswerSteaming(
history: JSON.stringify(history),
conversation_id: conversationId,
prompt_id: promptId,
proxy_id: proxyId,
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,

View File

@@ -43,6 +43,7 @@ export interface RetrievalPayload {
history: string;
conversation_id: string | null;
prompt_id?: string | null;
proxy_id?: string | null;
chunks: string;
token_limit: number;
isNoneDoc: boolean;

View File

@@ -47,6 +47,7 @@ export const fetchAnswer = createAsyncThunk<
state.conversation.queries,
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.proxy?.id ?? null,
state.preference.chunks,
state.preference.token_limit,
(event) => {
@@ -120,6 +121,7 @@ export const fetchAnswer = createAsyncThunk<
state.conversation.queries,
state.conversation.conversationId,
state.preference.prompt.id,
state.preference.proxy?.id ?? null,
state.preference.chunks,
state.preference.token_limit,
);

View File

@@ -41,6 +41,7 @@
"selectLanguage": "Select Language",
"chunks": "Chunks processed per query",
"prompt": "Active Prompt",
"proxy": "Active Proxy",
"deleteAllLabel": "Delete All Conversations",
"deleteAllBtn": "Delete All",
"addNew": "Add New",
@@ -205,6 +206,14 @@
"promptText": "Prompt Text",
"save": "Save",
"nameExists": "Name already exists"
},
"proxies": {
"addProxy": "Add Proxy",
"addDescription": "Add your custom proxy to query tools and save it to DocsGPT",
"editProxy": "Edit Proxy",
"proxyName": "Proxy Name",
"proxyProtocol": "Proxy Protocol",
"connectionString": "Connection String"
}
},
"sharedConv": {

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import Input from '../components/Input';
import { ActiveState } from '../models/misc';
@@ -19,7 +18,13 @@ export default function JWTModal({
if (modalState !== 'ACTIVE') return null;
return (
<WrapperModal className="p-4" isPerformingTask={true} close={() => {}}>
<WrapperModal
className="p-4"
isPerformingTask={true}
close={() => {
/* Modal close handler */
}}
>
<div className="mb-6">
<span className="text-lg text-jet dark:text-bright-gray">
Add JWT Token

View File

@@ -0,0 +1,279 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Input from '../components/Input';
import WrapperModal from '../modals/WrapperModal';
import { ActiveState } from '../models/misc';
function AddProxy({
setModalState,
handleAddProxy,
newProxyName,
setNewProxyName,
newProxyConnection,
setNewProxyConnection,
disableSave,
}: {
setModalState: (state: ActiveState) => void;
handleAddProxy?: () => void;
newProxyName: string;
setNewProxyName: (name: string) => void;
newProxyConnection: string;
setNewProxyConnection: (content: string) => void;
disableSave: boolean;
}) {
const { t } = useTranslation();
return (
<div>
<p className="mb-1 text-xl text-jet dark:text-bright-gray">
{t('modals.proxies.addProxy')}
</p>
<p className="mb-7 text-xs text-[#747474] dark:text-[#7F7F82]">
{t('modals.proxies.addDescription')}
</p>
<div>
<Input
placeholder={t('modals.proxies.proxyName')}
type="text"
className="mb-4"
value={newProxyName}
onChange={(e) => setNewProxyName(e.target.value)}
labelBgClassName="bg-white dark:bg-[#26272E]"
borderVariant="thin"
/>
<Input
placeholder={t('modals.proxies.proxyProtocol')}
type="text"
className="mb-4 opacity-70 cursor-not-allowed"
value="HTTP/S"
onChange={() => {
/* Protocol field is read-only */
}}
labelBgClassName="bg-white dark:bg-[#26272E]"
borderVariant="thin"
disabled={true}
/>
<Input
placeholder={t('modals.proxies.connectionString')}
type="text"
className="mb-4"
value={newProxyConnection}
onChange={(e) => setNewProxyConnection(e.target.value)}
labelBgClassName="bg-white dark:bg-[#26272E]"
borderVariant="thin"
/>
</div>
<div className="mt-6 flex flex-row-reverse">
<button
onClick={handleAddProxy}
className="rounded-3xl bg-purple-30 px-5 py-2 text-sm text-white transition-all hover:bg-violets-are-blue disabled:hover:bg-purple-30"
disabled={disableSave}
title={
disableSave && newProxyName ? t('modals.prompts.nameExists') : ''
}
>
{t('modals.prompts.save')}
</button>
</div>
</div>
);
}
function EditProxy({
setModalState,
handleEditProxy,
editProxyName,
setEditProxyName,
editProxyConnection,
setEditProxyConnection,
currentProxyEdit,
disableSave,
}: {
setModalState: (state: ActiveState) => void;
handleEditProxy?: (id: string, type: string) => void;
editProxyName: string;
setEditProxyName: (name: string) => void;
editProxyConnection: string;
setEditProxyConnection: (content: string) => void;
currentProxyEdit: { name: string; id: string; type: string };
disableSave: boolean;
}) {
const { t } = useTranslation();
return (
<div>
<div className="">
<p className="mb-1 text-xl text-jet dark:text-bright-gray">
{t('modals.proxies.editProxy')}
</p>
<p className="mb-7 text-xs text-[#747474] dark:text-[#7F7F82]">
{t('modals.proxies.addDescription')}
</p>
<div>
<Input
placeholder={t('modals.proxies.proxyName')}
type="text"
className="mb-4"
value={editProxyName}
onChange={(e) => setEditProxyName(e.target.value)}
labelBgClassName="bg-white dark:bg-charleston-green-2"
borderVariant="thin"
/>
<Input
placeholder={t('modals.proxies.proxyProtocol')}
type="text"
className="mb-4 opacity-70 cursor-not-allowed"
value="HTTP/S"
onChange={() => {
/* Protocol field is read-only */
}}
labelBgClassName="bg-white dark:bg-charleston-green-2"
borderVariant="thin"
disabled={true}
/>
<Input
placeholder={t('modals.proxies.connectionString')}
type="text"
className="mb-4"
value={editProxyConnection}
onChange={(e) => setEditProxyConnection(e.target.value)}
labelBgClassName="bg-white dark:bg-charleston-green-2"
borderVariant="thin"
/>
</div>
<div className="mt-6 flex flex-row-reverse gap-4">
<button
className={`rounded-3xl bg-purple-30 disabled:hover:bg-purple-30 hover:bg-violets-are-blue px-5 py-2 text-sm text-white transition-all ${
currentProxyEdit.type === 'public'
? 'cursor-not-allowed opacity-50'
: ''
}`}
onClick={() => {
handleEditProxy &&
handleEditProxy(currentProxyEdit.id, currentProxyEdit.type);
}}
disabled={currentProxyEdit.type === 'public' || disableSave}
title={
disableSave && editProxyName ? t('modals.prompts.nameExists') : ''
}
>
{t('modals.prompts.save')}
</button>
</div>
</div>
</div>
);
}
export default function ProxiesModal({
existingProxies,
modalState,
setModalState,
type,
newProxyName,
setNewProxyName,
newProxyConnection,
setNewProxyConnection,
editProxyName,
setEditProxyName,
editProxyConnection,
setEditProxyConnection,
currentProxyEdit,
handleAddProxy,
handleEditProxy,
}: {
existingProxies: { name: string; id: string; type: string }[];
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
type: 'ADD' | 'EDIT';
newProxyName: string;
setNewProxyName: (name: string) => void;
newProxyConnection: string;
setNewProxyConnection: (content: string) => void;
editProxyName: string;
setEditProxyName: (name: string) => void;
editProxyConnection: string;
setEditProxyConnection: (content: string) => void;
currentProxyEdit: { id: string; name: string; type: string };
handleAddProxy?: () => void;
handleEditProxy?: (id: string, type: string) => void;
}) {
const [disableSave, setDisableSave] = React.useState(true);
const { t } = useTranslation();
React.useEffect(() => {
// Check if fields are filled to enable/disable save button
if (type === 'ADD') {
const nameExists = existingProxies.some(
(proxy) => proxy.name.toLowerCase() === newProxyName.toLowerCase(),
);
setDisableSave(
newProxyName === '' || newProxyConnection === '' || nameExists,
);
} else {
const nameExists = existingProxies.some(
(proxy) =>
proxy.name.toLowerCase() === editProxyName.toLowerCase() &&
proxy.id !== currentProxyEdit.id,
);
setDisableSave(
editProxyName === '' || editProxyConnection === '' || nameExists,
);
}
}, [
newProxyName,
newProxyConnection,
editProxyName,
editProxyConnection,
type,
existingProxies,
currentProxyEdit,
]);
let view;
if (type === 'ADD') {
view = (
<AddProxy
setModalState={setModalState}
handleAddProxy={handleAddProxy}
newProxyName={newProxyName}
setNewProxyName={setNewProxyName}
newProxyConnection={newProxyConnection}
setNewProxyConnection={setNewProxyConnection}
disableSave={disableSave}
/>
);
} else if (type === 'EDIT') {
view = (
<EditProxy
setModalState={setModalState}
handleEditProxy={handleEditProxy}
editProxyName={editProxyName}
setEditProxyName={setEditProxyName}
editProxyConnection={editProxyConnection}
setEditProxyConnection={setEditProxyConnection}
currentProxyEdit={currentProxyEdit}
disableSave={disableSave}
/>
);
} else {
view = <></>;
}
return modalState === 'ACTIVE' ? (
<WrapperModal
close={() => {
setModalState('INACTIVE');
if (type === 'ADD') {
setNewProxyName('');
setNewProxyConnection('');
}
}}
className="sm:w-[512px] mt-24"
>
{view}
</WrapperModal>
) : null;
}

View File

@@ -11,6 +11,7 @@ import { ActiveState, Doc } from '../models/misc';
export interface Preference {
apiKey: string;
prompt: { name: string; id: string; type: string };
proxy: { name: string; id: string; type: string } | null;
chunks: string;
token_limit: number;
selectedDocs: Doc | null;
@@ -27,6 +28,7 @@ export interface Preference {
const initialState: Preference = {
apiKey: 'xxx',
prompt: { name: 'default', id: 'default', type: 'public' },
proxy: null,
chunks: '2',
token_limit: 2000,
selectedDocs: {
@@ -73,6 +75,9 @@ export const prefSlice = createSlice({
setPrompt: (state, action) => {
state.prompt = action.payload;
},
setProxy: (state, action) => {
state.proxy = action.payload;
},
setChunks: (state, action) => {
state.chunks = action.payload;
},
@@ -92,6 +97,7 @@ export const {
setConversations,
setToken,
setPrompt,
setProxy,
setChunks,
setTokenLimit,
setModalStateDeleteConv,
@@ -126,6 +132,16 @@ prefListenerMiddleware.startListening({
},
});
prefListenerMiddleware.startListening({
matcher: isAnyOf(setProxy),
effect: (action, listenerApi) => {
localStorage.setItem(
'DocsGPTProxy',
JSON.stringify((listenerApi.getState() as RootState).preference.proxy),
);
},
});
prefListenerMiddleware.startListening({
matcher: isAnyOf(setChunks),
effect: (action, listenerApi) => {
@@ -165,6 +181,7 @@ export const selectConversationId = (state: RootState) =>
state.conversation.conversationId;
export const selectToken = (state: RootState) => state.preference.token;
export const selectPrompt = (state: RootState) => state.preference.prompt;
export const selectProxy = (state: RootState) => state.preference.proxy;
export const selectChunks = (state: RootState) => state.preference.chunks;
export const selectTokenLimit = (state: RootState) =>
state.preference.token_limit;

View File

@@ -8,14 +8,17 @@ import { useDarkTheme } from '../hooks';
import {
selectChunks,
selectPrompt,
selectProxy,
selectToken,
selectTokenLimit,
setChunks,
setModalStateDeleteConv,
setPrompt,
setProxy,
setTokenLimit,
} from '../preferences/preferenceSlice';
import Prompts from './Prompts';
import Proxies from './Proxies';
export default function General() {
const {
@@ -48,6 +51,9 @@ export default function General() {
const [prompts, setPrompts] = React.useState<
{ name: string; id: string; type: string }[]
>([]);
const [proxies, setProxies] = React.useState<
{ name: string; id: string; type: string }[]
>([]);
const selectedChunks = useSelector(selectChunks);
const selectedTokenLimit = useSelector(selectTokenLimit);
const [isDarkTheme, toggleTheme] = useDarkTheme();
@@ -62,6 +68,44 @@ export default function General() {
: languageOptions[0],
);
const selectedPrompt = useSelector(selectPrompt);
const selectedProxy = useSelector(selectProxy);
React.useEffect(() => {
// Set default proxy state first (only if no stored preference exists)
const storedProxy = localStorage.getItem('DocsGPTProxy');
if (!storedProxy) {
const noneProxy = { name: 'None', id: 'none', type: 'public' };
dispatch(setProxy(noneProxy));
} else {
try {
const parsedProxy = JSON.parse(storedProxy);
dispatch(setProxy(parsedProxy));
} catch (e) {
console.error('Error parsing stored proxy', e);
// Fallback to None if parsing fails
dispatch(setProxy({ name: 'None', id: 'none', type: 'public' }));
}
}
// Fetch available proxies
const handleFetchProxies = async () => {
try {
const response = await userService.getProxies(token);
if (!response.ok) {
console.warn('Proxies API not implemented yet or failed to fetch');
return;
}
const proxiesData = await response.json();
if (proxiesData && Array.isArray(proxiesData)) {
// Filter out 'none' as we add it separately in the component
const filteredProxies = proxiesData.filter((p) => p.id !== 'none');
setProxies(filteredProxies);
}
} catch (error) {
console.error('Error fetching proxies:', error);
}
};
handleFetchProxies();
}, [token, dispatch]);
React.useEffect(() => {
const handleFetchPrompts = async () => {
@@ -77,12 +121,13 @@ export default function General() {
}
};
handleFetchPrompts();
}, []);
}, [token]);
React.useEffect(() => {
localStorage.setItem('docsgpt-locale', selectedLanguage?.value as string);
changeLanguage(selectedLanguage?.value);
}, [selectedLanguage, changeLanguage]);
return (
<div className="mt-12 flex flex-col gap-4">
{' '}
@@ -171,6 +216,16 @@ export default function General() {
setPrompts={setPrompts}
/>
</div>
<div className="flex flex-col gap-4">
<Proxies
proxies={proxies}
selectedProxy={selectedProxy}
onSelectProxy={(name, id, type) =>
dispatch(setProxy({ name: name, id: id, type: type }))
}
setProxies={setProxies}
/>
</div>
<hr className="border-t w-[calc(min(665px,100%))] my-4 border-silver dark:border-silver/40" />
<div className="flex flex-col gap-2">
<button

View File

@@ -0,0 +1,316 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import { ActiveState } from '../models/misc';
import ProxiesModal from '../preferences/ProxiesModal';
import { selectToken } from '../preferences/preferenceSlice';
export interface ProxyProps {
proxies: { name: string; id: string; type: string }[];
selectedProxy: {
name: string;
id: string;
type: string;
} | null;
onSelectProxy: (name: string, id: string, type: string) => void;
setProxies: React.Dispatch<
React.SetStateAction<{ name: string; id: string; type: string }[]>
>;
}
export default function Proxies({
proxies,
selectedProxy,
onSelectProxy,
setProxies,
}: ProxyProps) {
const handleSelectProxy = ({
name,
id,
type,
}: {
name: string;
id: string;
type: string;
}) => {
setEditProxyName(name);
onSelectProxy(name, id, type);
};
const token = useSelector(selectToken);
const [newProxyName, setNewProxyName] = React.useState('');
const [newProxyConnection, setNewProxyConnection] = React.useState('');
const [editProxyName, setEditProxyName] = React.useState('');
const [editProxyConnection, setEditProxyConnection] = React.useState('');
const [currentProxyEdit, setCurrentProxyEdit] = React.useState({
id: '',
name: '',
type: '',
});
const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD');
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
const { t } = useTranslation();
const handleAddProxy = async () => {
try {
const response = await userService.createProxy(
{
name: newProxyName,
connection: newProxyConnection,
},
token,
);
if (!response.ok) {
throw new Error('Failed to add proxy');
}
const newProxy = await response.json();
const newProxyObject = {
name: newProxyName,
id: newProxy.id,
type: 'private',
};
console.log(
'Before selecting new proxy:',
newProxyName,
newProxy.id,
'private',
);
if (setProxies) {
const updatedProxies = [...proxies, newProxyObject];
setProxies(updatedProxies);
console.log('Updated proxies list:', updatedProxies);
}
setModalState('INACTIVE');
onSelectProxy(newProxyName, newProxy.id, 'private');
setNewProxyName('');
setNewProxyConnection('');
} catch (error) {
console.error(error);
// Fallback to just adding to the local state if API doesn't exist yet
const newId = `proxy_${Date.now()}`;
if (setProxies) {
// Store connection string in localStorage for local fallback
localStorage.setItem(`proxy_connection_${newId}`, newProxyConnection);
setProxies([
...proxies,
{ name: newProxyName, id: newId, type: 'private' },
]);
}
setModalState('INACTIVE');
onSelectProxy(newProxyName, newId, 'private');
setNewProxyName('');
setNewProxyConnection('');
}
};
const handleDeleteProxy = (id: string) => {
// We don't delete the "none" proxy
if (id === 'none') return;
userService
.deleteProxy({ id }, token)
.then((response) => {
if (response.ok) {
// Remove from local state after successful deletion
setProxies(proxies.filter((proxy) => proxy.id !== id));
// Also remove any locally stored connection string
localStorage.removeItem(`proxy_connection_${id}`);
// If we deleted the currently selected proxy, switch to "None"
if (selectedProxy && selectedProxy.id === id) {
onSelectProxy('None', 'none', 'public');
}
} else {
console.warn('Failed to delete proxy');
}
})
.catch((error) => {
console.error(error);
});
};
const handleFetchProxyConnection = async (id: string) => {
try {
// We don't need to fetch connection for the "none" proxy
if (id === 'none') {
setEditProxyConnection('');
return;
}
// Check if this is a locally stored proxy (for API fallback)
const localConnection = localStorage.getItem(`proxy_connection_${id}`);
if (localConnection) {
setEditProxyConnection(localConnection);
return;
}
// Otherwise proceed with API call
const response = await userService.getSingleProxy(id, token);
if (!response.ok) {
throw new Error('Failed to fetch proxy connection');
}
const proxyData = await response.json();
setEditProxyConnection(proxyData.connection);
} catch (error) {
console.error(error);
// Set empty string instead of a placeholder
setEditProxyConnection('');
}
};
const handleSaveChanges = (id: string, type: string) => {
userService
.updateProxy(
{
id: id,
name: editProxyName,
connection: editProxyConnection,
},
token,
)
.then((response) => {
if (!response.ok) {
// If API doesn't exist yet, just handle locally
console.warn('API not implemented yet');
// Store connection string in localStorage
localStorage.setItem(`proxy_connection_${id}`, editProxyConnection);
}
if (setProxies) {
const existingProxyIndex = proxies.findIndex(
(proxy) => proxy.id === id,
);
if (existingProxyIndex === -1) {
setProxies([
...proxies,
{ name: editProxyName, id: id, type: type },
]);
} else {
const updatedProxies = [...proxies];
updatedProxies[existingProxyIndex] = {
name: editProxyName,
id: id,
type: type,
};
setProxies(updatedProxies);
}
}
setModalState('INACTIVE');
onSelectProxy(editProxyName, id, type);
})
.catch((error) => {
console.error(error);
// Handle locally if API fails
// Store connection string in localStorage
localStorage.setItem(`proxy_connection_${id}`, editProxyConnection);
if (setProxies) {
const existingProxyIndex = proxies.findIndex(
(proxy) => proxy.id === id,
);
if (existingProxyIndex !== -1) {
const updatedProxies = [...proxies];
updatedProxies[existingProxyIndex] = {
name: editProxyName,
id: id,
type: type,
};
setProxies(updatedProxies);
}
}
setModalState('INACTIVE');
onSelectProxy(editProxyName, id, type);
});
};
// Split proxies into 'None' and custom proxies
const customProxies = proxies.filter(
(p) => p.id !== 'none' && p.name !== 'None',
);
// Create options array with None first
const noneProxy = { name: 'None', id: 'none', type: 'public' };
const allProxies = [noneProxy, ...customProxies];
// Ensure valid selectedProxy or default to None
const finalSelectedProxy =
selectedProxy && selectedProxy.id !== 'from-url'
? selectedProxy
: noneProxy;
// Check if the current proxy is the None proxy
const isNoneSelected =
!finalSelectedProxy ||
finalSelectedProxy.id === 'none' ||
finalSelectedProxy.name === 'None';
return (
<>
<div>
<div className="flex flex-col gap-4">
<p className="font-medium dark:text-bright-gray">
{t('settings.general.proxy')}
</p>
<div className="flex flex-row justify-start items-baseline gap-6">
<Dropdown
options={allProxies}
selectedValue={finalSelectedProxy.name}
placeholder="None"
onSelect={handleSelectProxy}
size="w-56"
rounded="3xl"
border="border"
showEdit={!isNoneSelected}
showDelete={!isNoneSelected}
onEdit={({
id,
name,
type,
}: {
id: string;
name: string;
type: string;
}) => {
setModalType('EDIT');
setEditProxyName(name);
handleFetchProxyConnection(id);
setCurrentProxyEdit({ id: id, name: name, type: type });
setModalState('ACTIVE');
}}
onDelete={(id: string) => {
handleDeleteProxy(id);
}}
/>
<button
className="rounded-3xl w-20 h-10 text-sm border border-solid border-violets-are-blue text-violets-are-blue transition-colors hover:text-white hover:bg-violets-are-blue"
onClick={() => {
setModalType('ADD');
setNewProxyName('');
setNewProxyConnection('');
setModalState('ACTIVE');
}}
>
{t('settings.general.add')}
</button>
</div>
</div>
</div>
{modalState === 'ACTIVE' && (
<ProxiesModal
existingProxies={proxies}
type={modalType}
modalState={modalState}
setModalState={setModalState}
newProxyName={newProxyName}
setNewProxyName={setNewProxyName}
newProxyConnection={newProxyConnection}
setNewProxyConnection={setNewProxyConnection}
editProxyName={editProxyName}
setEditProxyName={setEditProxyName}
editProxyConnection={editProxyConnection}
setEditProxyConnection={setEditProxyConnection}
currentProxyEdit={currentProxyEdit}
handleAddProxy={handleAddProxy}
handleEditProxy={handleSaveChanges}
/>
)}
</>
);
}