Compare commits
27 Commits
github-fix
...
sharepoint
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c46d8a094 | ||
|
|
8fb945ab09 | ||
|
|
835d71727c | ||
|
|
ce32dd2907 | ||
|
|
72bc24a490 | ||
|
|
d6c49bdbf0 | ||
|
|
1805292528 | ||
|
|
d09ce7e1f7 | ||
|
|
a8d2024791 | ||
|
|
f0b954dbfb | ||
|
|
065939302b | ||
|
|
5fa87db9e7 | ||
|
|
cc54cea783 | ||
|
|
50bee7c2b0 | ||
|
|
d9f0072112 | ||
|
|
e7b15b316e | ||
|
|
a4507008c1 | ||
|
|
c5ba85f929 | ||
|
|
2e636bd67e | ||
|
|
4a039f1abf | ||
|
|
434d8e2070 | ||
|
|
2b73c0c9a0 | ||
|
|
da62133d21 | ||
|
|
160ad2dc79 | ||
|
|
0ec86c2c71 | ||
|
|
03452ffd9f | ||
|
|
8edb6dcf2a |
@@ -6,4 +6,17 @@ VITE_API_STREAMING=true
|
||||
OPENAI_API_BASE=
|
||||
OPENAI_API_VERSION=
|
||||
AZURE_DEPLOYMENT_NAME=
|
||||
AZURE_EMBEDDINGS_DEPLOYMENT_NAME=
|
||||
AZURE_EMBEDDINGS_DEPLOYMENT_NAME=
|
||||
|
||||
#Azure AD Application (client) ID
|
||||
MICROSOFT_CLIENT_ID=your-azure-ad-client-id
|
||||
#Azure AD Application client secret
|
||||
MICROSOFT_CLIENT_SECRET=your-azure-ad-client-secret
|
||||
#Azure AD Tenant ID (or 'common' for multi-tenant)
|
||||
MICROSOFT_TENANT_ID=your-azure-ad-tenant-id
|
||||
#If you are using a Microsoft Entra ID tenant,
|
||||
#configure the AUTHORITY variable as
|
||||
#"https://login.microsoftonline.com/TENANT_GUID"
|
||||
#or "https://login.microsoftonline.com/contoso.onmicrosoft.com".
|
||||
#Alternatively, use "https://login.microsoftonline.com/common" for multi-tenant app.
|
||||
MICROSOFT_AUTHORITY=https://{tenentId}.ciamlogin.com/{tenentId}
|
||||
|
||||
6
.github/workflows/pytest.yml
vendored
@@ -16,15 +16,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-cov
|
||||
cd application
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
cd ../tests
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Test with pytest and generate coverage report
|
||||
run: |
|
||||
python -m pytest --cov=application --cov-report=xml
|
||||
python -m pytest --cov=application --cov-report=xml --cov-report=term-missing
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: github.event_name == 'pull_request' && matrix.python-version == '3.12'
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
- [x] MCP support (August 2025)
|
||||
- [x] Google Drive integration (September 2025)
|
||||
- [x] Add OAuth 2.0 authentication for MCP (September 2025)
|
||||
- [ ] Sharepoint integration (October 2025)
|
||||
- [ ] SharePoint integration (October 2025)
|
||||
- [ ] Deep Agents (October 2025)
|
||||
- [ ] Agent scheduling
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ class BaseAgent(ABC):
|
||||
decoded_token: Optional[Dict] = None,
|
||||
attachments: Optional[List[Dict]] = None,
|
||||
json_schema: Optional[Dict] = None,
|
||||
limited_token_mode: Optional[bool] = False,
|
||||
token_limit: Optional[int] = settings.DEFAULT_AGENT_LIMITS["token_limit"],
|
||||
limited_request_mode: Optional[bool] = False,
|
||||
request_limit: Optional[int] = settings.DEFAULT_AGENT_LIMITS["request_limit"],
|
||||
):
|
||||
self.endpoint = endpoint
|
||||
self.llm_name = llm_name
|
||||
@@ -38,7 +42,7 @@ class BaseAgent(ABC):
|
||||
self.user_api_key = user_api_key
|
||||
self.prompt = prompt
|
||||
self.decoded_token = decoded_token or {}
|
||||
self.user: str = decoded_token.get("sub")
|
||||
self.user: str = self.decoded_token.get("sub")
|
||||
self.tool_config: Dict = {}
|
||||
self.tools: List[Dict] = []
|
||||
self.tool_calls: List[Dict] = []
|
||||
@@ -54,6 +58,10 @@ class BaseAgent(ABC):
|
||||
)
|
||||
self.attachments = attachments or []
|
||||
self.json_schema = json_schema
|
||||
self.limited_token_mode = limited_token_mode
|
||||
self.token_limit = token_limit
|
||||
self.limited_request_mode = limited_request_mode
|
||||
self.request_limit = request_limit
|
||||
|
||||
@log_activity()
|
||||
def gen(
|
||||
|
||||
@@ -20,20 +20,24 @@ class ToolActionParser:
|
||||
try:
|
||||
call_args = json.loads(call.arguments)
|
||||
tool_parts = call.name.split("_")
|
||||
|
||||
|
||||
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
|
||||
if len(tool_parts) < 2:
|
||||
logger.warning(f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id")
|
||||
logger.warning(
|
||||
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
|
||||
)
|
||||
return None, None, None
|
||||
|
||||
|
||||
tool_id = tool_parts[-1]
|
||||
action_name = "_".join(tool_parts[:-1])
|
||||
|
||||
|
||||
# Validate that tool_id looks like a numerical ID
|
||||
if not tool_id.isdigit():
|
||||
logger.warning(f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call.")
|
||||
|
||||
except (AttributeError, TypeError) as e:
|
||||
logger.warning(
|
||||
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
||||
)
|
||||
|
||||
except (AttributeError, TypeError, json.JSONDecodeError) as e:
|
||||
logger.error(f"Error parsing OpenAI LLM call: {e}")
|
||||
return None, None, None
|
||||
return tool_id, action_name, call_args
|
||||
@@ -42,19 +46,23 @@ class ToolActionParser:
|
||||
try:
|
||||
call_args = call.arguments
|
||||
tool_parts = call.name.split("_")
|
||||
|
||||
|
||||
# If the tool name doesn't contain an underscore, it's likely a hallucinated tool
|
||||
if len(tool_parts) < 2:
|
||||
logger.warning(f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id")
|
||||
logger.warning(
|
||||
f"Invalid tool name format: {call.name}. Expected format: action_name_tool_id"
|
||||
)
|
||||
return None, None, None
|
||||
|
||||
|
||||
tool_id = tool_parts[-1]
|
||||
action_name = "_".join(tool_parts[:-1])
|
||||
|
||||
|
||||
# Validate that tool_id looks like a numerical ID
|
||||
if not tool_id.isdigit():
|
||||
logger.warning(f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call.")
|
||||
|
||||
logger.warning(
|
||||
f"Tool ID '{tool_id}' is not numerical. This might be a hallucinated tool call."
|
||||
)
|
||||
|
||||
except (AttributeError, TypeError) as e:
|
||||
logger.error(f"Error parsing Google LLM call: {e}")
|
||||
return None, None, None
|
||||
|
||||
@@ -72,6 +72,9 @@ class AnswerResource(Resource, BaseAnswerResource):
|
||||
agent = processor.create_agent()
|
||||
retriever = processor.create_retriever()
|
||||
|
||||
if error := self.check_usage(processor.agent_config):
|
||||
return error
|
||||
|
||||
stream = self.complete_stream(
|
||||
question=data["question"],
|
||||
agent=agent,
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
from typing import Any, Dict, Generator, List, Optional
|
||||
|
||||
from flask import Response
|
||||
from flask import Response, make_response, jsonify
|
||||
from flask_restx import Namespace
|
||||
|
||||
from application.api.answer.services.conversation_service import ConversationService
|
||||
@@ -25,6 +25,7 @@ class BaseAnswerResource:
|
||||
def __init__(self):
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo[settings.MONGO_DB_NAME]
|
||||
self.db = db
|
||||
self.user_logs_collection = db["user_logs"]
|
||||
self.gpt_model = get_gpt_model()
|
||||
self.conversation_service = ConversationService()
|
||||
@@ -40,6 +41,88 @@ class BaseAnswerResource:
|
||||
return missing_fields
|
||||
return None
|
||||
|
||||
def check_usage(
|
||||
self, agent_config: Dict
|
||||
) -> Optional[Response]:
|
||||
"""Check if there is a usage limit and if it is exceeded
|
||||
|
||||
Args:
|
||||
agent_config: The config dict of agent instance
|
||||
|
||||
Returns:
|
||||
None or Response if either of limits exceeded.
|
||||
|
||||
"""
|
||||
api_key = agent_config.get("user_api_key")
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
agents_collection = self.db["agents"]
|
||||
agent = agents_collection.find_one({"key": api_key})
|
||||
|
||||
if not agent:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Invalid API key."
|
||||
}
|
||||
),
|
||||
401
|
||||
)
|
||||
|
||||
limited_token_mode = agent.get("limited_token_mode", False)
|
||||
limited_request_mode = agent.get("limited_request_mode", False)
|
||||
token_limit = int(agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]))
|
||||
request_limit = int(agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]))
|
||||
|
||||
token_usage_collection = self.db["token_usage"]
|
||||
|
||||
end_date = datetime.datetime.now()
|
||||
start_date = end_date - datetime.timedelta(hours=24)
|
||||
|
||||
match_query = {
|
||||
"timestamp": {"$gte": start_date, "$lte": end_date},
|
||||
"api_key": api_key
|
||||
}
|
||||
|
||||
if limited_token_mode:
|
||||
token_pipeline = [
|
||||
{"$match": match_query},
|
||||
{
|
||||
"$group": {
|
||||
"_id": None,
|
||||
"total_tokens": {"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}}
|
||||
}
|
||||
}
|
||||
]
|
||||
token_result = list(token_usage_collection.aggregate(token_pipeline))
|
||||
daily_token_usage = token_result[0]["total_tokens"] if token_result else 0
|
||||
else:
|
||||
daily_token_usage = 0
|
||||
|
||||
if limited_request_mode:
|
||||
daily_request_usage = token_usage_collection.count_documents(match_query)
|
||||
else:
|
||||
daily_request_usage = 0
|
||||
|
||||
if not limited_token_mode and not limited_request_mode:
|
||||
return None
|
||||
elif limited_token_mode and token_limit > daily_token_usage:
|
||||
return None
|
||||
elif limited_request_mode and request_limit > daily_request_usage:
|
||||
return None
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Exceeding usage limit, please try again later."
|
||||
}
|
||||
),
|
||||
429, # too many requests
|
||||
)
|
||||
|
||||
def complete_stream(
|
||||
self,
|
||||
question: str,
|
||||
@@ -193,6 +276,43 @@ class BaseAnswerResource:
|
||||
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
except GeneratorExit:
|
||||
# Client aborted the connection
|
||||
logger.info(
|
||||
f"Stream aborted by client for question: {question[:50]}... "
|
||||
)
|
||||
# Save partial response to database before exiting
|
||||
if should_save_conversation and response_full:
|
||||
try:
|
||||
if isNoneDoc:
|
||||
for doc in source_log_docs:
|
||||
doc["source"] = "None"
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_PROVIDER,
|
||||
api_key=settings.API_KEY,
|
||||
user_api_key=user_api_key,
|
||||
decoded_token=decoded_token,
|
||||
)
|
||||
self.conversation_service.save_conversation(
|
||||
conversation_id,
|
||||
question,
|
||||
response_full,
|
||||
thought,
|
||||
source_log_docs,
|
||||
tool_calls,
|
||||
llm,
|
||||
self.gpt_model,
|
||||
decoded_token,
|
||||
index=index,
|
||||
api_key=user_api_key,
|
||||
agent_id=agent_id,
|
||||
is_shared_usage=is_shared_usage,
|
||||
shared_token=shared_token,
|
||||
attachment_ids=attachment_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving partial response: {str(e)}", exc_info=True)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in stream: {str(e)}", exc_info=True)
|
||||
data = json.dumps(
|
||||
|
||||
@@ -76,6 +76,9 @@ class StreamResource(Resource, BaseAnswerResource):
|
||||
agent = processor.create_agent()
|
||||
retriever = processor.create_retriever()
|
||||
|
||||
if error := self.check_usage(processor.agent_config):
|
||||
return error
|
||||
|
||||
return Response(
|
||||
self.complete_stream(
|
||||
question=data["question"],
|
||||
|
||||
@@ -23,15 +23,9 @@ from application.core.settings import settings
|
||||
from application.api import api
|
||||
|
||||
|
||||
from application.utils import (
|
||||
check_required_fields
|
||||
)
|
||||
|
||||
|
||||
from application.parser.connectors.connector_creator import ConnectorCreator
|
||||
|
||||
|
||||
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo[settings.MONGO_DB_NAME]
|
||||
sources_collection = db["sources"]
|
||||
@@ -43,185 +37,6 @@ api.add_namespace(connectors_ns)
|
||||
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/upload")
|
||||
class UploadConnector(Resource):
|
||||
@api.expect(
|
||||
api.model(
|
||||
"ConnectorUploadModel",
|
||||
{
|
||||
"user": fields.String(required=True, description="User ID"),
|
||||
"source": fields.String(
|
||||
required=True, description="Source type (google_drive, github, etc.)"
|
||||
),
|
||||
"name": fields.String(required=True, description="Job name"),
|
||||
"data": fields.String(required=True, description="Configuration data"),
|
||||
"repo_url": fields.String(description="GitHub repository URL"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.doc(
|
||||
description="Uploads connector source for vectorization",
|
||||
)
|
||||
def post(self):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
data = request.form
|
||||
required_fields = ["user", "source", "name", "data"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
try:
|
||||
config = json.loads(data["data"])
|
||||
source_data = None
|
||||
sync_frequency = config.get("sync_frequency", "never")
|
||||
|
||||
if data["source"] == "github":
|
||||
source_data = config.get("repo_url")
|
||||
elif data["source"] in ["crawler", "url"]:
|
||||
source_data = config.get("url")
|
||||
elif data["source"] == "reddit":
|
||||
source_data = config
|
||||
elif data["source"] in ConnectorCreator.get_supported_connectors():
|
||||
session_token = config.get("session_token")
|
||||
if not session_token:
|
||||
return make_response(jsonify({
|
||||
"success": False,
|
||||
"error": f"Missing session_token in {data['source']} configuration"
|
||||
}), 400)
|
||||
|
||||
file_ids = config.get("file_ids", [])
|
||||
if isinstance(file_ids, str):
|
||||
file_ids = [id.strip() for id in file_ids.split(',') if id.strip()]
|
||||
elif not isinstance(file_ids, list):
|
||||
file_ids = []
|
||||
|
||||
folder_ids = config.get("folder_ids", [])
|
||||
if isinstance(folder_ids, str):
|
||||
folder_ids = [id.strip() for id in folder_ids.split(',') if id.strip()]
|
||||
elif not isinstance(folder_ids, list):
|
||||
folder_ids = []
|
||||
|
||||
config["file_ids"] = file_ids
|
||||
config["folder_ids"] = folder_ids
|
||||
|
||||
task = ingest_connector_task.delay(
|
||||
job_name=data["name"],
|
||||
user=decoded_token.get("sub"),
|
||||
source_type=data["source"],
|
||||
session_token=session_token,
|
||||
file_ids=file_ids,
|
||||
folder_ids=folder_ids,
|
||||
recursive=config.get("recursive", False),
|
||||
retriever=config.get("retriever", "classic"),
|
||||
sync_frequency=sync_frequency
|
||||
)
|
||||
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
|
||||
task = ingest_connector_task.delay(
|
||||
source_data=source_data,
|
||||
job_name=data["name"],
|
||||
user=decoded_token.get("sub"),
|
||||
loader=data["source"],
|
||||
sync_frequency=sync_frequency
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(
|
||||
f"Error uploading connector source: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/task_status")
|
||||
class ConnectorTaskStatus(Resource):
|
||||
task_status_model = api.model(
|
||||
"ConnectorTaskStatusModel",
|
||||
{"task_id": fields.String(required=True, description="Task ID")},
|
||||
)
|
||||
|
||||
@api.expect(task_status_model)
|
||||
@api.doc(description="Get connector task status")
|
||||
def get(self):
|
||||
task_id = request.args.get("task_id")
|
||||
if not task_id:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Task ID is required"}), 400
|
||||
)
|
||||
try:
|
||||
from application.celery_init import celery
|
||||
|
||||
task = celery.AsyncResult(task_id)
|
||||
task_meta = task.info
|
||||
print(f"Task status: {task.status}")
|
||||
if not isinstance(
|
||||
task_meta, (dict, list, str, int, float, bool, type(None))
|
||||
):
|
||||
task_meta = str(task_meta)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting task status: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"status": task.status, "result": task_meta}), 200)
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/sources")
|
||||
class ConnectorSources(Resource):
|
||||
@api.doc(description="Get connector sources")
|
||||
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:
|
||||
sources = sources_collection.find({"user": user, "type": "connector:file"}).sort("date", -1)
|
||||
connector_sources = []
|
||||
for source in sources:
|
||||
connector_sources.append({
|
||||
"id": str(source["_id"]),
|
||||
"name": source.get("name"),
|
||||
"date": source.get("date"),
|
||||
"type": source.get("type"),
|
||||
"source": source.get("source"),
|
||||
"tokens": source.get("tokens", ""),
|
||||
"retriever": source.get("retriever", "classic"),
|
||||
"syncFrequency": source.get("sync_frequency", ""),
|
||||
})
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error retrieving connector sources: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify(connector_sources), 200)
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/delete")
|
||||
class DeleteConnectorSource(Resource):
|
||||
@api.doc(
|
||||
description="Delete a connector source",
|
||||
params={"source_id": "The source ID to delete"},
|
||||
)
|
||||
def delete(self):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
source_id = request.args.get("source_id")
|
||||
if not source_id:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "source_id is required"}), 400
|
||||
)
|
||||
try:
|
||||
result = sources_collection.delete_one(
|
||||
{"_id": ObjectId(source_id), "user": decoded_token.get("sub")}
|
||||
)
|
||||
if result.deleted_count == 0:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Source not found"}), 404
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(
|
||||
f"Error deleting connector source: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"success": True}), 200)
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/auth")
|
||||
class ConnectorAuth(Resource):
|
||||
@api.doc(description="Get connector OAuth authorization URL", params={"provider": "Connector provider (e.g., google_drive)"})
|
||||
@@ -298,10 +113,14 @@ class ConnectorsCallback(Resource):
|
||||
session_token = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
credentials = auth.create_credentials_from_token_info(token_info)
|
||||
service = auth.build_drive_service(credentials)
|
||||
user_info = service.about().get(fields="user").execute()
|
||||
user_email = user_info.get('user', {}).get('emailAddress', 'Connected User')
|
||||
if provider == "google_drive":
|
||||
credentials = auth.create_credentials_from_token_info(token_info)
|
||||
service = auth.build_drive_service(credentials)
|
||||
user_info = service.about().get(fields="user").execute()
|
||||
user_email = user_info.get('user', {}).get('emailAddress', 'Connected User')
|
||||
else:
|
||||
user_email = token_info.get('user_info', {}).get('email', 'Connected User')
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"Could not get user info: {e}")
|
||||
user_email = 'Connected User'
|
||||
@@ -337,27 +156,6 @@ class ConnectorsCallback(Resource):
|
||||
return redirect("/api/connectors/callback-status?status=error&message=Authentication+failed.+Please+try+again+and+make+sure+to+grant+all+requested+permissions.")
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/refresh")
|
||||
class ConnectorRefresh(Resource):
|
||||
@api.expect(api.model("ConnectorRefreshModel", {"provider": fields.String(required=True), "refresh_token": fields.String(required=True)}))
|
||||
@api.doc(description="Refresh connector access token")
|
||||
def post(self):
|
||||
try:
|
||||
data = request.get_json()
|
||||
provider = data.get('provider')
|
||||
refresh_token = data.get('refresh_token')
|
||||
|
||||
if not provider or not refresh_token:
|
||||
return make_response(jsonify({"success": False, "error": "provider and refresh_token are required"}), 400)
|
||||
|
||||
auth = ConnectorCreator.create_auth(provider)
|
||||
token_info = auth.refresh_access_token(refresh_token)
|
||||
return make_response(jsonify({"success": True, "token_info": token_info}), 200)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error refreshing token for connector: {e}")
|
||||
return make_response(jsonify({"success": False, "error": str(e)}), 500)
|
||||
|
||||
|
||||
@connectors_ns.route("/api/connectors/files")
|
||||
class ConnectorFiles(Resource):
|
||||
@api.expect(api.model("ConnectorFilesModel", {
|
||||
|
||||
@@ -10,6 +10,7 @@ from flask import current_app, jsonify, make_response, request
|
||||
from flask_restx import fields, Namespace, Resource
|
||||
|
||||
from application.api import api
|
||||
from application.core.settings import settings
|
||||
from application.api.user.base import (
|
||||
agents_collection,
|
||||
db,
|
||||
@@ -74,6 +75,10 @@ class GetAgent(Resource):
|
||||
"agent_type": agent.get("agent_type", ""),
|
||||
"status": agent.get("status", ""),
|
||||
"json_schema": agent.get("json_schema"),
|
||||
"limited_token_mode": agent.get("limited_token_mode", False),
|
||||
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"limited_request_mode": agent.get("limited_request_mode", False),
|
||||
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
@@ -143,6 +148,10 @@ class GetAgents(Resource):
|
||||
"agent_type": agent.get("agent_type", ""),
|
||||
"status": agent.get("status", ""),
|
||||
"json_schema": agent.get("json_schema"),
|
||||
"limited_token_mode": agent.get("limited_token_mode", False),
|
||||
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"limited_request_mode": agent.get("limited_request_mode", False),
|
||||
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
@@ -199,6 +208,22 @@ class CreateAgent(Resource):
|
||||
required=False,
|
||||
description="JSON schema for enforcing structured output format",
|
||||
),
|
||||
"limited_token_mode": fields.Boolean(
|
||||
required=False,
|
||||
description="Whether the agent is in limited token mode"
|
||||
),
|
||||
"token_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Token limit for the agent in limited mode"
|
||||
),
|
||||
"limited_request_mode": fields.Boolean(
|
||||
required=False,
|
||||
description="Whether the agent is in limited request mode"
|
||||
),
|
||||
"request_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Request limit for the agent in limited mode"
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -344,6 +369,10 @@ class CreateAgent(Resource):
|
||||
"agent_type": data.get("agent_type", ""),
|
||||
"status": data.get("status"),
|
||||
"json_schema": data.get("json_schema"),
|
||||
"limited_token_mode": data.get("limited_token_mode", False),
|
||||
"token_limit": data.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"limited_request_mode": data.get("limited_request_mode", False),
|
||||
"request_limit": data.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"createdAt": datetime.datetime.now(datetime.timezone.utc),
|
||||
"updatedAt": datetime.datetime.now(datetime.timezone.utc),
|
||||
"lastUsedAt": None,
|
||||
@@ -399,6 +428,22 @@ class UpdateAgent(Resource):
|
||||
required=False,
|
||||
description="JSON schema for enforcing structured output format",
|
||||
),
|
||||
"limited_token_mode": fields.Boolean(
|
||||
required=False,
|
||||
description="Whether the agent is in limited token mode"
|
||||
),
|
||||
"token_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Token limit for the agent in limited mode"
|
||||
),
|
||||
"limited_request_mode": fields.Boolean(
|
||||
require=False,
|
||||
description="Whether the agent is in limited request mode"
|
||||
),
|
||||
"request_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Request limit for the agent in limited mode"
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -486,6 +531,10 @@ class UpdateAgent(Resource):
|
||||
"agent_type",
|
||||
"status",
|
||||
"json_schema",
|
||||
"limited_token_mode",
|
||||
"token_limit",
|
||||
"limited_request_mode",
|
||||
"request_limit"
|
||||
]
|
||||
|
||||
for field in allowed_fields:
|
||||
@@ -602,6 +651,54 @@ class UpdateAgent(Resource):
|
||||
update_fields[field] = json_schema
|
||||
else:
|
||||
update_fields[field] = None
|
||||
elif field == "limited_token_mode":
|
||||
is_mode_enabled = data.get("limited_token_mode", False)
|
||||
if is_mode_enabled and data.get("token_limit") is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Token limit must be provided when limited token mode is enabled",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
elif field == "limited_request_mode":
|
||||
is_mode_enabled = data.get("limited_request_mode", False)
|
||||
if is_mode_enabled and data.get("request_limit") is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Request limit must be provided when limited request mode is enabled",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
elif field == "token_limit":
|
||||
token_limit = data.get("token_limit")
|
||||
if token_limit is not None and not data.get("limited_token_mode"):
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Token limit cannot be set when limited token mode is disabled",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
elif field == "request_limit":
|
||||
request_limit = data.get("request_limit")
|
||||
if request_limit is not None and not data.get("limited_request_mode"):
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Request limit cannot be set when limited request mode is disabled",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
else:
|
||||
value = data[field]
|
||||
if field in ["name", "description", "prompt_id", "agent_type"]:
|
||||
|
||||
@@ -9,6 +9,7 @@ from flask import current_app, jsonify, make_response, request
|
||||
from flask_restx import fields, Namespace, Resource
|
||||
|
||||
from application.api import api
|
||||
from application.core.settings import settings
|
||||
from application.api.user.base import (
|
||||
agents_collection,
|
||||
db,
|
||||
@@ -75,6 +76,10 @@ class SharedAgent(Resource):
|
||||
"agent_type": shared_agent.get("agent_type", ""),
|
||||
"status": shared_agent.get("status", ""),
|
||||
"json_schema": shared_agent.get("json_schema"),
|
||||
"limited_token_mode": shared_agent.get("limited_token_mode", False),
|
||||
"token_limit": shared_agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"limited_request_mode": shared_agent.get("limited_request_mode", False),
|
||||
"request_limit": shared_agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"created_at": shared_agent.get("createdAt", ""),
|
||||
"updated_at": shared_agent.get("updatedAt", ""),
|
||||
"shared": shared_agent.get("shared_publicly", False),
|
||||
@@ -149,6 +154,10 @@ class SharedAgents(Resource):
|
||||
"agent_type": agent.get("agent_type", ""),
|
||||
"status": agent.get("status", ""),
|
||||
"json_schema": agent.get("json_schema"),
|
||||
"limited_token_mode": agent.get("limited_token_mode", False),
|
||||
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"limited_request_mode": agent.get("limited_request_mode", False),
|
||||
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"pinned": str(agent["_id"]) in pinned_ids,
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from flask import current_app, jsonify, make_response, redirect, request
|
||||
from flask_restx import fields, Namespace, Resource
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from application.api import api
|
||||
from application.api.user.base import sources_collection
|
||||
@@ -136,31 +134,6 @@ class PaginatedSources(Resource):
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
|
||||
@sources_ns.route("/docs_check")
|
||||
class CheckDocs(Resource):
|
||||
check_docs_model = api.model(
|
||||
"CheckDocsModel",
|
||||
{"docs": fields.String(required=True, description="Document name")},
|
||||
)
|
||||
|
||||
@api.expect(check_docs_model)
|
||||
@api.doc(description="Check if document exists")
|
||||
def post(self):
|
||||
data = request.get_json()
|
||||
required_fields = ["docs"]
|
||||
missing_fields = check_required_fields(data, required_fields)
|
||||
if missing_fields:
|
||||
return missing_fields
|
||||
try:
|
||||
vectorstore = "vectors/" + secure_filename(data["docs"])
|
||||
if os.path.exists(vectorstore) or data["docs"] == "default":
|
||||
return {"status": "exists"}, 200
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error checking document: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"status": "not found"}), 404)
|
||||
|
||||
|
||||
@sources_ns.route("/delete_by_ids")
|
||||
class DeleteByIds(Resource):
|
||||
@api.doc(
|
||||
|
||||
@@ -562,10 +562,21 @@ class TaskStatus(Resource):
|
||||
task = celery.AsyncResult(task_id)
|
||||
task_meta = task.info
|
||||
print(f"Task status: {task.status}")
|
||||
|
||||
if task.status == "PENDING":
|
||||
inspect = celery.control.inspect()
|
||||
active_workers = inspect.ping()
|
||||
if not active_workers:
|
||||
raise ConnectionError("Service unavailable")
|
||||
|
||||
if not isinstance(
|
||||
task_meta, (dict, list, str, int, float, bool, type(None))
|
||||
):
|
||||
task_meta = str(task_meta) # Convert to a string representation
|
||||
except ConnectionError as err:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": str(err)}), 503
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting task status: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
@@ -28,6 +28,10 @@ class Settings(BaseSettings):
|
||||
"claude-2": 1e5,
|
||||
"gemini-2.5-flash": 1e6,
|
||||
}
|
||||
DEFAULT_AGENT_LIMITS: dict = {
|
||||
"token_limit": 50000,
|
||||
"request_limit": 500,
|
||||
}
|
||||
UPLOAD_FOLDER: str = "inputs"
|
||||
PARSE_PDF_AS_IMAGE: bool = False
|
||||
PARSE_IMAGE_REMOTE: bool = False
|
||||
@@ -51,6 +55,11 @@ class Settings(BaseSettings):
|
||||
"http://127.0.0.1:7091/api/connectors/callback" ##add redirect url as it is to your provider's console(gcp)
|
||||
)
|
||||
|
||||
# Microsoft Entra ID (Azure AD) integration
|
||||
MICROSOFT_CLIENT_ID: Optional[str] = None # Azure AD Application (client) ID
|
||||
MICROSOFT_CLIENT_SECRET: Optional[str] = None # Azure AD Application client secret
|
||||
MICROSOFT_TENANT_ID: Optional[str] = "common" # Azure AD Tenant ID (or 'common' for multi-tenant)
|
||||
MICROSOFT_AUTHORITY: Optional[str] = None # e.g., "https://login.microsoftonline.com/{tenant_id}"
|
||||
# GitHub source
|
||||
GITHUB_ACCESS_TOKEN: Optional[str] = None # PAT token with read repo access
|
||||
|
||||
|
||||
@@ -46,5 +46,9 @@ class AnthropicLLM(BaseLLM):
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for completion in stream_response:
|
||||
yield completion.completion
|
||||
try:
|
||||
for completion in stream_response:
|
||||
yield completion.completion
|
||||
finally:
|
||||
if hasattr(stream_response, 'close'):
|
||||
stream_response.close()
|
||||
|
||||
@@ -121,11 +121,19 @@ class DocsGPTAPILLM(BaseLLM):
|
||||
model="docsgpt", messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
|
||||
for line in response:
|
||||
if len(line.choices) > 0 and line.choices[0].delta.content is not None and len(line.choices[0].delta.content) > 0:
|
||||
yield line.choices[0].delta.content
|
||||
elif len(line.choices) > 0:
|
||||
yield line.choices[0]
|
||||
try:
|
||||
for line in response:
|
||||
if (
|
||||
len(line.choices) > 0
|
||||
and line.choices[0].delta.content is not None
|
||||
and len(line.choices[0].delta.content) > 0
|
||||
):
|
||||
yield line.choices[0].delta.content
|
||||
elif len(line.choices) > 0:
|
||||
yield line.choices[0]
|
||||
finally:
|
||||
if hasattr(response, 'close'):
|
||||
response.close()
|
||||
|
||||
def _supports_tools(self):
|
||||
return True
|
||||
@@ -373,17 +373,21 @@ class GoogleLLM(BaseLLM):
|
||||
config=config,
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
if hasattr(chunk, "candidates") and chunk.candidates:
|
||||
for candidate in chunk.candidates:
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if part.function_call:
|
||||
yield part
|
||||
elif part.text:
|
||||
yield part.text
|
||||
elif hasattr(chunk, "text"):
|
||||
yield chunk.text
|
||||
try:
|
||||
for chunk in response:
|
||||
if hasattr(chunk, "candidates") and chunk.candidates:
|
||||
for candidate in chunk.candidates:
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if part.function_call:
|
||||
yield part
|
||||
elif part.text:
|
||||
yield part.text
|
||||
elif hasattr(chunk, "text"):
|
||||
yield chunk.text
|
||||
finally:
|
||||
if hasattr(response, 'close'):
|
||||
response.close()
|
||||
|
||||
def _supports_tools(self):
|
||||
"""Return whether this LLM supports function calling."""
|
||||
|
||||
@@ -170,15 +170,19 @@ class OpenAILLM(BaseLLM):
|
||||
|
||||
response = self.client.chat.completions.create(**request_params)
|
||||
|
||||
for line in response:
|
||||
if (
|
||||
len(line.choices) > 0
|
||||
and line.choices[0].delta.content is not None
|
||||
and len(line.choices[0].delta.content) > 0
|
||||
):
|
||||
yield line.choices[0].delta.content
|
||||
elif len(line.choices) > 0:
|
||||
yield line.choices[0]
|
||||
try:
|
||||
for line in response:
|
||||
if (
|
||||
len(line.choices) > 0
|
||||
and line.choices[0].delta.content is not None
|
||||
and len(line.choices[0].delta.content) > 0
|
||||
):
|
||||
yield line.choices[0].delta.content
|
||||
elif len(line.choices) > 0:
|
||||
yield line.choices[0]
|
||||
finally:
|
||||
if hasattr(response, 'close'):
|
||||
response.close()
|
||||
|
||||
def _supports_tools(self):
|
||||
return True
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from application.parser.connectors.google_drive.loader import GoogleDriveLoader
|
||||
from application.parser.connectors.google_drive.auth import GoogleDriveAuth
|
||||
from application.parser.connectors.share_point.auth import SharePointAuth
|
||||
from application.parser.connectors.share_point.loader import SharePointLoader
|
||||
|
||||
|
||||
class ConnectorCreator:
|
||||
@@ -12,10 +14,12 @@ class ConnectorCreator:
|
||||
|
||||
connectors = {
|
||||
"google_drive": GoogleDriveLoader,
|
||||
"share_point": SharePointLoader,
|
||||
}
|
||||
|
||||
auth_providers = {
|
||||
"google_drive": GoogleDriveAuth,
|
||||
"share_point": SharePointAuth,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
10
application/parser/connectors/share_point/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Share Point connector package for DocsGPT.
|
||||
|
||||
This module provides authentication and document loading capabilities for Share Point.
|
||||
"""
|
||||
|
||||
from .auth import SharePointAuth
|
||||
from .loader import SharePointLoader
|
||||
|
||||
__all__ = ['SharePointAuth', 'SharePointLoader']
|
||||
91
application/parser/connectors/share_point/auth.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from msal import ConfidentialClientApplication
|
||||
|
||||
from application.core.settings import settings
|
||||
from application.parser.connectors.base import BaseConnectorAuth
|
||||
|
||||
|
||||
class SharePointAuth(BaseConnectorAuth):
|
||||
"""
|
||||
Handles Microsoft OAuth 2.0 authentication.
|
||||
|
||||
# Documentation:
|
||||
- https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
|
||||
- https://learn.microsoft.com/en-gb/entra/msal/python/
|
||||
"""
|
||||
|
||||
# Microsoft Graph scopes for SharePoint access
|
||||
SCOPES = [
|
||||
"User.Read",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.client_id = settings.MICROSOFT_CLIENT_ID
|
||||
self.client_secret = settings.MICROSOFT_CLIENT_SECRET
|
||||
|
||||
if not self.client_id or not self.client_secret:
|
||||
raise ValueError(
|
||||
"Microsoft OAuth credentials not configured. Please set MICROSOFT_CLIENT_ID and MICROSOFT_CLIENT_SECRET in settings."
|
||||
)
|
||||
|
||||
self.redirect_uri = settings.CONNECTOR_REDIRECT_BASE_URI
|
||||
self.tenant_id = settings.MICROSOFT_TENANT_ID
|
||||
self.authority = getattr(settings, "MICROSOFT_AUTHORITY", f"https://{self.tenant_id}.ciamlogin.com/{self.tenant_id}")
|
||||
|
||||
self.auth_app = ConfidentialClientApplication(
|
||||
client_id=self.client_id, client_credential=self.client_secret, authority=self.authority
|
||||
)
|
||||
|
||||
def get_authorization_url(self, state: Optional[str] = None) -> str:
|
||||
return self.auth_app.get_authorization_request_url(
|
||||
scopes=self.SCOPES, state=state, redirect_uri=self.redirect_uri
|
||||
)
|
||||
|
||||
def exchange_code_for_tokens(self, authorization_code: str) -> Dict[str, Any]:
|
||||
result = self.auth_app.acquire_token_by_authorization_code(
|
||||
code=authorization_code, scopes=self.SCOPES, redirect_uri=self.redirect_uri
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
logging.error(f"Error acquiring token: {result.get('error_description')}")
|
||||
raise ValueError(f"Error acquiring token: {result.get('error_description')}")
|
||||
|
||||
return self.map_token_response(result)
|
||||
|
||||
def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]:
|
||||
result = self.auth_app.acquire_token_by_refresh_token(refresh_token=refresh_token, scopes=self.SCOPES)
|
||||
|
||||
if "error" in result:
|
||||
logging.error(f"Error acquiring token: {result.get('error_description')}")
|
||||
raise ValueError(f"Error acquiring token: {result.get('error_description')}")
|
||||
|
||||
return self.map_token_response(result)
|
||||
|
||||
def is_token_expired(self, token_info: Dict[str, Any]) -> bool:
|
||||
if not token_info or "expiry" not in token_info:
|
||||
# If no expiry info, consider token expired to be safe
|
||||
return True
|
||||
|
||||
# Get expiry timestamp and current time
|
||||
expiry_timestamp = token_info["expiry"]
|
||||
current_timestamp = int(datetime.datetime.now().timestamp())
|
||||
|
||||
# Token is expired if current time is greater than or equal to expiry time
|
||||
return current_timestamp >= expiry_timestamp
|
||||
|
||||
def map_token_response(self, result) -> Dict[str, Any]:
|
||||
return {
|
||||
"access_token": result.get("access_token"),
|
||||
"refresh_token": result.get("refresh_token"),
|
||||
"token_uri": result.get("id_token_claims", {}).get("iss"),
|
||||
"scopes": result.get("scope"),
|
||||
"expiry": result.get("id_token_claims", {}).get("exp"),
|
||||
"user_info": {
|
||||
"name": result.get("id_token_claims", {}).get("name"),
|
||||
"email": result.get("id_token_claims", {}).get("preferred_username"),
|
||||
},
|
||||
"raw_token": result,
|
||||
}
|
||||
44
application/parser/connectors/share_point/loader.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from typing import List, Dict, Any
|
||||
from application.parser.connectors.base import BaseConnectorLoader
|
||||
from application.parser.schema.base import Document
|
||||
|
||||
|
||||
class SharePointLoader(BaseConnectorLoader):
|
||||
def __init__(self, session_token: str):
|
||||
pass
|
||||
|
||||
def load_data(self, inputs: Dict[str, Any]) -> List[Document]:
|
||||
"""
|
||||
Load documents from the external knowledge base.
|
||||
|
||||
Args:
|
||||
inputs: Configuration dictionary containing:
|
||||
- file_ids: Optional list of specific file IDs to load
|
||||
- folder_ids: Optional list of folder IDs to browse/download
|
||||
- limit: Maximum number of items to return
|
||||
- list_only: If True, return metadata without content
|
||||
- recursive: Whether to recursively process folders
|
||||
|
||||
Returns:
|
||||
List of Document objects
|
||||
"""
|
||||
pass
|
||||
|
||||
def download_to_directory(self, local_dir: str, source_config: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Download files/folders to a local directory.
|
||||
|
||||
Args:
|
||||
local_dir: Local directory path to download files to
|
||||
source_config: Configuration for what to download
|
||||
|
||||
Returns:
|
||||
Dictionary containing download results:
|
||||
- files_downloaded: Number of files downloaded
|
||||
- directory_path: Path where files were downloaded
|
||||
- empty_result: Whether no files were downloaded
|
||||
- source_type: Type of connector
|
||||
- config_used: Configuration that was used
|
||||
- error: Error message if download failed (optional)
|
||||
"""
|
||||
pass
|
||||
@@ -40,6 +40,7 @@ markupsafe==3.0.2
|
||||
marshmallow==3.26.1
|
||||
mpmath==1.3.0
|
||||
multidict==6.4.3
|
||||
msal==1.34.0
|
||||
mypy-extensions==1.0.0
|
||||
networkx==3.4.2
|
||||
numpy==2.2.1
|
||||
@@ -87,4 +88,4 @@ werkzeug>=3.1.0,<3.1.2
|
||||
yarl==1.20.0
|
||||
markdownify==1.1.0
|
||||
tldextract==5.1.3
|
||||
websockets==14.1
|
||||
websockets==14.1
|
||||
@@ -72,4 +72,4 @@ services:
|
||||
- mongodb_data_container:/data/db
|
||||
|
||||
volumes:
|
||||
mongodb_data_container:
|
||||
mongodb_data_container:
|
||||
@@ -42,7 +42,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
|
||||
|
||||
* **Option 1: Using a `.env` file (Recommended):**
|
||||
* If you haven't already, create a file named `.env` in the **root directory** of your DocsGPT project.
|
||||
* Modify the `.env` file to adjust settings as needed. You can find a comprehensive list of configurable options in [`application/core/settings.py`](application/core/settings.py).
|
||||
* Modify the `.env` file to adjust settings as needed. You can find a comprehensive list of configurable options in [`application/core/settings.py`](https://github.com/arc53/DocsGPT/blob/main/application/core/settings.py).
|
||||
|
||||
* **Option 2: Exporting Environment Variables:**
|
||||
* Alternatively, you can export environment variables directly in your terminal. However, using a `.env` file is generally more organized for development.
|
||||
@@ -67,7 +67,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
|
||||
|
||||
3. **Download Embedding Model:**
|
||||
|
||||
The backend requires an embedding model. Download the `mpnet-base-v2` model and place it in the `model/` directory within the project root. You can use the following script:
|
||||
The backend requires an embedding model. Download the `mpnet-base-v2` model and place it in the `models/` directory within the project root. You can use the following script:
|
||||
|
||||
```bash
|
||||
wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip
|
||||
@@ -75,7 +75,7 @@ To run the DocsGPT backend locally, you'll need to set up a Python environment a
|
||||
rm mpnet-base-v2.zip
|
||||
```
|
||||
|
||||
Alternatively, you can manually download the zip file from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it, and place the extracted folder in `model/`.
|
||||
Alternatively, you can manually download the zip file from [here](https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip), unzip it, and place the extracted folder in `models/`.
|
||||
|
||||
4. **Install Backend Dependencies:**
|
||||
|
||||
@@ -160,4 +160,4 @@ To run the DocsGPT frontend locally, you'll need Node.js and npm (Node Package M
|
||||
|
||||
This command will start the Vite development server. The frontend application will typically be accessible at [http://localhost:5173/](http://localhost:5173/). The terminal will display the exact URL where the frontend is running.
|
||||
|
||||
With both the backend and frontend running, you should now have a fully functional DocsGPT development environment. You can access the application in your browser at [http://localhost:5173/](http://localhost:5173/) and start developing!
|
||||
With both the backend and frontend running, you should now have a fully functional DocsGPT development environment. You can access the application in your browser at [http://localhost:5173/](http://localhost:5173/) and start developing!
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Please put appropriate value
|
||||
VITE_BASE_URL=http://localhost:5173
|
||||
VITE_API_HOST=http://127.0.0.1:7091
|
||||
VITE_API_STREAMING=true
|
||||
VITE_API_STREAMING=true
|
||||
VITE_NOTIFICATION_TEXT="What's new in 0.14.0 — Changelog"
|
||||
VITE_NOTIFICATION_LINK="#"
|
||||
163
frontend/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mermaid": "^11.6.0",
|
||||
"mermaid": "^11.12.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
@@ -90,22 +90,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@antfu/install-pkg": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.0.0.tgz",
|
||||
"integrity": "sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
|
||||
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"package-manager-detector": "^0.2.8",
|
||||
"tinyexec": "^0.3.2"
|
||||
"package-manager-detector": "^1.3.0",
|
||||
"tinyexec": "^1.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@antfu/utils": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz",
|
||||
"integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz",
|
||||
"integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
@@ -957,18 +957,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/utils": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz",
|
||||
"integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz",
|
||||
"integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antfu/install-pkg": "^1.0.0",
|
||||
"@antfu/utils": "^8.1.0",
|
||||
"@antfu/install-pkg": "^1.1.0",
|
||||
"@antfu/utils": "^9.2.0",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"debug": "^4.4.0",
|
||||
"globals": "^15.14.0",
|
||||
"debug": "^4.4.1",
|
||||
"globals": "^15.15.0",
|
||||
"kolorist": "^1.8.0",
|
||||
"local-pkg": "^1.0.0",
|
||||
"local-pkg": "^1.1.1",
|
||||
"mlly": "^1.7.4"
|
||||
}
|
||||
},
|
||||
@@ -1053,9 +1053,9 @@
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@mermaid-js/parser": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.4.0.tgz",
|
||||
"integrity": "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA==",
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
|
||||
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"langium": "3.3.1"
|
||||
@@ -2856,9 +2856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -4141,15 +4141,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -5326,9 +5327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.4.tgz",
|
||||
"integrity": "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
|
||||
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
@@ -6909,13 +6910,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.21",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
|
||||
"integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
|
||||
"version": "0.16.23",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.23.tgz",
|
||||
"integrity": "sha512-7VlC1hsEEolL9xNO05v9VjrvWZePkCVBJqj8ruICxYjZfHaHbaU53AlP+PODyFIXEnaEIEWi3wJy7FPZ95JAVg==",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^8.3.0"
|
||||
},
|
||||
@@ -7280,14 +7282,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/local-pkg": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
|
||||
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
|
||||
"integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mlly": "^1.7.4",
|
||||
"pkg-types": "^2.0.1",
|
||||
"quansync": "^0.2.8"
|
||||
"pkg-types": "^2.3.0",
|
||||
"quansync": "^0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -7483,15 +7485,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.7",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz",
|
||||
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
|
||||
"version": "16.4.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.0.tgz",
|
||||
"integrity": "sha512-CTPAcRBq57cn3R8n3hwc2REddc28hjR7RzDXQ+lXLmMJYqn20BaI2cGw6QjgZGIgVfp2Wdfw4aMzgNteQ6qJgQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
@@ -7927,14 +7929,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz",
|
||||
"integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==",
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.0.tgz",
|
||||
"integrity": "sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
"@iconify/utils": "^2.1.33",
|
||||
"@mermaid-js/parser": "^0.4.0",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@iconify/utils": "^3.0.1",
|
||||
"@mermaid-js/parser": "^0.6.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
@@ -7942,12 +7944,12 @@
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"katex": "^0.16.9",
|
||||
"dayjs": "^1.11.18",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.22",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.7",
|
||||
"marked": "^16.2.1",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.6",
|
||||
"ts-dedent": "^2.2.0",
|
||||
@@ -8606,15 +8608,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mlly": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz",
|
||||
"integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
|
||||
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"pathe": "^2.0.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"ufo": "^1.5.4"
|
||||
"acorn": "^8.15.0",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^1.3.1",
|
||||
"ufo": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mlly/node_modules/confbox": {
|
||||
@@ -8923,13 +8925,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/package-manager-detector": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
|
||||
"integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quansync": "^0.2.7"
|
||||
}
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.0.tgz",
|
||||
"integrity": "sha512-rRZ+pR1Usc+ND9M2NkmCvE/LYJS+8ORVV9X0KuNSY/gFsp7RBHJM/ADh9LYq4Vvfq6QkKrW6/weuh8SMEtN5gw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
@@ -9084,13 +9083,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
|
||||
"integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.1",
|
||||
"exsolve": "^1.0.1",
|
||||
"confbox": "^0.2.2",
|
||||
"exsolve": "^1.0.7",
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
@@ -9302,9 +9301,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
||||
"integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -10539,9 +10538,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
|
||||
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
@@ -10791,9 +10790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ufo": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
|
||||
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mermaid": "^11.6.0",
|
||||
"mermaid": "^11.12.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
|
||||
@@ -15,6 +15,7 @@ import useTokenAuth from './hooks/useTokenAuth';
|
||||
import Navigation from './Navigation';
|
||||
import PageNotFound from './PageNotFound';
|
||||
import Setting from './settings';
|
||||
import Notification from './components/Notification';
|
||||
|
||||
function AuthWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthLoading } = useTokenAuth();
|
||||
@@ -52,11 +53,27 @@ function MainLayout() {
|
||||
}
|
||||
export default function App() {
|
||||
const [, , componentMounted] = useDarkTheme();
|
||||
const [showNotification, setShowNotification] = useState<boolean>(() => {
|
||||
const saved = localStorage.getItem('showNotification');
|
||||
return saved ? JSON.parse(saved) : true;
|
||||
});
|
||||
const notificationText = import.meta.env.VITE_NOTIFICATION_TEXT;
|
||||
const notificationLink = import.meta.env.VITE_NOTIFICATION_LINK;
|
||||
if (!componentMounted) {
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<div className="relative h-full overflow-hidden">
|
||||
{notificationLink && notificationText && showNotification && (
|
||||
<Notification
|
||||
notificationText={notificationText}
|
||||
notificationLink={notificationLink}
|
||||
handleCloseNotification={() => {
|
||||
setShowNotification(false);
|
||||
localStorage.setItem('showNotification', 'false');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
|
||||
@@ -399,7 +399,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
{conversations?.loading && !isDeletingConversation && (
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<img
|
||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||
src={isDarkTheme ? Spinner : SpinnerDark}
|
||||
className="animate-spin cursor-pointer bg-transparent"
|
||||
alt="Loading conversations"
|
||||
/>
|
||||
|
||||
@@ -53,6 +53,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
agent_type: 'classic',
|
||||
status: '',
|
||||
json_schema: undefined,
|
||||
limited_token_mode: false,
|
||||
token_limit: undefined,
|
||||
limited_request_mode: false,
|
||||
request_limit: undefined,
|
||||
});
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [prompts, setPrompts] = useState<
|
||||
@@ -74,7 +78,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
const [publishLoading, setPublishLoading] = useState(false);
|
||||
const [jsonSchemaText, setJsonSchemaText] = useState('');
|
||||
const [jsonSchemaValid, setJsonSchemaValid] = useState(true);
|
||||
const [isJsonSchemaExpanded, setIsJsonSchemaExpanded] = useState(false);
|
||||
const [isAdvancedSectionExpanded, setIsAdvancedSectionExpanded] =
|
||||
useState(false);
|
||||
|
||||
const initialAgentRef = useRef<Agent | null>(null);
|
||||
const sourceAnchorButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -191,6 +196,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
formData.append('agent_type', agent.agent_type);
|
||||
formData.append('status', 'draft');
|
||||
|
||||
if (agent.limited_token_mode && agent.token_limit) {
|
||||
formData.append('limited_token_mode', 'True');
|
||||
formData.append('token_limit', JSON.stringify(agent.token_limit));
|
||||
} else formData.append('token_limit', '0');
|
||||
|
||||
if (agent.limited_request_mode && agent.request_limit) {
|
||||
formData.append('limited_request_mode', 'True');
|
||||
formData.append('request_limit', JSON.stringify(agent.request_limit));
|
||||
} else formData.append('request_limit', '0');
|
||||
|
||||
if (imageFile) formData.append('image', imageFile);
|
||||
|
||||
if (agent.tools && agent.tools.length > 0)
|
||||
@@ -280,6 +295,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
formData.append('json_schema', JSON.stringify(agent.json_schema));
|
||||
}
|
||||
|
||||
if (agent.limited_token_mode && agent.token_limit) {
|
||||
formData.append('limited_token_mode', 'True');
|
||||
formData.append('token_limit', JSON.stringify(agent.token_limit));
|
||||
} else formData.append('token_limit', '0');
|
||||
|
||||
if (agent.limited_request_mode && agent.request_limit) {
|
||||
formData.append('limited_request_mode', 'True');
|
||||
formData.append('request_limit', JSON.stringify(agent.request_limit));
|
||||
} else formData.append('request_limit', '0');
|
||||
|
||||
try {
|
||||
setPublishLoading(true);
|
||||
const response =
|
||||
@@ -825,7 +850,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
</div>
|
||||
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||
<button
|
||||
onClick={() => setIsJsonSchemaExpanded(!isJsonSchemaExpanded)}
|
||||
onClick={() =>
|
||||
setIsAdvancedSectionExpanded(!isAdvancedSectionExpanded)
|
||||
}
|
||||
className="flex w-full items-center justify-between text-left focus:outline-none"
|
||||
>
|
||||
<div>
|
||||
@@ -834,7 +861,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<div className="ml-4 flex items-center">
|
||||
<svg
|
||||
className={`h-5 w-5 transform transition-transform duration-200 ${
|
||||
isJsonSchemaExpanded ? 'rotate-180' : ''
|
||||
isAdvancedSectionExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -849,7 +876,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
{isJsonSchemaExpanded && (
|
||||
{isAdvancedSectionExpanded && (
|
||||
<div className="mt-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">JSON response schema</h2>
|
||||
@@ -892,6 +919,115 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
: 'Invalid JSON - fix to enable saving'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">Token limiting</h2>
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
Limit daily total tokens that can be used by this agent
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newTokenMode = !agent.limited_token_mode;
|
||||
setAgent({
|
||||
...agent,
|
||||
limited_token_mode: newTokenMode,
|
||||
limited_request_mode: newTokenMode
|
||||
? false
|
||||
: agent.limited_request_mode,
|
||||
});
|
||||
}}
|
||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||
agent.limited_token_mode
|
||||
? 'bg-purple-30'
|
||||
: 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
agent.limited_token_mode ? '' : '-translate-x-5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={agent.token_limit || ''}
|
||||
onChange={(e) =>
|
||||
setAgent({
|
||||
...agent,
|
||||
token_limit: e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
disabled={!agent.limited_token_mode}
|
||||
placeholder="Enter token limit"
|
||||
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
|
||||
!agent.limited_token_mode
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">Request limiting</h2>
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
Limit daily total requests that can be made to this
|
||||
agent
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newRequestMode = !agent.limited_request_mode;
|
||||
setAgent({
|
||||
...agent,
|
||||
limited_request_mode: newRequestMode,
|
||||
limited_token_mode: newRequestMode
|
||||
? false
|
||||
: agent.limited_token_mode,
|
||||
});
|
||||
}}
|
||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||
agent.limited_request_mode
|
||||
? 'bg-purple-30'
|
||||
: 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
agent.limited_request_mode ? '' : '-translate-x-5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={agent.request_limit || ''}
|
||||
onChange={(e) =>
|
||||
setAgent({
|
||||
...agent,
|
||||
request_limit: e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
disabled={!agent.limited_request_mode}
|
||||
placeholder="Enter request limit"
|
||||
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
|
||||
!agent.limited_request_mode
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,12 @@ import AgentImage from '../components/AgentImage';
|
||||
import { Agent } from './types';
|
||||
|
||||
export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||
// Check if shared metadata exists and has properties (type is 'any' so we validate it's a non-empty object)
|
||||
const hasSharedMetadata =
|
||||
agent.shared_metadata &&
|
||||
typeof agent.shared_metadata === 'object' &&
|
||||
agent.shared_metadata !== null &&
|
||||
Object.keys(agent.shared_metadata).length > 0;
|
||||
return (
|
||||
<div className="border-dark-gray dark:border-grey flex w-full max-w-[720px] flex-col rounded-3xl border p-6 shadow-xs sm:w-fit sm:min-w-[480px]">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -20,7 +26,7 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{agent.shared_metadata && (
|
||||
{hasSharedMetadata && (
|
||||
<div className="mt-4 flex items-center gap-8">
|
||||
{agent.shared_metadata?.shared_by && (
|
||||
<p className="text-eerie-black text-xs font-light sm:text-sm dark:text-[#E0E0E0]">
|
||||
|
||||
@@ -57,7 +57,7 @@ export const fetchPreviewAnswer = createAsyncThunk<
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.preference.selectedDocs,
|
||||
null, // No conversation ID for previews
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
@@ -128,7 +128,7 @@ export const fetchPreviewAnswer = createAsyncThunk<
|
||||
question,
|
||||
signal,
|
||||
state.preference.token,
|
||||
state.preference.selectedDocs!,
|
||||
state.preference.selectedDocs,
|
||||
null, // No conversation ID for previews
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
|
||||
@@ -28,4 +28,8 @@ export type Agent = {
|
||||
updated_at?: string;
|
||||
last_used_at?: string;
|
||||
json_schema?: object;
|
||||
limited_token_mode?: boolean;
|
||||
token_limit?: number;
|
||||
limited_request_mode?: boolean;
|
||||
request_limit?: number;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ const endpoints = {
|
||||
CONFIG: '/api/config',
|
||||
NEW_TOKEN: '/api/generate_token',
|
||||
DOCS: '/api/sources',
|
||||
DOCS_CHECK: '/api/docs_check',
|
||||
DOCS_PAGINATED: '/api/sources/paginated',
|
||||
API_KEYS: '/api/get_api_keys',
|
||||
CREATE_API_KEY: '/api/create_api_key',
|
||||
|
||||
@@ -10,8 +10,6 @@ const userService = {
|
||||
apiClient.get(`${endpoints.USER.DOCS}`, token),
|
||||
getDocsWithPagination: (query: string, token: string | null): Promise<any> =>
|
||||
apiClient.get(`${endpoints.USER.DOCS_PAGINATED}?${query}`, token),
|
||||
checkDocs: (data: any, token: string | null): Promise<any> =>
|
||||
apiClient.post(endpoints.USER.DOCS_CHECK, data, token),
|
||||
getAPIKeys: (token: string | null): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.API_KEYS, token),
|
||||
createAPIKey: (data: any, token: string | null): Promise<any> =>
|
||||
|
||||
3
frontend/src/assets/arrow-full-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.0303 7.03033C13.3232 6.73744 13.3232 6.26256 13.0303 5.96967L8.25736 1.1967C7.96447 0.903806 7.48959 0.903806 7.1967 1.1967C6.90381 1.48959 6.90381 1.96447 7.1967 2.25736L11.4393 6.5L7.1967 10.7426C6.90381 11.0355 6.90381 11.5104 7.1967 11.8033C7.48959 12.0962 7.96447 12.0962 8.25736 11.8033L13.0303 7.03033ZM0.5 6.5V7.25H12.5V6.5V5.75H0.5V6.5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 478 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.41 10.59L2.83 6L7.41 1.41L6 0L0 6L6 12L7.41 10.59Z" fill="black" fill-opacity="0.54"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 200 B |
@@ -1 +0,0 @@
|
||||
<svg width="18" height="18" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.93179 5.43179C4.75605 5.60753 4.75605 5.89245 4.93179 6.06819C5.10753 6.24392 5.39245 6.24392 5.56819 6.06819L7.49999 4.13638L9.43179 6.06819C9.60753 6.24392 9.89245 6.24392 10.0682 6.06819C10.2439 5.89245 10.2439 5.60753 10.0682 5.43179L7.81819 3.18179C7.73379 3.0974 7.61933 3.04999 7.49999 3.04999C7.38064 3.04999 7.26618 3.0974 7.18179 3.18179L4.93179 5.43179ZM10.0682 9.56819C10.2439 9.39245 10.2439 9.10753 10.0682 8.93179C9.89245 8.75606 9.60753 8.75606 9.43179 8.93179L7.49999 10.8636L5.56819 8.93179C5.39245 8.75606 5.10753 8.75606 4.93179 8.93179C4.75605 9.10753 4.75605 9.39245 4.93179 9.56819L7.18179 11.8182C7.35753 11.9939 7.64245 11.9939 7.81819 11.8182L10.0682 9.56819Z" fill="gray" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
Before Width: | Height: | Size: 859 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00182 11.2502C7.23838 11.2502 6.50621 10.9552 5.96637 10.4301C5.42654 9.90499 5.12327 9.1928 5.12327 8.4502C5.12327 7.70759 5.42654 6.9954 5.96637 6.4703C6.50621 5.9452 7.23838 5.6502 8.00182 5.6502C8.76525 5.6502 9.49743 5.9452 10.0373 6.4703C10.5771 6.9954 10.8804 7.70759 10.8804 8.4502C10.8804 9.1928 10.5771 9.90499 10.0373 10.4301C9.49743 10.9552 8.76525 11.2502 8.00182 11.2502ZM14.1126 9.2262C14.1455 8.9702 14.1701 8.7142 14.1701 8.4502C14.1701 8.1862 14.1455 7.9222 14.1126 7.6502L15.8479 6.3462C16.0042 6.2262 16.0453 6.0102 15.9466 5.8342L14.3017 3.0662C14.203 2.8902 13.981 2.8182 13.8 2.8902L11.7522 3.6902C11.3245 3.3782 10.8804 3.1062 10.3622 2.9062L10.0579 0.786197C10.0412 0.69197 9.99076 0.606538 9.91549 0.545038C9.84022 0.483538 9.745 0.449939 9.6467 0.450197H6.35693C6.15132 0.450197 5.97861 0.594197 5.94571 0.786197L5.64141 2.9062C5.12327 3.1062 4.67915 3.3782 4.25148 3.6902L2.2036 2.8902C2.02266 2.8182 1.8006 2.8902 1.70191 3.0662L0.0570212 5.8342C-0.0498963 6.0102 -0.00054964 6.2262 0.155714 6.3462L1.89107 7.6502C1.85817 7.9222 1.8335 8.1862 1.8335 8.4502C1.8335 8.7142 1.85817 8.9702 1.89107 9.2262L0.155714 10.5542C-0.00054964 10.6742 -0.0498963 10.8902 0.0570212 11.0662L1.70191 13.8342C1.8006 14.0102 2.02266 14.0742 2.2036 14.0102L4.25148 13.2022C4.67915 13.5222 5.12327 13.7942 5.64141 13.9942L5.94571 16.1142C5.97861 16.3062 6.15132 16.4502 6.35693 16.4502H9.6467C9.85231 16.4502 10.025 16.3062 10.0579 16.1142L10.3622 13.9942C10.8804 13.7862 11.3245 13.5222 11.7522 13.2022L13.8 14.0102C13.981 14.0742 14.203 14.0102 14.3017 13.8342L15.9466 11.0662C16.0453 10.8902 16.0042 10.6742 15.8479 10.5542L14.1126 9.2262Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.75 8.70825C3.75 6.46942 3.75 5.34921 4.44588 4.65413C5.14096 3.95825 6.26117 3.95825 8.5 3.95825H10.875C13.1138 3.95825 14.234 3.95825 14.9291 4.65413C15.625 5.34921 15.625 6.46942 15.625 8.70825V12.6666C15.625 14.9054 15.625 16.0256 14.9291 16.7207C14.234 17.4166 13.1138 17.4166 10.875 17.4166H8.5C6.26117 17.4166 5.14096 17.4166 4.44588 16.7207C3.75 16.0256 3.75 14.9054 3.75 12.6666V8.70825Z" stroke="#B9B9B9" stroke-width="1.5"/>
|
||||
<path d="M3.75 15.0416C3.12011 15.0416 2.51602 14.7914 2.07062 14.346C1.62522 13.9006 1.375 13.2965 1.375 12.6666V7.91658C1.375 4.93121 1.375 3.43813 2.30283 2.51109C3.23067 1.58404 4.72296 1.58325 7.70833 1.58325H10.875C11.5049 1.58325 12.109 1.83347 12.5544 2.27887C12.9998 2.72427 13.25 3.32836 13.25 3.95825" stroke="#B9B9B9" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 901 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="18" viewBox="0 0 22 18" fill="none">
|
||||
<path d="M18.7359 2.29769C17.4059 1.66546 15.9659 1.20658 14.4659 0.941453C14.4528 0.941024 14.4397 0.943541 14.4276 0.948827C14.4155 0.954112 14.4047 0.962038 14.3959 0.972045C14.2159 1.30856 14.0059 1.74704 13.8659 2.08355C12.2749 1.83882 10.6569 1.83882 9.06594 2.08355C8.92594 1.73684 8.71594 1.30856 8.52594 0.972045C8.51594 0.951651 8.48594 0.941453 8.45594 0.941453C6.95594 1.20658 5.52594 1.66546 4.18594 2.29769C4.17594 2.29769 4.16594 2.30789 4.15594 2.31809C1.43594 6.46839 0.685937 10.5065 1.05594 14.5039C1.05594 14.5243 1.06594 14.5447 1.08594 14.5548C2.88594 15.9009 4.61594 16.7167 6.32594 17.2571C6.35594 17.2673 6.38594 17.2571 6.39594 17.2367C6.79594 16.6759 7.15594 16.0844 7.46594 15.4624C7.48594 15.4216 7.46594 15.3808 7.42594 15.3706C6.85594 15.1463 6.31594 14.8812 5.78594 14.5752C5.74594 14.5549 5.74594 14.4937 5.77594 14.4631C5.88594 14.3815 5.99594 14.2897 6.10594 14.2081C6.12594 14.1877 6.15594 14.1877 6.17594 14.1979C9.61594 15.7989 13.3259 15.7989 16.7259 14.1979C16.7459 14.1877 16.7759 14.1877 16.7959 14.2081C16.9059 14.2999 17.0159 14.3815 17.1259 14.4733C17.1659 14.5039 17.1659 14.565 17.1159 14.5854C16.5959 14.9016 16.0459 15.1565 15.4759 15.3808C15.4359 15.391 15.4259 15.442 15.4359 15.4726C15.7559 16.0946 16.1159 16.6861 16.5059 17.2469C16.5359 17.2571 16.5659 17.2673 16.5959 17.2571C18.3159 16.7167 20.0459 15.9009 21.8459 14.5548C21.8659 14.5447 21.8759 14.5243 21.8759 14.5039C22.3159 9.88449 21.1459 5.87695 18.7759 2.31809C18.7659 2.30789 18.7559 2.29769 18.7359 2.29769ZM7.98594 12.0667C6.95594 12.0667 6.09594 11.098 6.09594 9.90488C6.09594 8.7118 6.93594 7.74305 7.98594 7.74305C9.04594 7.74305 9.88594 8.72199 9.87594 9.90488C9.87594 11.098 9.03594 12.0667 7.98594 12.0667ZM14.9559 12.0667C13.9259 12.0667 13.0659 11.098 13.0659 9.90488C13.0659 8.7118 13.9059 7.74305 14.9559 7.74305C16.0159 7.74305 16.8559 8.72199 16.8459 9.90488C16.8459 11.098 16.0159 12.0667 14.9559 12.0667Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.04867 15.49H15.1775C16.5339 15.49 17.3172 14.7067 17.3172 13.1548V4.83755C17.3172 3.29309 16.5265 2.50977 14.9515 2.50977H2.82302C1.47463 2.50977 0.683594 3.28569 0.683594 4.83755V13.1545C0.683594 14.7138 1.48138 15.49 3.04867 15.49ZM8.10441 9.3803L2.41609 3.76816C2.58163 3.70034 2.77738 3.66273 3.01106 3.66273H14.9971C15.2305 3.66273 15.434 3.70034 15.6072 3.78327L9.92692 9.38062C9.60291 9.7043 9.31652 9.84766 9.01534 9.84766C8.71384 9.84766 8.42841 9.70398 8.10441 9.3803ZM1.83559 13.1548V4.76234L6.16717 9.01162L1.84331 13.2824C1.83592 13.2448 1.83559 13.1998 1.83559 13.1548ZM16.1646 4.84494V13.2599L11.8629 9.0113L16.1649 4.78516L16.1646 4.84494ZM3.01106 14.3377C2.79249 14.3377 2.61184 14.3075 2.4537 14.2397L6.95852 9.78723L7.44838 10.2694C7.97552 10.7891 8.48017 11.0077 9.01534 11.0077C9.54249 11.0077 10.0548 10.7891 10.5823 10.2694L11.0718 9.78723L15.5696 14.2319C15.4111 14.3075 15.2154 14.3374 14.9968 14.3374L3.01106 14.3377Z" fill="#ECECF1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22" fill="none">
|
||||
<path d="M19.8155 2.09139C19.0825 1.34392 18.2005 0.970703 17.1685 0.970703H4.68155C3.64955 0.970703 2.76755 1.34392 2.03455 2.09139C1.30155 2.83885 0.935547 3.73825 0.935547 4.79061V17.524C0.935547 18.5763 1.30155 19.4757 2.03455 20.2232C2.76755 20.9707 3.64955 21.3439 4.68155 21.3439H7.59555C7.78555 21.3439 7.92855 21.3368 8.02455 21.3235C8.13624 21.3007 8.23705 21.2399 8.31055 21.1512C8.40555 21.0492 8.45355 20.9013 8.45355 20.7076L8.44655 19.8051C8.44255 19.23 8.44055 18.7752 8.44055 18.4387L8.14055 18.4917C7.95055 18.5274 7.71055 18.5427 7.41955 18.5386C7.11624 18.5329 6.81389 18.5019 6.51555 18.4458C6.19795 18.386 5.89897 18.2497 5.64355 18.0481C5.37604 17.8418 5.17652 17.5572 5.07155 17.2323L4.94155 16.9264C4.83198 16.6851 4.69432 16.4581 4.53155 16.2503C4.34555 16.0025 4.15655 15.8353 3.96555 15.7466L3.87555 15.6803C3.81279 15.6345 3.75571 15.5811 3.70555 15.5212C3.65764 15.4657 3.6182 15.4031 3.58855 15.3356C3.56255 15.2734 3.58455 15.2224 3.65355 15.1827C3.72355 15.1419 3.84855 15.1225 4.03155 15.1225L4.29155 15.1633C4.46455 15.198 4.67955 15.304 4.93455 15.4804C5.19261 15.6598 5.40818 15.8957 5.56555 16.1708C5.76555 16.5328 6.00555 16.8091 6.28755 16.9998C6.56955 17.1895 6.85355 17.2854 7.13955 17.2854C7.42555 17.2854 7.67255 17.2629 7.88155 17.2191C8.08366 17.1765 8.28005 17.1094 8.46655 17.0192C8.54455 16.4278 8.75655 15.9709 9.10355 15.6528C8.65392 15.6078 8.2083 15.5281 7.77055 15.4142C7.34334 15.2945 6.93249 15.1208 6.54755 14.8972C6.14479 14.6736 5.78905 14.3714 5.50055 14.008C5.22355 13.6541 4.99555 13.1901 4.81755 12.616C4.64055 12.0409 4.55155 11.377 4.55155 10.6255C4.55155 9.55581 4.89355 8.64519 5.57855 7.89263C5.25855 7.08908 5.28855 6.18662 5.66955 5.18831C5.92155 5.10775 6.29455 5.16791 6.78855 5.36676C7.28255 5.56561 7.64455 5.7359 7.87455 5.87662C8.10455 6.01939 8.28855 6.13869 8.42755 6.23557C9.24052 6.00486 10.0807 5.88889 10.9245 5.8909C11.7835 5.8909 12.6155 6.00613 13.4225 6.23557L13.9165 5.91741C14.2965 5.68476 14.6973 5.48946 15.1135 5.33413C15.5735 5.15669 15.9235 5.10877 16.1675 5.18831C16.5575 6.18764 16.5915 7.08908 16.2705 7.89365C16.9555 8.64519 17.2985 9.55581 17.2985 10.6265C17.2985 11.3781 17.2095 12.044 17.0315 12.6221C16.8545 13.2013 16.6245 13.6653 16.3425 14.0151C16.049 14.3739 15.6917 14.6731 15.2895 14.8972C14.8695 15.1358 14.4615 15.3081 14.0665 15.4142C13.6288 15.5284 13.1832 15.6085 12.7335 15.6538C13.1835 16.0515 13.4095 16.6786 13.4095 17.5362V20.7076C13.4095 20.8575 13.4305 20.9788 13.4745 21.0716C13.4948 21.1163 13.5236 21.1564 13.5593 21.1895C13.5951 21.2226 13.637 21.2481 13.6825 21.2643C13.7785 21.299 13.8625 21.3215 13.9365 21.3296C14.0105 21.3398 14.1165 21.3429 14.2545 21.3429H17.1685C18.2005 21.3429 19.0825 20.9696 19.8155 20.2222C20.5475 19.4757 20.9145 18.5753 20.9145 17.523V4.79061C20.9145 3.73825 20.5485 2.83885 19.8155 2.09139Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="600" height="450" viewBox="0 0 600 450" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25 25H575M25 225H575M25 425H575" stroke="#949494" stroke-opacity="0.54" stroke-width="50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 256 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21" fill="none">
|
||||
<path d="M10.9355 0.442139C5.41555 0.442139 0.935547 5.01053 0.935547 10.6394C0.935547 16.2683 5.41555 20.8367 10.9355 20.8367C16.4555 20.8367 20.9355 16.2683 20.9355 10.6394C20.9355 5.01053 16.4555 0.442139 10.9355 0.442139ZM11.9355 15.7381H9.93555V9.61971H11.9355V15.7381ZM11.9355 7.58025H9.93555V5.54079H11.9355V7.58025Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 446 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="22" height="12" viewBox="0 0 22 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.65 4C10.83 1.67 8.61 0 6 0C2.69 0 0 2.69 0 6C0 9.31 2.69 12 6 12C8.61 12 10.83 10.33 11.65 8H16V12H20V8H22V4H11.65ZM6 8C4.9 8 4 7.1 4 6C4 4.9 4.9 4 6 4C7.1 4 8 4.9 8 6C8 7.1 7.1 8 6 8Z" fill="black" fill-opacity="0.54"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 337 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 4H4C2.9 4 2 4.9 2 6V24L6 20H20C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4ZM20 18H6L4 20V6H20V18Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 232 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM18 14H4L2 16V2H18V14Z" fill="gray" fill-opacity="0.80"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 248 B |
BIN
frontend/src/assets/notification-bg.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.25 5.25H12.25L10.5 3.5H5.25C4.2875 3.5 3.50875 4.2875 3.50875 5.25L3.5 15.75C3.5 16.7125 4.2875 17.5 5.25 17.5H19.25C20.2125 17.5 21 16.7125 21 15.75V7C21 6.0375 20.2125 5.25 19.25 5.25ZM19.25 15.75H5.25V5.25H9.77375L11.5238 7H19.25V15.75ZM17.5 10.5H7V8.75H17.5V10.5ZM14 14H7V12.25H14V14Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 423 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.6294 0.371048C15.4618 0.204652 15.2516 0.0876771 15.0218 0.0329776C14.7921 -0.0217218 14.5517 -0.012028 14.3271 0.0609937L1.36688 4.38403C1.12652 4.4602 0.914122 4.60588 0.756509 4.80269C0.598897 4.9995 0.503143 5.2386 0.481341 5.48979C0.459539 5.74099 0.512667 5.99301 0.634015 6.21403C0.755364 6.43505 0.939488 6.61515 1.16313 6.73159L6.54036 9.38919L9.19796 14.7841C9.30478 14.9953 9.46824 15.1726 9.67006 15.2962C9.87187 15.4198 10.1041 15.4848 10.3407 15.484H10.4293C10.6828 15.4653 10.9247 15.3708 11.1238 15.2129C11.3228 15.0549 11.4698 14.8407 11.5455 14.5981L15.9306 1.67327C16.0089 1.44998 16.0221 1.20903 15.9688 0.978485C15.9155 0.747942 15.7978 0.537288 15.6294 0.371048ZM1.91612 5.60653L13.2287 1.83273L6.94786 8.11354L1.91612 5.60653ZM10.4027 14.0843L7.88688 9.05256L14.1677 2.77175L10.4027 14.0843Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 947 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="21" height="18" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.00999999 18L21 9L0.00999999 0L0 7L15 9L0 11L0.00999999 18Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 192 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22" fill="none">
|
||||
<path d="M10.5024 14.1916C9.50037 14.1916 8.5394 13.8044 7.83087 13.1152C7.12234 12.426 6.72429 11.4913 6.72429 10.5166C6.72429 9.54193 7.12234 8.60718 7.83087 7.91799C8.5394 7.22879 9.50037 6.8416 10.5024 6.8416C11.5044 6.8416 12.4654 7.22879 13.1739 7.91799C13.8824 8.60718 14.2805 9.54193 14.2805 10.5166C14.2805 11.4913 13.8824 12.426 13.1739 13.1152C12.4654 13.8044 11.5044 14.1916 10.5024 14.1916ZM18.5227 11.5351C18.5659 11.1991 18.5983 10.8631 18.5983 10.5166C18.5983 10.1701 18.5659 9.8236 18.5227 9.4666L20.8004 7.7551C21.0055 7.5976 21.0595 7.3141 20.9299 7.0831L18.771 3.4501C18.6415 3.2191 18.35 3.1246 18.1125 3.2191L15.4247 4.2691C14.8634 3.8596 14.2805 3.5026 13.6004 3.2401L13.201 0.457604C13.1791 0.333931 13.1129 0.221802 13.0141 0.141083C12.9153 0.0603633 12.7903 0.0162658 12.6613 0.0166035H8.34347C8.07361 0.0166035 7.84692 0.205604 7.80374 0.457604L7.40435 3.2401C6.72429 3.5026 6.14138 3.8596 5.58007 4.2691L2.89222 3.2191C2.65474 3.1246 2.36329 3.2191 2.23375 3.4501L0.0748404 7.0831C-0.0654889 7.3141 -0.000721403 7.5976 0.204375 7.7551L2.48203 9.4666C2.43885 9.8236 2.40647 10.1701 2.40647 10.5166C2.40647 10.8631 2.43885 11.1991 2.48203 11.5351L0.204375 13.2781C-0.000721403 13.4356 -0.0654889 13.7191 0.0748404 13.9501L2.23375 17.5831C2.36329 17.8141 2.65474 17.8981 2.89222 17.8141L5.58007 16.7536C6.14138 17.1736 6.72429 17.5306 7.40435 17.7931L7.80374 20.5756C7.84692 20.8276 8.07361 21.0166 8.34347 21.0166H12.6613C12.9312 21.0166 13.1578 20.8276 13.201 20.5756L13.6004 17.7931C14.2805 17.5201 14.8634 17.1736 15.4247 16.7536L18.1125 17.8141C18.35 17.8981 18.6415 17.8141 18.771 17.5831L20.9299 13.9501C21.0595 13.7191 21.0055 13.4356 20.8004 13.2781L18.5227 11.5351Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
16
frontend/src/assets/sharepoint.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" id="b" xmlns="http://www.w3.org/2000/svg" fill="#000000" stroke="#000000" stroke-width="3.312">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
|
||||
<defs>
|
||||
|
||||
<style>.c{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style>
|
||||
|
||||
</defs>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="58" height="49" viewBox="0 0 58 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M33.0002 45.3337H6.3335C5.27263 45.3337 4.25521 44.9122 3.50507 44.1621C2.75492 43.4119 2.3335 42.3945 2.3335 41.3337V6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V24.0003M42.3335 40.0003L49.0002 46.667M49.0002 46.667L55.6668 40.0003M49.0002 46.667V30.667" stroke="#949494" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.3335 6.66699C2.3335 5.60613 2.75492 4.58871 3.50507 3.83856C4.25521 3.08842 5.27263 2.66699 6.3335 2.66699H51.6668C52.7277 2.66699 53.7451 3.08842 54.4953 3.83856C55.2454 4.58871 55.6668 5.60613 55.6668 6.66699V18.667H2.3335V6.66699Z" stroke="#949494" stroke-width="4"/>
|
||||
<path d="M7.66667 10.6673C7.66667 9.19456 8.86057 8.00065 10.3333 8.00065C11.8061 8.00065 13 9.19456 13 10.6673C13 12.1401 11.8061 13.334 10.3333 13.334C8.86057 13.334 7.66667 12.1401 7.66667 10.6673Z" fill="#949494"/>
|
||||
<path d="M15.6667 10.6673C15.6667 9.19456 16.8606 8.00065 18.3333 8.00065C19.8061 8.00065 21 9.19456 21 10.6673C21 12.1401 19.8061 13.334 18.3333 13.334C16.8606 13.334 15.6667 12.1401 15.6667 10.6673Z" fill="#949494"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
13
frontend/src/components/ConnectedStateSkeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
const ConnectedStateSkeleton = () => (
|
||||
<div className="mb-4">
|
||||
<div className="flex w-full animate-pulse items-center justify-between rounded-[10px] bg-gray-200 px-4 py-2 dark:bg-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-4 w-32 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
<div className="h-4 w-16 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ConnectedStateSkeleton;
|
||||
@@ -150,7 +150,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
{isConnected ? (
|
||||
<div className="mb-4">
|
||||
<div className="flex w-full items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-sm font-medium text-[#212121]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex max-w-[500px] items-center gap-2">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Chunks from './Chunks';
|
||||
import ContextMenu, { MenuOption } from './ContextMenu';
|
||||
import SkeletonLoader from './SkeletonLoader';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import userService from '../api/services/userService';
|
||||
import FileIcon from '../assets/file.svg';
|
||||
@@ -15,7 +16,7 @@ import ThreeDots from '../assets/three-dots.svg';
|
||||
import EyeView from '../assets/eye-view.svg';
|
||||
import SyncIcon from '../assets/sync.svg';
|
||||
import CheckmarkIcon from '../assets/checkMark2.svg';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
import { useOutsideAlerter, useLoaderState } from '../hooks';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
@@ -55,7 +56,7 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
onBackToDocuments,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useLoaderState(true, 500);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [directoryStructure, setDirectoryStructure] =
|
||||
useState<DirectoryStructure | null>(null);
|
||||
@@ -716,7 +717,13 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{renderFileTree(getCurrentDirectory())}</TableBody>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<SkeletonLoader component="fileTable" />
|
||||
) : (
|
||||
renderFileTree(getCurrentDirectory())
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
13
frontend/src/components/FileSelectionSkeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
const FilesSectionSkeleton = () => (
|
||||
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div className="h-8 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="h-4 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default FilesSectionSkeleton;
|
||||
@@ -5,6 +5,7 @@ import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { formatBytes } from '../utils/stringUtils';
|
||||
import Chunks from './Chunks';
|
||||
import ContextMenu, { MenuOption } from './ContextMenu';
|
||||
import SkeletonLoader from './SkeletonLoader';
|
||||
import userService from '../api/services/userService';
|
||||
import FileIcon from '../assets/file.svg';
|
||||
import FolderIcon from '../assets/folder.svg';
|
||||
@@ -12,7 +13,7 @@ import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import ThreeDots from '../assets/three-dots.svg';
|
||||
import EyeView from '../assets/eye-view.svg';
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
import { useOutsideAlerter, useLoaderState } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import {
|
||||
Table,
|
||||
@@ -53,7 +54,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
onBackToDocuments,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useLoaderState(true, 500);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [directoryStructure, setDirectoryStructure] =
|
||||
useState<DirectoryStructure | null>(null);
|
||||
@@ -839,7 +840,13 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{renderFileTree(currentDirectory)}</TableBody>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<SkeletonLoader component="fileTable" />
|
||||
) : (
|
||||
renderFileTree(currentDirectory)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ export const FileUpload = ({
|
||||
const filesToUpload = multiple ? acceptedFiles : [acceptedFiles[0]];
|
||||
onUpload(filesToUpload);
|
||||
|
||||
const file = multiple ? acceptedFiles[0] : acceptedFiles[0];
|
||||
const file = acceptedFiles[0];
|
||||
setCurrentFile(file);
|
||||
|
||||
if (showPreview && file.type.startsWith('image/')) {
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
getSessionToken,
|
||||
setSessionToken,
|
||||
removeSessionToken,
|
||||
validateProviderSession,
|
||||
} from '../utils/providerUtils';
|
||||
import ConnectedStateSkeleton from './ConnectedStateSkeleton';
|
||||
import FilesSectionSkeleton from './FileSelectionSkeleton';
|
||||
|
||||
interface PickerFile {
|
||||
id: string;
|
||||
@@ -50,20 +53,9 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
|
||||
const validateSession = async (sessionToken: string) => {
|
||||
try {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
const validateResponse = await fetch(
|
||||
`${apiHost}/api/connectors/validate-session`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: 'google_drive',
|
||||
session_token: sessionToken,
|
||||
}),
|
||||
},
|
||||
const validateResponse = await validateProviderSession(
|
||||
token,
|
||||
'google_drive',
|
||||
);
|
||||
|
||||
if (!validateResponse.ok) {
|
||||
@@ -234,30 +226,6 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
onSelectionChange([], []);
|
||||
};
|
||||
|
||||
const ConnectedStateSkeleton = () => (
|
||||
<div className="mb-4">
|
||||
<div className="flex w-full animate-pulse items-center justify-between rounded-[10px] bg-gray-200 px-4 py-2 dark:bg-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-4 w-32 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
<div className="h-4 w-16 rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FilesSectionSkeleton = () => (
|
||||
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div className="h-8 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="h-4 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isValidating ? (
|
||||
|
||||
@@ -7,11 +7,9 @@ import userService from '../api/services/userService';
|
||||
import AlertIcon from '../assets/alert.svg';
|
||||
import ClipIcon from '../assets/clip.svg';
|
||||
import ExitIcon from '../assets/exit.svg';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SendArrowIcon from './SendArrowIcon';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import DocumentationDark from '../assets/documentation-dark.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import {
|
||||
addAttachment,
|
||||
@@ -19,7 +17,7 @@ import {
|
||||
selectAttachments,
|
||||
updateAttachment,
|
||||
} from '../upload/uploadSlice';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
selectSelectedDocs,
|
||||
@@ -29,6 +27,7 @@ import Upload from '../upload/Upload';
|
||||
import { getOS, isTouchDevice } from '../utils/browserUtils';
|
||||
import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
import { handleAbort } from '../conversation/conversationSlice';
|
||||
|
||||
type MessageInputProps = {
|
||||
onSubmit: (text: string) => void;
|
||||
@@ -46,7 +45,6 @@ export default function MessageInput({
|
||||
autoFocus = true,
|
||||
}: MessageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [value, setValue] = useState('');
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const sourceButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -256,6 +254,11 @@ export default function MessageInput({
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
handleAbort();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-2 flex w-full flex-col">
|
||||
<div className="border-dark-gray bg-lotion dark:border-grey relative flex w-full flex-col rounded-[23px] border dark:bg-transparent">
|
||||
@@ -427,26 +430,33 @@ export default function MessageInput({
|
||||
{/* Additional badges can be added here in the future */}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={loading ? undefined : handleSubmit}
|
||||
aria-label={loading ? t('loading') : t('send')}
|
||||
className={`flex h-7 w-7 items-center justify-center rounded-full sm:h-9 sm:w-9 ${loading || !value.trim() ? 'bg-black opacity-60 dark:bg-[#F0F3F4] dark:opacity-80' : 'bg-black opacity-100 dark:bg-[#F0F3F4]'} ml-auto shrink-0`}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<img
|
||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||
className="mx-auto my-auto block h-3.5 w-3.5 animate-spin sm:h-4 sm:w-4"
|
||||
alt={t('loading')}
|
||||
{loading ? (
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
aria-label={t('cancel')}
|
||||
className={`ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#7F54D6] text-white sm:h-9 sm:w-9`}
|
||||
disabled={!loading}
|
||||
>
|
||||
<div className="flex h-3 w-3 items-center justify-center rounded-[3px] bg-white sm:h-3.5 sm:w-3.5" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
aria-label={t('send')}
|
||||
className={`ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full transition-colors duration-300 ease-in-out sm:h-9 sm:w-9 ${
|
||||
value.trim() && !loading
|
||||
? 'bg-purple-30 text-white'
|
||||
: 'bg-[#EDEDED] text-[#959595] dark:bg-[#37383D] dark:text-[#77787D]'
|
||||
}`}
|
||||
disabled={!value.trim() || loading}
|
||||
>
|
||||
<SendArrowIcon
|
||||
className="mx-auto my-auto block h-3.5 w-3.5 sm:h-4 sm:w-4"
|
||||
aria-label={t('send')}
|
||||
role="img"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={`mx-auto my-auto block h-3.5 w-3.5 translate-x-[-0.9px] translate-y-[1.1px] sm:h-4 sm:w-4 ${isDarkTheme ? 'invert filter' : ''}`}
|
||||
src={PaperPlane}
|
||||
alt={t('send')}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
45
frontend/src/components/Notification.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import close from '../assets/cross.svg';
|
||||
import rightArrow from '../assets/arrow-full-right.svg';
|
||||
import bg from '../assets/notification-bg.jpg';
|
||||
|
||||
interface NotificationProps {
|
||||
notificationText: string;
|
||||
notificationLink: string;
|
||||
handleCloseNotification: () => void;
|
||||
}
|
||||
|
||||
export default function Notification({
|
||||
notificationText,
|
||||
notificationLink,
|
||||
handleCloseNotification,
|
||||
}: NotificationProps) {
|
||||
return (
|
||||
<a
|
||||
className="absolute right-2 bottom-6 z-20 flex w-3/4 items-center justify-center gap-2 rounded-lg bg-cover bg-center bg-no-repeat px-2 py-4 sm:right-4 md:w-2/5 lg:w-1/3 xl:w-1/4 2xl:w-1/5"
|
||||
style={{ backgroundImage: `url(${bg})` }}
|
||||
href={notificationLink}
|
||||
target="_blank"
|
||||
aria-label="Notification"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<p className="text-white-3000 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
|
||||
{notificationText}
|
||||
</p>
|
||||
<span>
|
||||
<img className="w-full" src={rightArrow} alt="" />
|
||||
</span>
|
||||
|
||||
<button
|
||||
className="absolute top-2 right-2 z-30 h-4 w-4 hover:opacity-70"
|
||||
aria-label="Close notification"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCloseNotification();
|
||||
}}
|
||||
>
|
||||
<img className="w-full" src={close} alt="Close notification" />
|
||||
</button>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
17
frontend/src/components/SendArrowIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SVGProps } from 'react';
|
||||
const SendArrowIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="14"
|
||||
viewBox="0 0 11 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M0.292786 6.20676C0.105315 6.01923 -3.59956e-07 5.76492 -3.71547e-07 5.49976C-3.83138e-07 5.23459 0.105315 4.98028 0.292786 4.79276L4.79279 0.292756C4.98031 0.105284 5.23462 -3.07464e-05 5.49979 -3.0758e-05C5.76495 -3.07696e-05 6.01926 0.105284 6.20679 0.292756L10.7068 4.79276C10.8889 4.98136 10.9897 5.23396 10.9875 5.49616C10.9852 5.75835 10.88 6.00917 10.6946 6.19457C10.5092 6.37998 10.2584 6.48515 9.99619 6.48743C9.73399 6.48971 9.48139 6.38891 9.29279 6.20676L6.49979 3.49976L6.49979 12.9998C6.49979 13.265 6.39443 13.5193 6.20689 13.7069C6.01936 13.8944 5.765 13.9998 5.49979 13.9998C5.23457 13.9998 4.98022 13.8944 4.79268 13.7069C4.60514 13.5193 4.49979 13.265 4.49979 12.9998L4.49979 3.49976L1.70679 6.20676C1.51926 6.39423 1.26495 6.49954 0.999786 6.49954C0.734622 6.49954 0.480314 6.39423 0.292786 6.20676Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SendArrowIcon;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import ShareIcon from '../assets/share.svg';
|
||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||
|
||||
type ShareButtonProps = {
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
export default function ShareButton({ conversationId }: ShareButtonProps) {
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
title="Share"
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
}}
|
||||
className="hover:bg-bright-gray absolute top-4 right-20 z-20 rounded-full dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="m-2 h-5 w-5 filter dark:invert"
|
||||
alt="share"
|
||||
src={ShareIcon}
|
||||
/>
|
||||
</button>
|
||||
{isShareModalOpen && (
|
||||
<ShareConversationModal
|
||||
close={() => {
|
||||
setShareModalState(false);
|
||||
}}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
175
frontend/src/components/SharePointPicker.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ConnectorAuth from './ConnectorAuth';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
getSessionToken,
|
||||
setSessionToken,
|
||||
removeSessionToken,
|
||||
validateProviderSession,
|
||||
} from '../utils/providerUtils';
|
||||
import ConnectedStateSkeleton from './ConnectedStateSkeleton';
|
||||
import FilesSectionSkeleton from './FileSelectionSkeleton';
|
||||
|
||||
interface SharePointPickerProps {
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
const SharePointPicker: React.FC<SharePointPickerProps> = ({ token }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [authError, setAuthError] = useState<string>('');
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const sessionToken = getSessionToken('share_point');
|
||||
if (sessionToken) {
|
||||
setIsValidating(true);
|
||||
setIsConnected(true); // Optimistically set as connected for skeleton
|
||||
validateSession(sessionToken);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const validateSession = async (sessionToken: string) => {
|
||||
try {
|
||||
const validateResponse = await validateProviderSession(
|
||||
token,
|
||||
'share_point',
|
||||
);
|
||||
|
||||
if (!validateResponse.ok) {
|
||||
setIsConnected(false);
|
||||
setAuthError(
|
||||
t('modals.uploadDoc.connectors.sharePoint.sessionExpired'),
|
||||
);
|
||||
setIsValidating(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const validateData = await validateResponse.json();
|
||||
if (validateData.success) {
|
||||
setUserEmail(
|
||||
validateData.user_email ||
|
||||
t('modals.uploadDoc.connectors.auth.connectedUser'),
|
||||
);
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
setAccessToken(validateData.access_token || null);
|
||||
setIsValidating(false);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
setIsConnected(false);
|
||||
setAuthError(
|
||||
validateData.error ||
|
||||
t('modals.uploadDoc.connectors.sharePoint.sessionExpiredGeneric'),
|
||||
);
|
||||
setIsValidating(false);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating session:', error);
|
||||
setAuthError(t('modals.uploadDoc.connectors.sharePoint.validateFailed'));
|
||||
setIsConnected(false);
|
||||
setIsValidating(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
const sessionToken = getSessionToken('share_point');
|
||||
if (sessionToken) {
|
||||
try {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
await fetch(`${apiHost}/api/connectors/disconnect`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: 'share_point',
|
||||
session_token: sessionToken,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error disconnecting from SharePoint:', err);
|
||||
}
|
||||
}
|
||||
|
||||
removeSessionToken('share_point');
|
||||
setIsConnected(false);
|
||||
setAccessToken(null);
|
||||
setUserEmail('');
|
||||
setAuthError('');
|
||||
};
|
||||
|
||||
const handleOpenPicker = async () => {
|
||||
alert('Feature not supported yet.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isValidating ? (
|
||||
<>
|
||||
<ConnectedStateSkeleton />
|
||||
<FilesSectionSkeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ConnectorAuth
|
||||
provider="share_point"
|
||||
label={t('modals.uploadDoc.connectors.sharePoint.connect')}
|
||||
onSuccess={(data) => {
|
||||
setUserEmail(
|
||||
data.user_email ||
|
||||
t('modals.uploadDoc.connectors.auth.connectedUser'),
|
||||
);
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
|
||||
if (data.session_token) {
|
||||
setSessionToken('share_point', data.session_token);
|
||||
validateSession(data.session_token);
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
setAuthError(error);
|
||||
setIsConnected(false);
|
||||
}}
|
||||
isConnected={isConnected}
|
||||
userEmail={userEmail}
|
||||
onDisconnect={handleDisconnect}
|
||||
errorMessage={authError}
|
||||
/>
|
||||
|
||||
{isConnected && (
|
||||
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t('modals.uploadDoc.connectors.sharePoint.selectedFiles')}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => handleOpenPicker()}
|
||||
className="rounded-md bg-[#A076F6] px-3 py-1 text-sm text-white hover:bg-[#8A5FD4]"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? t('modals.uploadDoc.connectors.sharePoint.loading')
|
||||
: t('modals.uploadDoc.connectors.sharePoint.selectFiles')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharePointPicker;
|
||||
@@ -6,7 +6,7 @@ interface SkeletonLoaderProps {
|
||||
| 'default'
|
||||
| 'analysis'
|
||||
| 'logs'
|
||||
| 'table'
|
||||
| 'fileTable'
|
||||
| 'chatbot'
|
||||
| 'dropdown'
|
||||
| 'chunkCards'
|
||||
@@ -44,15 +44,15 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
<>
|
||||
{[...Array(4)].map((_, idx) => (
|
||||
<tr key={idx} className="animate-pulse">
|
||||
<td className="w-[45%] px-4 py-4">
|
||||
<td className="w-[40%] px-4 py-4">
|
||||
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||
</td>
|
||||
<td className="w-[30%] px-4 py-4">
|
||||
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||
</td>
|
||||
<td className="w-[20%] px-4 py-4">
|
||||
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||
</td>
|
||||
<td className="w-[25%] px-4 py-4">
|
||||
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||
</td>
|
||||
<td className="w-[10%] px-4 py-4">
|
||||
<div className="h-4 w-full rounded-sm bg-gray-300 dark:bg-gray-600"></div>
|
||||
</td>
|
||||
@@ -241,7 +241,7 @@ const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
|
||||
);
|
||||
|
||||
const componentMap = {
|
||||
table: renderTable,
|
||||
fileTable: renderTable,
|
||||
chatbot: renderChatbot,
|
||||
dropdown: renderDropdown,
|
||||
logs: renderLogs,
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
||||
import { Doc, ActiveState } from '../models/misc';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
|
||||
type Props = {
|
||||
options: Doc[] | null;
|
||||
selectedDocs: Doc | null;
|
||||
setSelectedDocs: any;
|
||||
isDocsListOpen: boolean;
|
||||
setIsDocsListOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteClick: any;
|
||||
handlePostDocumentSelect: any;
|
||||
};
|
||||
|
||||
function SourceDropdown({
|
||||
options,
|
||||
setSelectedDocs,
|
||||
selectedDocs,
|
||||
setIsDocsListOpen,
|
||||
isDocsListOpen,
|
||||
handleDeleteClick,
|
||||
handlePostDocumentSelect, // Callback function fired after a document is selected
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const embeddingsName =
|
||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const [documentToDelete, setDocumentToDelete] = useState<Doc | null>(null);
|
||||
|
||||
const handleEmptyDocumentSelect = () => {
|
||||
dispatch(setSelectedDocs(null));
|
||||
setIsDocsListOpen(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsDocsListOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const confirmDelete = (option: Doc) => {
|
||||
setDocumentToDelete(option);
|
||||
setDeleteModalState('ACTIVE');
|
||||
};
|
||||
|
||||
const handleConfirmedDelete = () => {
|
||||
if (documentToDelete) {
|
||||
handleDeleteClick(documentToDelete);
|
||||
setDeleteModalState('INACTIVE');
|
||||
setDocumentToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelDelete = () => {
|
||||
setDeleteModalState('INACTIVE');
|
||||
setDocumentToDelete(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-5/6 rounded-3xl" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
|
||||
className={`border-silver flex w-full cursor-pointer items-center border bg-white p-[11px] dark:bg-transparent ${
|
||||
isDocsListOpen
|
||||
? 'dark:border-silver/40 rounded-t-3xl'
|
||||
: 'dark:border-purple-taupe rounded-3xl'
|
||||
}`}
|
||||
>
|
||||
<span className="dark:text-bright-gray mr-2 ml-1 flex-1 overflow-hidden text-left text-ellipsis">
|
||||
<div className="flex flex-row gap-2">
|
||||
<p className="max-w-3/4 truncate whitespace-nowrap">
|
||||
{selectedDocs?.name || 'None'}
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<img
|
||||
src={Arrow2}
|
||||
alt="arrow"
|
||||
className={`transform ${
|
||||
isDocsListOpen ? 'rotate-180' : 'rotate-0'
|
||||
} h-3 w-3 transition-transform`}
|
||||
/>
|
||||
</button>
|
||||
{isDocsListOpen && (
|
||||
<div className="border-silver dark:border-silver/40 dark:bg-dark-charcoal absolute right-0 left-0 z-20 -mt-1 max-h-28 overflow-y-auto rounded-b-xl border bg-white shadow-lg">
|
||||
{options ? (
|
||||
options.map((option: any, index: number) => {
|
||||
if (option.model === embeddingsName) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="dark:text-bright-gray flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:hover:bg-[#545561]"
|
||||
onClick={() => {
|
||||
dispatch(setSelectedDocs(option));
|
||||
setIsDocsListOpen(false);
|
||||
handlePostDocumentSelect(option);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onClick={() => {
|
||||
setIsDocsListOpen(false);
|
||||
}}
|
||||
className="ml-4 flex-1 overflow-hidden py-3 text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{option.name}
|
||||
</span>
|
||||
{option.location === 'local' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
confirmDelete(option);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div
|
||||
className="dark:text-bright-gray dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-between hover:bg-gray-100"
|
||||
onClick={handleEmptyDocumentSelect}
|
||||
>
|
||||
<span
|
||||
className="ml-4 flex-1 overflow-hidden py-3 text-ellipsis whitespace-nowrap"
|
||||
onClick={() => {
|
||||
handlePostDocumentSelect(null);
|
||||
}}
|
||||
>
|
||||
{t('none')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
message={t('settings.sources.deleteWarning', {
|
||||
name: documentToDelete?.name,
|
||||
})}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={handleConfirmedDelete}
|
||||
handleCancel={handleCancelDelete}
|
||||
submitLabel={t('convTile.delete')}
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SourceDropdown;
|
||||
@@ -37,7 +37,7 @@ export default function UploadToast() {
|
||||
case 'completed':
|
||||
return t('modals.uploadDoc.progress.completed');
|
||||
case 'failed':
|
||||
return t('attachments.uploadFailed');
|
||||
return t('modals.uploadDoc.progress.failed');
|
||||
default:
|
||||
return t('modals.uploadDoc.progress.preparing');
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ export default function Conversation() {
|
||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false);
|
||||
}
|
||||
}, [queries[queries.length - 1]]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-end gap-1">
|
||||
<ConversationMessages
|
||||
|
||||
@@ -556,63 +556,67 @@ const ConversationBubble = forwardRef<
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative mr-2 block items-center justify-center">
|
||||
<CopyButton textToCopy={message} />
|
||||
</div>
|
||||
<div className="relative mr-2 block items-center justify-center">
|
||||
<SpeakButton text={message} />
|
||||
</div>
|
||||
{handleFeedback && (
|
||||
{!isStreaming && (
|
||||
<>
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isLikeHovered
|
||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
||||
: 'bg-white-3000 dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Like
|
||||
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (feedback === 'LIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('LIKE');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsLikeHovered(true)}
|
||||
onMouseLeave={() => setIsLikeHovered(false)}
|
||||
></Like>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mr-2 block items-center justify-center">
|
||||
<CopyButton textToCopy={message} />
|
||||
</div>
|
||||
<div className="relative mr-2 block items-center justify-center">
|
||||
<SpeakButton text={message} />
|
||||
</div>
|
||||
{handleFeedback && (
|
||||
<>
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isLikeHovered
|
||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
||||
: 'bg-white-3000 dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Like
|
||||
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (feedback === 'LIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('LIKE');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsLikeHovered(true)}
|
||||
onMouseLeave={() => setIsLikeHovered(false)}
|
||||
></Like>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isDislikeHovered
|
||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
||||
: 'bg-white-3000 dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Dislike
|
||||
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (feedback === 'DISLIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('DISLIKE');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsDislikeHovered(true)}
|
||||
onMouseLeave={() => setIsDislikeHovered(false)}
|
||||
></Dislike>
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isDislikeHovered
|
||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
||||
: 'bg-white-3000 dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Dislike
|
||||
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (feedback === 'DISLIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('DISLIKE');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsDislikeHovered(true)}
|
||||
onMouseLeave={() => setIsDislikeHovered(false)}
|
||||
></Dislike>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ export function handleFetchAnswer(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
token: string | null,
|
||||
selectedDocs: Doc[] | null,
|
||||
selectedDocs: Doc[],
|
||||
conversationId: string | null,
|
||||
promptId: string | null,
|
||||
chunks: string,
|
||||
@@ -42,7 +42,7 @@ export function handleFetchAnswer(
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
isNoneDoc: selectedDocs.length === 0,
|
||||
agent_id: agentId,
|
||||
save_conversation: save_conversation,
|
||||
};
|
||||
@@ -52,7 +52,7 @@ export function handleFetchAnswer(
|
||||
payload.attachments = attachments;
|
||||
}
|
||||
|
||||
if (selectedDocs && Array.isArray(selectedDocs)) {
|
||||
if (selectedDocs.length > 0) {
|
||||
if (selectedDocs.length > 1) {
|
||||
// Handle multiple documents
|
||||
payload.active_docs = selectedDocs.map((doc) => doc.id!);
|
||||
@@ -91,7 +91,7 @@ export function handleFetchAnswerSteaming(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
token: string | null,
|
||||
selectedDocs: Doc[] | null,
|
||||
selectedDocs: Doc[],
|
||||
conversationId: string | null,
|
||||
promptId: string | null,
|
||||
chunks: string,
|
||||
@@ -108,7 +108,7 @@ export function handleFetchAnswerSteaming(
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
isNoneDoc: selectedDocs.length === 0,
|
||||
index: indx,
|
||||
agent_id: agentId,
|
||||
save_conversation: save_conversation,
|
||||
@@ -119,7 +119,7 @@ export function handleFetchAnswerSteaming(
|
||||
payload.attachments = attachments;
|
||||
}
|
||||
|
||||
if (selectedDocs && Array.isArray(selectedDocs)) {
|
||||
if (selectedDocs.length > 0) {
|
||||
if (selectedDocs.length > 1) {
|
||||
// Handle multiple documents
|
||||
payload.active_docs = selectedDocs.map((doc) => doc.id!);
|
||||
@@ -185,7 +185,7 @@ export function handleFetchAnswerSteaming(
|
||||
export function handleSearch(
|
||||
question: string,
|
||||
token: string | null,
|
||||
selectedDocs: Doc[] | null,
|
||||
selectedDocs: Doc[],
|
||||
conversation_id: string | null,
|
||||
chunks: string,
|
||||
token_limit: number,
|
||||
@@ -195,9 +195,9 @@ export function handleSearch(
|
||||
conversation_id: conversation_id,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
isNoneDoc: selectedDocs === null,
|
||||
isNoneDoc: selectedDocs.length === 0,
|
||||
};
|
||||
if (selectedDocs && Array.isArray(selectedDocs)) {
|
||||
if (selectedDocs.length > 0) {
|
||||
if (selectedDocs.length > 1) {
|
||||
// Handle multiple documents
|
||||
payload.active_docs = selectedDocs.map((doc) => doc.id!);
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import { Prompt } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
|
||||
type UsePromptManagerProps = {
|
||||
initialPrompts: Prompt[];
|
||||
onPromptSelect: (name: string, id: string, type: string) => void;
|
||||
onPromptsUpdate: (updatedPrompts: Prompt[]) => void;
|
||||
};
|
||||
|
||||
type PromptContentResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type PromptCreateResponse = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const usePromptManager = ({
|
||||
initialPrompts,
|
||||
onPromptSelect,
|
||||
onPromptsUpdate,
|
||||
}: UsePromptManagerProps) => {
|
||||
const token = useSelector(selectToken);
|
||||
|
||||
const [prompts, setPrompts] = useState<Prompt[]>(initialPrompts);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setPrompts(initialPrompts);
|
||||
}, [initialPrompts]);
|
||||
|
||||
const handleApiCall = async <T>(
|
||||
apiCall: () => Promise<Response>,
|
||||
errorMessage: string,
|
||||
): Promise<T | null> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await apiCall();
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
console.error(`${errorMessage}: ${response.status} ${errorData}`);
|
||||
throw new Error(`${errorMessage} (Status: ${response.status})`);
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message);
|
||||
console.error(err);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addPrompt = useCallback(
|
||||
async (name: string, content: string): Promise<Prompt | null> => {
|
||||
const newPromptData = await handleApiCall<PromptCreateResponse>(
|
||||
() => userService.createPrompt({ name, content }, token),
|
||||
'Failed to add prompt',
|
||||
);
|
||||
|
||||
if (newPromptData) {
|
||||
const newPrompt: Prompt = {
|
||||
name,
|
||||
id: newPromptData.id,
|
||||
type: 'private',
|
||||
};
|
||||
const updatedPrompts = [...prompts, newPrompt];
|
||||
setPrompts(updatedPrompts);
|
||||
onPromptsUpdate(updatedPrompts);
|
||||
onPromptSelect(newPrompt.name, newPrompt.id, newPrompt.type);
|
||||
return newPrompt;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[token, prompts, onPromptsUpdate, onPromptSelect],
|
||||
);
|
||||
|
||||
const deletePrompt = useCallback(
|
||||
async (idToDelete: string): Promise<void> => {
|
||||
const originalPrompts = [...prompts];
|
||||
const updatedPrompts = prompts.filter(
|
||||
(prompt) => prompt.id !== idToDelete,
|
||||
);
|
||||
setPrompts(updatedPrompts);
|
||||
onPromptsUpdate(updatedPrompts);
|
||||
|
||||
const result = await handleApiCall<null>(
|
||||
() => userService.deletePrompt({ id: idToDelete }, token),
|
||||
'Failed to delete prompt',
|
||||
);
|
||||
|
||||
if (result === null && error) {
|
||||
setPrompts(originalPrompts);
|
||||
onPromptsUpdate(originalPrompts);
|
||||
} else {
|
||||
if (updatedPrompts.length > 0) {
|
||||
onPromptSelect(
|
||||
updatedPrompts[0].name,
|
||||
updatedPrompts[0].id,
|
||||
updatedPrompts[0].type,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[token, prompts, onPromptsUpdate, onPromptSelect, error],
|
||||
);
|
||||
|
||||
const fetchPromptContent = useCallback(
|
||||
async (id: string): Promise<string | null> => {
|
||||
const promptDetails = await handleApiCall<PromptContentResponse>(
|
||||
() => userService.getSinglePrompt(id, token),
|
||||
'Failed to fetch prompt content',
|
||||
);
|
||||
return promptDetails ? promptDetails.content : null;
|
||||
},
|
||||
[token],
|
||||
);
|
||||
|
||||
const updatePrompt = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
name: string,
|
||||
content: string,
|
||||
type: string,
|
||||
): Promise<boolean> => {
|
||||
const result = await handleApiCall<{ success: boolean }>(
|
||||
() => userService.updatePrompt({ id, name, content }, token),
|
||||
'Failed to update prompt',
|
||||
);
|
||||
|
||||
if (result?.success) {
|
||||
const updatedPrompts = prompts.map((p) =>
|
||||
p.id === id ? { ...p, name, type } : p,
|
||||
);
|
||||
setPrompts(updatedPrompts);
|
||||
onPromptsUpdate(updatedPrompts);
|
||||
onPromptSelect(name, id, type);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[token, prompts, onPromptsUpdate, onPromptSelect],
|
||||
);
|
||||
|
||||
return {
|
||||
prompts,
|
||||
isLoading,
|
||||
error,
|
||||
addPrompt,
|
||||
deletePrompt,
|
||||
fetchPromptContent,
|
||||
updatePrompt,
|
||||
setError,
|
||||
};
|
||||
};
|
||||
@@ -262,6 +262,7 @@
|
||||
"upload": "Upload is in progress",
|
||||
"training": "Upload is in progress",
|
||||
"completed": "Upload completed",
|
||||
"failed": "Upload failed",
|
||||
"wait": "This may take several minutes",
|
||||
"preparing": "Preparing upload",
|
||||
"tokenLimit": "Over the token limit, please consider uploading smaller document",
|
||||
@@ -297,6 +298,10 @@
|
||||
"google_drive": {
|
||||
"label": "Google Drive",
|
||||
"heading": "Upload from Google Drive"
|
||||
},
|
||||
"share_point": {
|
||||
"label": "SharePoint",
|
||||
"heading": "Upload from SharePoint"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
@@ -326,6 +331,24 @@
|
||||
"remove": "Remove",
|
||||
"folderAlt": "Folder",
|
||||
"fileAlt": "File"
|
||||
},
|
||||
"sharePoint": {
|
||||
"connect": "Connect to SharePoint",
|
||||
"sessionExpired": "Session expired. Please reconnect to SharePoint.",
|
||||
"sessionExpiredGeneric": "Session expired. Please reconnect your account.",
|
||||
"validateFailed": "Failed to validate session. Please reconnect.",
|
||||
"noSession": "No valid session found. Please reconnect to SharePoint.",
|
||||
"noAccessToken": "No access token available. Please reconnect to SharePoint.",
|
||||
"pickerFailed": "Failed to open file picker. Please try again.",
|
||||
"selectedFiles": "Selected Files",
|
||||
"selectFiles": "Select Files",
|
||||
"loading": "Loading...",
|
||||
"noFilesSelected": "No files or folders selected",
|
||||
"folders": "Folders",
|
||||
"files": "Files",
|
||||
"remove": "Remove",
|
||||
"folderAlt": "Folder",
|
||||
"fileAlt": "File"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -424,8 +447,7 @@
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "Attach",
|
||||
"remove": "Remove attachment",
|
||||
"uploadFailed": "Upload failed"
|
||||
"remove": "Remove attachment"
|
||||
},
|
||||
"retry": "Retry"
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"upload": "Subida en progreso",
|
||||
"training": "Subida en progreso",
|
||||
"completed": "Subida completada",
|
||||
"failed": "Error al subir",
|
||||
"wait": "Esto puede tardar varios minutos",
|
||||
"preparing": "Preparando subida",
|
||||
"tokenLimit": "Excede el límite de tokens, considere cargar un documento más pequeño",
|
||||
@@ -260,6 +261,10 @@
|
||||
"google_drive": {
|
||||
"label": "Google Drive",
|
||||
"heading": "Subir desde Google Drive"
|
||||
},
|
||||
"share_point": {
|
||||
"label": "SharePoint",
|
||||
"heading": "Subir desde SharePoint"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
@@ -289,6 +294,24 @@
|
||||
"remove": "Eliminar",
|
||||
"folderAlt": "Carpeta",
|
||||
"fileAlt": "Archivo"
|
||||
},
|
||||
"sharePoint": {
|
||||
"connect": "Conectar a SharePoint",
|
||||
"sessionExpired": "Sesión expirada. Por favor, reconecte a SharePoint.",
|
||||
"sessionExpiredGeneric": "Sesión expirada. Por favor, reconecte su cuenta.",
|
||||
"validateFailed": "Error al validar la sesión. Por favor, reconecte.",
|
||||
"noSession": "No se encontró una sesión válida. Por favor, reconecte a SharePoint.",
|
||||
"noAccessToken": "No hay token de acceso disponible. Por favor, reconecte a SharePoint.",
|
||||
"pickerFailed": "Error al abrir el selector de archivos. Por favor, inténtelo de nuevo.",
|
||||
"selectedFiles": "Archivos Seleccionados",
|
||||
"selectFiles": "Seleccionar Archivos",
|
||||
"loading": "Cargando...",
|
||||
"noFilesSelected": "No hay archivos o carpetas seleccionados",
|
||||
"folders": "Carpetas",
|
||||
"files": "Archivos",
|
||||
"remove": "Eliminar",
|
||||
"folderAlt": "Carpeta",
|
||||
"fileAlt": "Archivo"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -387,8 +410,7 @@
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "Adjuntar",
|
||||
"remove": "Eliminar adjunto",
|
||||
"uploadFailed": "Error al subir"
|
||||
"remove": "Eliminar adjunto"
|
||||
},
|
||||
"retry": "Reintentar"
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"upload": "アップロード中",
|
||||
"training": "アップロード中",
|
||||
"completed": "アップロード完了",
|
||||
"failed": "アップロード失敗",
|
||||
"wait": "数分かかる場合があります",
|
||||
"preparing": "アップロードを準備中",
|
||||
"tokenLimit": "トークン制限を超えています。より小さいドキュメントをアップロードしてください",
|
||||
@@ -260,6 +261,10 @@
|
||||
"google_drive": {
|
||||
"label": "Google Drive",
|
||||
"heading": "Google Driveからアップロード"
|
||||
},
|
||||
"share_point": {
|
||||
"label": "SharePoint",
|
||||
"heading": "SharePointからアップロード"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
@@ -289,6 +294,24 @@
|
||||
"remove": "削除",
|
||||
"folderAlt": "フォルダ",
|
||||
"fileAlt": "ファイル"
|
||||
},
|
||||
"sharePoint": {
|
||||
"connect": "SharePointに接続",
|
||||
"sessionExpired": "セッションが期限切れです。SharePointに再接続してください。",
|
||||
"sessionExpiredGeneric": "セッションが期限切れです。アカウントに再接続してください。",
|
||||
"validateFailed": "セッションの検証に失敗しました。再接続してください。",
|
||||
"noSession": "有効なセッションが見つかりません。SharePointに再接続してください。",
|
||||
"noAccessToken": "アクセストークンが利用できません。SharePointに再接続してください。",
|
||||
"pickerFailed": "ファイルピッカーを開けませんでした。もう一度お試しください。",
|
||||
"selectedFiles": "選択されたファイル",
|
||||
"selectFiles": "ファイルを選択",
|
||||
"loading": "読み込み中...",
|
||||
"noFilesSelected": "ファイルまたはフォルダが選択されていません",
|
||||
"folders": "フォルダ",
|
||||
"files": "ファイル",
|
||||
"remove": "削除",
|
||||
"folderAlt": "フォルダ",
|
||||
"fileAlt": "ファイル"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -387,8 +410,7 @@
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "添付",
|
||||
"remove": "添付ファイルを削除",
|
||||
"uploadFailed": "アップロード失敗"
|
||||
"remove": "添付ファイルを削除"
|
||||
},
|
||||
"retry": "再試行"
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"upload": "Идет загрузка",
|
||||
"training": "Идет загрузка",
|
||||
"completed": "Загрузка завершена",
|
||||
"failed": "Ошибка загрузки",
|
||||
"wait": "Это может занять несколько минут",
|
||||
"preparing": "Подготовка загрузки",
|
||||
"tokenLimit": "Превышен лимит токенов, рассмотрите возможность загрузки документа меньшего размера",
|
||||
@@ -260,6 +261,10 @@
|
||||
"google_drive": {
|
||||
"label": "Google Drive",
|
||||
"heading": "Загрузить из Google Drive"
|
||||
},
|
||||
"share_point": {
|
||||
"label": "SharePoint",
|
||||
"heading": "Загрузить из SharePoint"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
@@ -289,6 +294,24 @@
|
||||
"remove": "Удалить",
|
||||
"folderAlt": "Папка",
|
||||
"fileAlt": "Файл"
|
||||
},
|
||||
"sharePoint": {
|
||||
"connect": "Подключиться к SharePoint",
|
||||
"sessionExpired": "Сеанс истек. Пожалуйста, переподключитесь к SharePoint.",
|
||||
"sessionExpiredGeneric": "Сеанс истек. Пожалуйста, переподключите свою учетную запись.",
|
||||
"validateFailed": "Не удалось проверить сеанс. Пожалуйста, переподключитесь.",
|
||||
"noSession": "Действительный сеанс не найден. Пожалуйста, переподключитесь к SharePoint.",
|
||||
"noAccessToken": "Токен доступа недоступен. Пожалуйста, переподключитесь к SharePoint.",
|
||||
"pickerFailed": "Не удалось открыть средство выбора файлов. Пожалуйста, попробуйте еще раз.",
|
||||
"selectedFiles": "Выбранные файлы",
|
||||
"selectFiles": "Выбрать файлы",
|
||||
"loading": "Загрузка...",
|
||||
"noFilesSelected": "Файлы или папки не выбраны",
|
||||
"folders": "Папки",
|
||||
"files": "Файлы",
|
||||
"remove": "Удалить",
|
||||
"folderAlt": "Папка",
|
||||
"fileAlt": "Файл"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -387,8 +410,7 @@
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "Прикрепить",
|
||||
"remove": "Удалить вложение",
|
||||
"uploadFailed": "Ошибка загрузки"
|
||||
"remove": "Удалить вложение"
|
||||
},
|
||||
"retry": "Повторить"
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"upload": "正在上傳",
|
||||
"training": "正在上傳",
|
||||
"completed": "上傳完成",
|
||||
"failed": "上傳失敗",
|
||||
"wait": "這可能需要幾分鐘",
|
||||
"preparing": "準備上傳",
|
||||
"tokenLimit": "超出令牌限制,請考慮上傳較小的文檔",
|
||||
@@ -260,6 +261,10 @@
|
||||
"google_drive": {
|
||||
"label": "Google Drive",
|
||||
"heading": "從Google Drive上傳"
|
||||
},
|
||||
"share_point": {
|
||||
"label": "SharePoint",
|
||||
"heading": "從SharePoint上傳"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
@@ -289,6 +294,24 @@
|
||||
"remove": "移除",
|
||||
"folderAlt": "資料夾",
|
||||
"fileAlt": "檔案"
|
||||
},
|
||||
"sharePoint": {
|
||||
"connect": "連接到 SharePoint",
|
||||
"sessionExpired": "工作階段已過期。請重新連接到 SharePoint。",
|
||||
"sessionExpiredGeneric": "工作階段已過期。請重新連接您的帳戶。",
|
||||
"validateFailed": "驗證工作階段失敗。請重新連接。",
|
||||
"noSession": "未找到有效工作階段。請重新連接到 SharePoint。",
|
||||
"noAccessToken": "存取權杖不可用。請重新連接到 SharePoint。",
|
||||
"pickerFailed": "無法開啟檔案選擇器。請重試。",
|
||||
"selectedFiles": "已選擇的檔案",
|
||||
"selectFiles": "選擇檔案",
|
||||
"loading": "載入中...",
|
||||
"noFilesSelected": "未選擇檔案或資料夾",
|
||||
"folders": "資料夾",
|
||||
"files": "檔案",
|
||||
"remove": "移除",
|
||||
"folderAlt": "資料夾",
|
||||
"fileAlt": "檔案"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -387,8 +410,7 @@
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "附件",
|
||||
"remove": "刪除附件",
|
||||
"uploadFailed": "上傳失敗"
|
||||
"remove": "刪除附件"
|
||||
},
|
||||
"retry": "重試"
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"upload": "正在上传",
|
||||
"training": "正在上传",
|
||||
"completed": "上传完成",
|
||||
"failed": "上传失败",
|
||||
"wait": "这可能需要几分钟",
|
||||
"preparing": "准备上传",
|
||||
"tokenLimit": "超出令牌限制,请考虑上传较小的文档",
|
||||
@@ -260,6 +261,10 @@
|
||||
"google_drive": {
|
||||
"label": "Google Drive",
|
||||
"heading": "从Google Drive上传"
|
||||
},
|
||||
"share_point": {
|
||||
"label": "SharePoint",
|
||||
"heading": "从SharePoint上传"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
@@ -289,6 +294,24 @@
|
||||
"remove": "删除",
|
||||
"folderAlt": "文件夹",
|
||||
"fileAlt": "文件"
|
||||
},
|
||||
"sharePoint": {
|
||||
"connect": "连接到 SharePoint",
|
||||
"sessionExpired": "会话已过期。请重新连接到 SharePoint。",
|
||||
"sessionExpiredGeneric": "会话已过期。请重新连接您的账户。",
|
||||
"validateFailed": "验证会话失败。请重新连接。",
|
||||
"noSession": "未找到有效会话。请重新连接到 SharePoint。",
|
||||
"noAccessToken": "访问令牌不可用。请重新连接到 SharePoint。",
|
||||
"pickerFailed": "无法打开文件选择器。请重试。",
|
||||
"selectedFiles": "已选择的文件",
|
||||
"selectFiles": "选择文件",
|
||||
"loading": "加载中...",
|
||||
"noFilesSelected": "未选择文件或文件夹",
|
||||
"folders": "文件夹",
|
||||
"files": "文件",
|
||||
"remove": "删除",
|
||||
"folderAlt": "文件夹",
|
||||
"fileAlt": "文件"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -387,8 +410,7 @@
|
||||
},
|
||||
"attachments": {
|
||||
"attach": "附件",
|
||||
"remove": "删除附件",
|
||||
"uploadFailed": "上传失败"
|
||||
"remove": "删除附件"
|
||||
},
|
||||
"retry": "重试"
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Input from '../components/Input';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import ConfirmationModal from './ConfirmationModal';
|
||||
import WrapperModal from './WrapperModal';
|
||||
|
||||
export default function ChunkModal({
|
||||
type,
|
||||
modalState,
|
||||
setModalState,
|
||||
handleSubmit,
|
||||
originalTitle,
|
||||
originalText,
|
||||
handleDelete,
|
||||
}: {
|
||||
type: 'ADD' | 'EDIT';
|
||||
modalState: ActiveState;
|
||||
setModalState: (state: ActiveState) => void;
|
||||
handleSubmit: (title: string, text: string) => void;
|
||||
originalTitle?: string;
|
||||
originalText?: string;
|
||||
handleDelete?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [title, setTitle] = React.useState('');
|
||||
const [chunkText, setChunkText] = React.useState('');
|
||||
const [deleteModal, setDeleteModal] = React.useState<ActiveState>('INACTIVE');
|
||||
|
||||
React.useEffect(() => {
|
||||
setTitle(originalTitle || '');
|
||||
setChunkText(originalText || '');
|
||||
}, [originalTitle, originalText]);
|
||||
|
||||
const resetForm = () => {
|
||||
setTitle('');
|
||||
setChunkText('');
|
||||
};
|
||||
|
||||
const handleDeleteConfirmed = () => {
|
||||
if (handleDelete) {
|
||||
handleDelete();
|
||||
}
|
||||
setDeleteModal('INACTIVE');
|
||||
setModalState('INACTIVE');
|
||||
};
|
||||
|
||||
if (modalState !== 'ACTIVE') return null;
|
||||
|
||||
const content = (
|
||||
<div>
|
||||
<h2 className="text-jet dark:text-bright-gray px-3 text-xl font-semibold">
|
||||
{t(`modals.chunk.${type === 'ADD' ? 'add' : 'edit'}`)}
|
||||
</h2>
|
||||
<div className="relative mt-6 px-3">
|
||||
<Input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
borderVariant="thin"
|
||||
placeholder={t('modals.chunk.title')}
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative mt-6 px-3">
|
||||
<div className="border-silver dark:border-silver/40 rounded-lg border pt-3 pb-1">
|
||||
<span className="text-gray-4000 dark:text-silver absolute -top-2 left-5 rounded-lg bg-white px-2 text-xs dark:bg-[#26272E]">
|
||||
{t('modals.chunk.bodyText')}
|
||||
</span>
|
||||
<textarea
|
||||
id="chunk-body-text"
|
||||
className="h-60 max-h-60 w-full resize-none px-3 outline-hidden dark:bg-transparent dark:text-white"
|
||||
value={chunkText}
|
||||
onChange={(e) => setChunkText(e.target.value)}
|
||||
aria-label={t('modals.chunk.promptText')}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{type === 'ADD' ? (
|
||||
<div className="mt-8 flex flex-row-reverse gap-1 px-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSubmit(title, chunkText);
|
||||
setModalState('INACTIVE');
|
||||
resetForm();
|
||||
}}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all"
|
||||
>
|
||||
{t('modals.chunk.add')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalState('INACTIVE');
|
||||
resetForm();
|
||||
}}
|
||||
className="dark:text-light-gray cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{t('modals.chunk.close')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 flex w-full items-center justify-between px-3">
|
||||
<button
|
||||
className="rounded-full border border-solid border-red-500 px-5 py-2 text-sm text-nowrap text-red-500 hover:bg-red-500 hover:text-white"
|
||||
onClick={() => {
|
||||
setDeleteModal('ACTIVE');
|
||||
}}
|
||||
>
|
||||
{t('modals.chunk.delete')}
|
||||
</button>
|
||||
<div className="flex flex-row-reverse gap-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSubmit(title, chunkText);
|
||||
setModalState('INACTIVE');
|
||||
}}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all"
|
||||
>
|
||||
{t('modals.chunk.update')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalState('INACTIVE');
|
||||
}}
|
||||
className="dark:text-light-gray cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:hover:bg-[#767183]/50"
|
||||
>
|
||||
{t('modals.chunk.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WrapperModal
|
||||
close={() => setModalState('INACTIVE')}
|
||||
className="sm:w-[620px]"
|
||||
isPerformingTask={true}
|
||||
>
|
||||
{content}
|
||||
</WrapperModal>
|
||||
|
||||
{type === 'EDIT' && (
|
||||
<ConfirmationModal
|
||||
message={t('modals.chunk.deleteConfirmation')}
|
||||
modalState={deleteModal}
|
||||
setModalState={setDeleteModal}
|
||||
handleSubmit={handleDeleteConfirmed}
|
||||
submitLabel={t('modals.chunk.delete')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -104,11 +104,11 @@ export const ShareConversationModal = ({
|
||||
|
||||
return (
|
||||
<WrapperModal close={close}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex max-h-[80vh] w-[600px] max-w-[80vw] flex-col gap-2 overflow-y-auto">
|
||||
<h2 className="text-eerie-black dark:text-chinese-white text-xl font-medium">
|
||||
{t('modals.shareConv.label')}
|
||||
</h2>
|
||||
<p className="text-eerie-black dark:text-silver/60 text-sm">
|
||||
<p className="text-eerie-black dark:text-silver/60 text-sm leading-relaxed">
|
||||
{t('modals.shareConv.note')}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -111,16 +111,6 @@ export function setLocalPrompt(prompt: string): void {
|
||||
export function setLocalRecentDocs(docs: Doc[] | null): void {
|
||||
if (docs && docs.length > 0) {
|
||||
localStorage.setItem('DocsGPTRecentDocs', JSON.stringify(docs));
|
||||
|
||||
docs.forEach((doc) => {
|
||||
let docPath = 'default';
|
||||
if (doc.type === 'local') {
|
||||
docPath = 'local' + '/' + doc.name + '/';
|
||||
}
|
||||
userService
|
||||
.checkDocs({ docs: docPath }, null)
|
||||
.then((response) => response.json());
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem('DocsGPTRecentDocs');
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface Preference {
|
||||
prompt: { name: string; id: string; type: string };
|
||||
chunks: string;
|
||||
token_limit: number;
|
||||
selectedDocs: Doc[] | null;
|
||||
selectedDocs: Doc[];
|
||||
sourceDocs: Doc[] | null;
|
||||
conversations: {
|
||||
data: { name: string; id: string }[] | null;
|
||||
@@ -137,8 +137,11 @@ prefListenerMiddleware.startListening({
|
||||
prefListenerMiddleware.startListening({
|
||||
matcher: isAnyOf(setSelectedDocs),
|
||||
effect: (action, listenerApi) => {
|
||||
const state = listenerApi.getState() as RootState;
|
||||
setLocalRecentDocs(
|
||||
(listenerApi.getState() as RootState).preference.selectedDocs ?? null,
|
||||
state.preference.selectedDocs.length > 0
|
||||
? state.preference.selectedDocs
|
||||
: null,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -179,7 +182,7 @@ export const selectApiKey = (state: RootState) => state.preference.apiKey;
|
||||
export const selectApiKeyStatus = (state: RootState) =>
|
||||
!!state.preference.apiKey;
|
||||
export const selectSelectedDocsStatus = (state: RootState) =>
|
||||
!!state.preference.selectedDocs;
|
||||
state.preference.selectedDocs.length > 0;
|
||||
export const selectSourceDocs = (state: RootState) =>
|
||||
state.preference.sourceDocs;
|
||||
export const selectModalStateDeleteConv = (state: RootState) =>
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
import { useLoaderState } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import CreateAPIKeyModal from '../modals/CreateAPIKeyModal';
|
||||
import SaveAPIKeyModal from '../modals/SaveAPIKeyModal';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { APIKeyData } from './types';
|
||||
|
||||
export default function APIKeys() {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const [isCreateModalOpen, setCreateModal] = useState(false);
|
||||
const [isSaveKeyModalOpen, setSaveKeyModal] = useState(false);
|
||||
const [newKey, setNewKey] = useState('');
|
||||
const [apiKeys, setApiKeys] = useState<APIKeyData[]>([]);
|
||||
const [loading, setLoading] = useLoaderState(true);
|
||||
const [keyToDelete, setKeyToDelete] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleFetchKeys = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await userService.getAPIKeys(token);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch API Keys');
|
||||
}
|
||||
const apiKeys = await response.json();
|
||||
setApiKeys(apiKeys);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteKey = (id: string) => {
|
||||
setLoading(true);
|
||||
userService
|
||||
.deleteAPIKey({ id }, token)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete API Key');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success === true) {
|
||||
setApiKeys((previous) => previous.filter((elem) => elem.id !== id));
|
||||
}
|
||||
setKeyToDelete(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateKey = (payload: {
|
||||
name: string;
|
||||
source?: string;
|
||||
retriever?: string;
|
||||
prompt_id: string;
|
||||
chunks: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
userService
|
||||
.createAPIKey(payload, token)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create API Key');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setApiKeys([...apiKeys, data]);
|
||||
setCreateModal(false);
|
||||
setNewKey(data.key);
|
||||
setSaveKeyModal(true);
|
||||
handleFetchKeys();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
handleFetchKeys();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mt-8 flex w-full max-w-full flex-col overflow-hidden">
|
||||
<div className="relative flex grow flex-col">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sonic-silver text-base font-medium">
|
||||
{t('settings.apiKeys.description')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex flex-col items-start justify-end gap-3 sm:flex-row sm:items-center">
|
||||
<button
|
||||
onClick={() => setCreateModal(true)}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue flex h-[30px] w-[108px] items-center justify-center rounded-full text-sm text-white"
|
||||
title={t('settings.apiKeys.createNew')}
|
||||
>
|
||||
{t('settings.apiKeys.createNew')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full">
|
||||
<div className="dark:border-silver/40 overflow-hidden rounded-md border border-gray-300">
|
||||
<div className="table-scroll overflow-x-auto">
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
<tr className="dark:border-silver/40 border-b border-gray-300">
|
||||
<th className="text-sonic-silver w-[35%] px-4 py-3 text-left text-xs font-medium">
|
||||
{t('settings.apiKeys.name')}
|
||||
</th>
|
||||
<th className="text-sonic-silver w-[35%] px-4 py-3 text-left text-xs font-medium">
|
||||
{t('settings.apiKeys.sourceDoc')}
|
||||
</th>
|
||||
<th className="text-sonic-silver w-[25%] px-4 py-3 text-left text-xs font-medium">
|
||||
<span className="hidden sm:inline">
|
||||
{t('settings.apiKeys.key')}
|
||||
</span>
|
||||
<span className="sm:hidden">
|
||||
{t('settings.apiKeys.key')}
|
||||
</span>
|
||||
</th>
|
||||
<th className="w-[5%] px-4 py-3 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0]">
|
||||
<span className="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="dark:divide-silver/40 divide-y divide-gray-300">
|
||||
{loading ? (
|
||||
<SkeletonLoader component="table" />
|
||||
) : !apiKeys?.length ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className="bg-transparent py-4 text-center text-gray-700 dark:text-neutral-200"
|
||||
>
|
||||
{t('settings.apiKeys.noData')}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
Array.isArray(apiKeys) &&
|
||||
apiKeys.map((element) => (
|
||||
<tr
|
||||
key={element.id}
|
||||
className="group transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
>
|
||||
<td className="w-[35%] max-w-0 min-w-48 px-4 py-4 text-sm font-semibold text-gray-700 dark:text-[#E0E0E0]">
|
||||
<div className="truncate" title={element.name}>
|
||||
{element.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-[35%] max-w-0 min-w-48 px-4 py-4 text-sm text-gray-700 dark:text-[#E0E0E0]">
|
||||
<div className="truncate" title={element.source}>
|
||||
{element.source}
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-[25%] px-4 py-4 font-mono text-sm text-gray-700 dark:text-[#E0E0E0]">
|
||||
<div className="truncate" title={element.key}>
|
||||
{element.key}
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-[5%] px-4 py-4 text-right">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() =>
|
||||
setKeyToDelete({
|
||||
id: element.id,
|
||||
name: element.name,
|
||||
})
|
||||
}
|
||||
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt={t('convTile.delete')}
|
||||
className="h-4 w-4 opacity-60 hover:opacity-100"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isCreateModalOpen && (
|
||||
<CreateAPIKeyModal
|
||||
createAPIKey={handleCreateKey}
|
||||
close={() => setCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
{isSaveKeyModalOpen && (
|
||||
<SaveAPIKeyModal apiKey={newKey} close={() => setSaveKeyModal(false)} />
|
||||
)}
|
||||
{keyToDelete && (
|
||||
<ConfirmationModal
|
||||
message={t('settings.apiKeys.deleteConfirmation', {
|
||||
name: keyToDelete.name,
|
||||
})}
|
||||
modalState="ACTIVE"
|
||||
setModalState={() => setKeyToDelete(null)}
|
||||
submitLabel={t('modals.deleteConv.delete')}
|
||||
handleSubmit={() => handleDeleteKey(keyToDelete.id)}
|
||||
handleCancel={() => setKeyToDelete(null)}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import React from 'react';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
|
||||
const Widgets: React.FC<{
|
||||
widgetScreenshot: File | null;
|
||||
onWidgetScreenshotChange: (screenshot: File | null) => void;
|
||||
}> = ({ widgetScreenshot, onWidgetScreenshotChange }) => {
|
||||
const widgetSources = ['Source 1', 'Source 2', 'Source 3'];
|
||||
const widgetMethods = ['Method 1', 'Method 2', 'Method 3'];
|
||||
const widgetTypes = ['Type 1', 'Type 2', 'Type 3'];
|
||||
|
||||
const [selectedWidgetSource, setSelectedWidgetSource] = React.useState(
|
||||
widgetSources[0],
|
||||
);
|
||||
const [selectedWidgetMethod, setSelectedWidgetMethod] = React.useState(
|
||||
widgetMethods[0],
|
||||
);
|
||||
const [selectedWidgetType, setSelectedWidgetType] = React.useState(
|
||||
widgetTypes[0],
|
||||
);
|
||||
|
||||
// const [widgetScreenshot, setWidgetScreenshot] = useState<File | null>(null);
|
||||
const [widgetCode, setWidgetCode] = React.useState<string>(''); // Your widget code state
|
||||
|
||||
const handleScreenshotChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const files = event.target.files;
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const selectedScreenshot = files[0];
|
||||
onWidgetScreenshotChange(selectedScreenshot); // Update the screenshot in the parent component
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
// Create a new textarea element to select the text
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = widgetCode;
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
// Select and copy the text
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Clean up the textarea element
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-[59px]">
|
||||
<p className="text-jet font-bold">Widget Source</p>
|
||||
<Dropdown
|
||||
options={widgetSources}
|
||||
selectedValue={selectedWidgetSource}
|
||||
onSelect={setSelectedWidgetSource}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<p className="text-jet font-bold">Widget Method</p>
|
||||
<Dropdown
|
||||
options={widgetMethods}
|
||||
selectedValue={selectedWidgetMethod}
|
||||
onSelect={setSelectedWidgetMethod}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<p className="text-jet font-bold">Widget Type</p>
|
||||
<Dropdown
|
||||
options={widgetTypes}
|
||||
selectedValue={selectedWidgetType}
|
||||
onSelect={setSelectedWidgetType}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<p className="text-jet font-bold">Widget Code Snippet</p>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={widgetCode}
|
||||
onChange={(e) => setWidgetCode(e.target.value)}
|
||||
className="mt-3 w-full rounded-lg border-2 p-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<button
|
||||
onClick={handleCopyToClipboard}
|
||||
className="rounded-lg bg-blue-400 px-2 py-2 font-bold text-white transition-all hover:bg-blue-600"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-lg font-semibold">Widget Screenshot</p>
|
||||
<input type="file" accept="image/*" onChange={handleScreenshotChange} />
|
||||
</div>
|
||||
|
||||
{widgetScreenshot && (
|
||||
<div className="mt-4">
|
||||
<img
|
||||
src={URL.createObjectURL(widgetScreenshot)}
|
||||
alt="Widget Screenshot"
|
||||
className="max-w-full rounded-lg border border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Widgets;
|
||||
@@ -25,16 +25,12 @@ import Sources from './Sources';
|
||||
import General from './General';
|
||||
import Logs from './Logs';
|
||||
import Tools from './Tools';
|
||||
import Widgets from './Widgets';
|
||||
|
||||
export default function Settings() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [widgetScreenshot, setWidgetScreenshot] = React.useState<File | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getActiveTabFromPath = () => {
|
||||
const path = location.pathname;
|
||||
@@ -43,7 +39,6 @@ export default function Settings() {
|
||||
return t('settings.analytics.label');
|
||||
if (path.includes('/settings/logs')) return t('settings.logs.label');
|
||||
if (path.includes('/settings/tools')) return t('settings.tools.label');
|
||||
if (path.includes('/settings/widgets')) return 'Widgets';
|
||||
return t('settings.general.label');
|
||||
};
|
||||
|
||||
@@ -57,7 +52,6 @@ export default function Settings() {
|
||||
navigate('/settings/analytics');
|
||||
else if (tab === t('settings.logs.label')) navigate('/settings/logs');
|
||||
else if (tab === t('settings.tools.label')) navigate('/settings/tools');
|
||||
else if (tab === 'Widgets') navigate('/settings/widgets');
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -72,9 +66,6 @@ export default function Settings() {
|
||||
const token = useSelector(selectToken);
|
||||
const documents = useSelector(selectSourceDocs);
|
||||
const paginatedDocuments = useSelector(selectPaginatedDocuments);
|
||||
const updateWidgetScreenshot = (screenshot: File | null) => {
|
||||
setWidgetScreenshot(screenshot);
|
||||
};
|
||||
|
||||
const updateDocumentsList = (documents: Doc[], index: number) => [
|
||||
...documents.slice(0, index),
|
||||
@@ -122,15 +113,6 @@ export default function Settings() {
|
||||
<Route path="analytics" element={<Analytics />} />
|
||||
<Route path="logs" element={<Logs />} />
|
||||
<Route path="tools" element={<Tools />} />
|
||||
<Route
|
||||
path="widgets"
|
||||
element={
|
||||
<Widgets
|
||||
widgetScreenshot={widgetScreenshot}
|
||||
onWidgetScreenshotChange={updateWidgetScreenshot}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/settings" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const preloadedState: { preference: Preference } = {
|
||||
: { name: 'default', id: 'default', type: 'private' },
|
||||
chunks: JSON.parse(chunks ?? '2').toString(),
|
||||
token_limit: token_limit ? parseInt(token_limit) : 2000,
|
||||
selectedDocs: doc !== null ? JSON.parse(doc) : null,
|
||||
selectedDocs: doc !== null ? JSON.parse(doc) : [],
|
||||
conversations: {
|
||||
data: null,
|
||||
loading: false,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ActiveState, Doc } from '../models/misc';
|
||||
|
||||
import { getDocs } from '../preferences/preferenceApi';
|
||||
import {
|
||||
selectSelectedDocs,
|
||||
selectSourceDocs,
|
||||
selectToken,
|
||||
setSelectedDocs,
|
||||
@@ -31,6 +32,7 @@ import { FormField, IngestorConfig, IngestorType } from './types/ingestor';
|
||||
|
||||
import { FilePicker } from '../components/FilePicker';
|
||||
import GoogleDrivePicker from '../components/GoogleDrivePicker';
|
||||
import SharePointPicker from '../components/SharePointPicker';
|
||||
|
||||
import ChevronRight from '../assets/chevron-right.svg';
|
||||
|
||||
@@ -50,6 +52,7 @@ function Upload({
|
||||
onSuccessfulUpload?: () => void;
|
||||
}) {
|
||||
const token = useSelector(selectToken);
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
|
||||
const [files, setfiles] = useState<File[]>(receivedFile);
|
||||
const [activeTab, setActiveTab] = useState<boolean>(true);
|
||||
@@ -250,6 +253,8 @@ function Upload({
|
||||
token={token}
|
||||
/>
|
||||
);
|
||||
case 'share_point_picker':
|
||||
return <SharePointPicker key={field.name} token={token} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -291,12 +296,12 @@ function Upload({
|
||||
id: clientTaskId,
|
||||
updates: {
|
||||
status: 'failed',
|
||||
errorMessage: errorMessage || t('attachments.uploadFailed'),
|
||||
errorMessage: errorMessage,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, t],
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const trackTraining = useCallback(
|
||||
@@ -308,6 +313,15 @@ function Upload({
|
||||
.getTaskStatus(backendTaskId, null)
|
||||
.then((response) => response.json())
|
||||
.then(async (data) => {
|
||||
if (!data.success && data.message) {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
handleTaskFailure(clientTaskId, data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'SUCCESS') {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -327,7 +341,13 @@ function Upload({
|
||||
(doc: Doc) => doc.id && !existingDocIds.has(doc.id),
|
||||
);
|
||||
if (newDoc) {
|
||||
dispatch(setSelectedDocs([newDoc]));
|
||||
// If only one doc is selected, replace it completely
|
||||
// If multiple docs are selected, append the new doc
|
||||
if (selectedDocs.length === 1) {
|
||||
dispatch(setSelectedDocs([newDoc]));
|
||||
} else {
|
||||
dispatch(setSelectedDocs([...selectedDocs, newDoc]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,12 +396,12 @@ function Upload({
|
||||
timeoutId = window.setTimeout(poll, 5000);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
handleTaskFailure(clientTaskId);
|
||||
handleTaskFailure(clientTaskId, error?.message);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import UrlIcon from '../../assets/url.svg';
|
||||
import GithubIcon from '../../assets/github.svg';
|
||||
import RedditIcon from '../../assets/reddit.svg';
|
||||
import DriveIcon from '../../assets/drive.svg';
|
||||
import SharePoint from '../../assets/sharepoint.svg';
|
||||
|
||||
export type IngestorType =
|
||||
| 'crawler'
|
||||
@@ -11,7 +12,8 @@ export type IngestorType =
|
||||
| 'reddit'
|
||||
| 'url'
|
||||
| 'google_drive'
|
||||
| 'local_file';
|
||||
| 'local_file'
|
||||
| 'share_point';
|
||||
|
||||
export interface IngestorConfig {
|
||||
type: IngestorType | null;
|
||||
@@ -33,7 +35,8 @@ export type FieldType =
|
||||
| 'boolean'
|
||||
| 'local_file_picker'
|
||||
| 'remote_file_picker'
|
||||
| 'google_drive_picker';
|
||||
| 'google_drive_picker'
|
||||
| 'share_point_picker';
|
||||
|
||||
export interface FormField {
|
||||
name: string;
|
||||
@@ -147,6 +150,24 @@ export const IngestorFormSchemas: IngestorSchema[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'share_point',
|
||||
label: 'Share Point',
|
||||
icon: SharePoint,
|
||||
heading: 'Upload from Share Point',
|
||||
validate: () => {
|
||||
const sharePointClientId = import.meta.env.VITE_SHARE_POINT_CLIENT_ID;
|
||||
return !!sharePointClientId;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'files',
|
||||
label: 'Select Files from Share Point',
|
||||
type: 'share_point_picker',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const IngestorDefaultConfigs: Record<
|
||||
@@ -175,6 +196,14 @@ export const IngestorDefaultConfigs: Record<
|
||||
},
|
||||
},
|
||||
local_file: { name: '', config: { files: [] } },
|
||||
share_point: {
|
||||
name: '',
|
||||
config: {
|
||||
file_ids: '',
|
||||
folder_ids: '',
|
||||
recursive: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface IngestorOption {
|
||||
|
||||
@@ -14,3 +14,21 @@ export const setSessionToken = (provider: string, token: string): void => {
|
||||
export const removeSessionToken = (provider: string): void => {
|
||||
localStorage.removeItem(`${provider}_session_token`);
|
||||
};
|
||||
|
||||
export const validateProviderSession = async (
|
||||
token: string | null,
|
||||
provider: string,
|
||||
) => {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
return await fetch(`${apiHost}/api/connectors/validate-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: provider,
|
||||
session_token: getSessionToken(provider),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
20
pytest.ini
Normal file
@@ -0,0 +1,20 @@
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--cov=application
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--cov-report=xml
|
||||
markers =
|
||||
unit: Unit tests
|
||||
integration: Integration tests
|
||||
slow: Slow running tests
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
@@ -292,7 +292,7 @@ function Use-DocsPublicAPIEndpoint {
|
||||
Write-ColorText "Setting up DocsGPT Public API Endpoint..." -ForegroundColor "White"
|
||||
|
||||
# Create .env file
|
||||
"LLM_NAME=docsgpt" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_PROVIDER=docsgpt" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
|
||||
Write-ColorText ".env file configured for DocsGPT Public API." -ForegroundColor "Green"
|
||||
@@ -397,7 +397,7 @@ function Serve-LocalOllama {
|
||||
|
||||
# Create .env file
|
||||
"API_KEY=xxxx" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"LLM_PROVIDER=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"OPENAI_BASE_URL=http://host.docker.internal:11434/v1" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
@@ -560,7 +560,7 @@ function Connect-LocalInferenceEngine {
|
||||
|
||||
# Create .env file
|
||||
"API_KEY=None" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"LLM_PROVIDER=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"OPENAI_BASE_URL=$openai_base_url" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
@@ -693,7 +693,7 @@ function Connect-CloudAPIProvider {
|
||||
|
||||
# Create .env file
|
||||
"API_KEY=$api_key" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_NAME=$llm_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"LLM_PROVIDER=$llm_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
|
||||
|
||||
56
tests/agents/test_agent_creator.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import pytest
|
||||
from application.agents.agent_creator import AgentCreator
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from application.agents.react_agent import ReActAgent
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAgentCreator:
|
||||
|
||||
def test_create_classic_agent(self, agent_base_params):
|
||||
agent = AgentCreator.create_agent("classic", **agent_base_params)
|
||||
assert isinstance(agent, ClassicAgent)
|
||||
assert agent.endpoint == agent_base_params["endpoint"]
|
||||
assert agent.llm_name == agent_base_params["llm_name"]
|
||||
assert agent.gpt_model == agent_base_params["gpt_model"]
|
||||
|
||||
def test_create_react_agent(self, agent_base_params):
|
||||
agent = AgentCreator.create_agent("react", **agent_base_params)
|
||||
assert isinstance(agent, ReActAgent)
|
||||
assert agent.endpoint == agent_base_params["endpoint"]
|
||||
assert agent.llm_name == agent_base_params["llm_name"]
|
||||
|
||||
def test_create_agent_case_insensitive(self, agent_base_params):
|
||||
agent_upper = AgentCreator.create_agent("CLASSIC", **agent_base_params)
|
||||
agent_mixed = AgentCreator.create_agent("ClAsSiC", **agent_base_params)
|
||||
|
||||
assert isinstance(agent_upper, ClassicAgent)
|
||||
assert isinstance(agent_mixed, ClassicAgent)
|
||||
|
||||
def test_create_agent_invalid_type(self, agent_base_params):
|
||||
with pytest.raises(ValueError, match="No agent class found for type"):
|
||||
AgentCreator.create_agent("invalid_agent_type", **agent_base_params)
|
||||
|
||||
def test_agent_registry_contains_expected_agents(self):
|
||||
assert "classic" in AgentCreator.agents
|
||||
assert "react" in AgentCreator.agents
|
||||
assert AgentCreator.agents["classic"] == ClassicAgent
|
||||
assert AgentCreator.agents["react"] == ReActAgent
|
||||
|
||||
def test_create_agent_with_optional_params(self, agent_base_params):
|
||||
agent_base_params["user_api_key"] = "user_key_123"
|
||||
agent_base_params["chat_history"] = [{"prompt": "test", "response": "test"}]
|
||||
agent_base_params["json_schema"] = {"type": "object"}
|
||||
|
||||
agent = AgentCreator.create_agent("classic", **agent_base_params)
|
||||
|
||||
assert agent.user_api_key == "user_key_123"
|
||||
assert len(agent.chat_history) == 1
|
||||
assert agent.json_schema == {"type": "object"}
|
||||
|
||||
def test_create_agent_with_attachments(self, agent_base_params):
|
||||
attachments = [{"name": "file.txt", "content": "test"}]
|
||||
agent_base_params["attachments"] = attachments
|
||||
|
||||
agent = AgentCreator.create_agent("classic", **agent_base_params)
|
||||
assert agent.attachments == attachments
|
||||
641
tests/agents/test_base_agent.py
Normal file
@@ -0,0 +1,641 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
from application.core.settings import settings
|
||||
from tests.conftest import FakeMongoCollection
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentInitialization:
|
||||
|
||||
def test_agent_initialization(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
assert agent.endpoint == agent_base_params["endpoint"]
|
||||
assert agent.llm_name == agent_base_params["llm_name"]
|
||||
assert agent.gpt_model == agent_base_params["gpt_model"]
|
||||
assert agent.api_key == agent_base_params["api_key"]
|
||||
assert agent.prompt == agent_base_params["prompt"]
|
||||
assert agent.user == agent_base_params["decoded_token"]["sub"]
|
||||
assert agent.tools == []
|
||||
assert agent.tool_calls == []
|
||||
|
||||
def test_agent_initialization_with_none_chat_history(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent_base_params["chat_history"] = None
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
assert agent.chat_history == []
|
||||
|
||||
def test_agent_initialization_with_chat_history(
|
||||
self,
|
||||
agent_base_params,
|
||||
sample_chat_history,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
agent_base_params["chat_history"] = sample_chat_history
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
assert len(agent.chat_history) == 2
|
||||
assert agent.chat_history[0]["prompt"] == "What is Python?"
|
||||
|
||||
def test_agent_decoded_token_defaults_to_empty_dict(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent_base_params["decoded_token"] = None
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
assert agent.decoded_token == {}
|
||||
assert agent.user is None
|
||||
|
||||
def test_agent_user_extracted_from_token(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent_base_params["decoded_token"] = {"sub": "user123"}
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
assert agent.user == "user123"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentBuildMessages:
|
||||
|
||||
def test_build_messages_basic(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
system_prompt = "System: {summaries}"
|
||||
query = "What is Python?"
|
||||
retrieved_data = [
|
||||
{"text": "Python is a programming language", "filename": "python.txt"}
|
||||
]
|
||||
|
||||
messages = agent._build_messages(system_prompt, query, retrieved_data)
|
||||
|
||||
assert len(messages) >= 2
|
||||
assert messages[0]["role"] == "system"
|
||||
assert "Python is a programming language" in messages[0]["content"]
|
||||
assert messages[-1]["role"] == "user"
|
||||
assert messages[-1]["content"] == query
|
||||
|
||||
def test_build_messages_with_chat_history(
|
||||
self,
|
||||
agent_base_params,
|
||||
sample_chat_history,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
agent_base_params["chat_history"] = sample_chat_history
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
system_prompt = "System: {summaries}"
|
||||
query = "New question?"
|
||||
retrieved_data = [{"text": "Data", "filename": "file.txt"}]
|
||||
|
||||
messages = agent._build_messages(system_prompt, query, retrieved_data)
|
||||
|
||||
user_messages = [m for m in messages if m["role"] == "user"]
|
||||
assistant_messages = [m for m in messages if m["role"] == "assistant"]
|
||||
|
||||
assert len(user_messages) >= 3
|
||||
assert len(assistant_messages) >= 2
|
||||
|
||||
def test_build_messages_with_tool_calls_in_history(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
tool_call_history = [
|
||||
{
|
||||
"tool_calls": [
|
||||
{
|
||||
"call_id": "123",
|
||||
"action_name": "test_action",
|
||||
"arguments": {"arg": "value"},
|
||||
"result": "success",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
agent_base_params["chat_history"] = tool_call_history
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
messages = agent._build_messages(
|
||||
"System: {summaries}", "query", [{"text": "data", "filename": "file.txt"}]
|
||||
)
|
||||
|
||||
tool_messages = [m for m in messages if m["role"] == "tool"]
|
||||
assert len(tool_messages) > 0
|
||||
|
||||
def test_build_messages_handles_missing_filename(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
retrieved_data = [{"text": "Document without filename or title"}]
|
||||
|
||||
messages = agent._build_messages("System: {summaries}", "query", retrieved_data)
|
||||
|
||||
assert messages[0]["role"] == "system"
|
||||
assert "Document without filename" in messages[0]["content"]
|
||||
|
||||
def test_build_messages_uses_title_as_fallback(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
retrieved_data = [{"text": "Data", "title": "Title Doc"}]
|
||||
|
||||
messages = agent._build_messages("System: {summaries}", "query", retrieved_data)
|
||||
|
||||
assert "Title Doc" in messages[0]["content"]
|
||||
|
||||
def test_build_messages_uses_source_as_fallback(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
retrieved_data = [{"text": "Data", "source": "source.txt"}]
|
||||
|
||||
messages = agent._build_messages("System: {summaries}", "query", retrieved_data)
|
||||
|
||||
assert "source.txt" in messages[0]["content"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentTools:
|
||||
|
||||
def test_get_user_tools(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_mongo_db,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"].docs = {
|
||||
"1": {"_id": "1", "user": "test_user", "name": "tool1", "status": True},
|
||||
"2": {"_id": "2", "user": "test_user", "name": "tool2", "status": True},
|
||||
}
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
tools = agent._get_user_tools("test_user")
|
||||
|
||||
assert len(tools) == 2
|
||||
assert "0" in tools
|
||||
assert "1" in tools
|
||||
|
||||
def test_get_user_tools_filters_by_status(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_mongo_db,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"].docs = {
|
||||
"1": {"_id": "1", "user": "test_user", "name": "tool1", "status": True},
|
||||
"2": {"_id": "2", "user": "test_user", "name": "tool2", "status": False},
|
||||
}
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
tools = agent._get_user_tools("test_user")
|
||||
|
||||
assert len(tools) == 1
|
||||
|
||||
def test_get_tools_by_api_key(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_mongo_db,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
):
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
tool_id = str(ObjectId())
|
||||
tool_obj_id = ObjectId(tool_id)
|
||||
|
||||
fake_agent_collection = FakeMongoCollection()
|
||||
fake_agent_collection.docs["api_key_123"] = {
|
||||
"key": "api_key_123",
|
||||
"tools": [tool_id],
|
||||
}
|
||||
|
||||
fake_tools_collection = FakeMongoCollection()
|
||||
fake_tools_collection.docs[tool_id] = {"_id": tool_obj_id, "name": "api_tool"}
|
||||
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["agents"] = fake_agent_collection
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"] = fake_tools_collection
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
tools = agent._get_tools("api_key_123")
|
||||
|
||||
assert tool_id in tools
|
||||
|
||||
def test_build_tool_parameters(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
action = {
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"param1": {
|
||||
"type": "string",
|
||||
"description": "Test param",
|
||||
"filled_by_llm": True,
|
||||
},
|
||||
"param2": {"type": "number", "filled_by_llm": False, "value": 42},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params = agent._build_tool_parameters(action)
|
||||
|
||||
assert "param1" in params["properties"]
|
||||
assert "param1" in params["required"]
|
||||
assert "filled_by_llm" not in params["properties"]["param1"]
|
||||
|
||||
def test_prepare_tools_with_api_tool(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
tools_dict = {
|
||||
"1": {
|
||||
"name": "api_tool",
|
||||
"config": {
|
||||
"actions": {
|
||||
"get_data": {
|
||||
"name": "get_data",
|
||||
"description": "Get data from API",
|
||||
"active": True,
|
||||
"url": "https://api.example.com/data",
|
||||
"method": "GET",
|
||||
"parameters": {"properties": {}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
agent._prepare_tools(tools_dict)
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
assert agent.tools[0]["type"] == "function"
|
||||
assert agent.tools[0]["function"]["name"] == "get_data_1"
|
||||
|
||||
def test_prepare_tools_with_regular_tool(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
tools_dict = {
|
||||
"1": {
|
||||
"name": "custom_tool",
|
||||
"actions": [
|
||||
{
|
||||
"name": "action1",
|
||||
"description": "Custom action",
|
||||
"active": True,
|
||||
"parameters": {"properties": {}},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
agent._prepare_tools(tools_dict)
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
assert agent.tools[0]["function"]["name"] == "action1_1"
|
||||
|
||||
def test_prepare_tools_filters_inactive_actions(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
tools_dict = {
|
||||
"1": {
|
||||
"name": "custom_tool",
|
||||
"actions": [
|
||||
{
|
||||
"name": "active_action",
|
||||
"description": "Active",
|
||||
"active": True,
|
||||
"parameters": {"properties": {}},
|
||||
},
|
||||
{
|
||||
"name": "inactive_action",
|
||||
"description": "Inactive",
|
||||
"active": False,
|
||||
"parameters": {"properties": {}},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
agent._prepare_tools(tools_dict)
|
||||
|
||||
assert len(agent.tools) == 1
|
||||
assert agent.tools[0]["function"]["name"] == "active_action_1"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentToolExecution:
|
||||
|
||||
def test_execute_tool_action_success(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_tool_manager,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
call = Mock()
|
||||
call.id = "call_123"
|
||||
call.name = "test_action_1"
|
||||
call.arguments = '{"param1": "value1"}'
|
||||
|
||||
tools_dict = {
|
||||
"1": {
|
||||
"name": "custom_tool",
|
||||
"config": {},
|
||||
"actions": [
|
||||
{
|
||||
"name": "test_action",
|
||||
"description": "Test",
|
||||
"parameters": {"properties": {}},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
results = list(agent._execute_tool_action(tools_dict, call))
|
||||
|
||||
assert len(results) >= 2
|
||||
assert results[0]["type"] == "tool_call"
|
||||
assert results[0]["data"]["status"] == "pending"
|
||||
assert results[-1]["data"]["status"] == "completed"
|
||||
|
||||
def test_execute_tool_action_invalid_tool_name(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
call = Mock()
|
||||
call.id = "call_123"
|
||||
call.name = "invalid_format"
|
||||
call.arguments = "{}"
|
||||
|
||||
tools_dict = {}
|
||||
|
||||
results = list(agent._execute_tool_action(tools_dict, call))
|
||||
|
||||
assert results[0]["type"] == "tool_call"
|
||||
assert results[0]["data"]["status"] == "error"
|
||||
assert (
|
||||
"Failed to parse" in results[0]["data"]["result"]
|
||||
or "not found" in results[0]["data"]["result"]
|
||||
)
|
||||
|
||||
def test_execute_tool_action_tool_not_found(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
call = Mock()
|
||||
call.id = "call_123"
|
||||
call.name = "action_999"
|
||||
call.arguments = "{}"
|
||||
|
||||
tools_dict = {"1": {"name": "tool1", "config": {}, "actions": []}}
|
||||
|
||||
results = list(agent._execute_tool_action(tools_dict, call))
|
||||
|
||||
assert results[0]["type"] == "tool_call"
|
||||
assert results[0]["data"]["status"] == "error"
|
||||
assert "not found" in results[0]["data"]["result"]
|
||||
|
||||
def test_execute_tool_action_with_parameters(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_tool_manager,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
call = Mock()
|
||||
call.id = "call_123"
|
||||
call.name = "test_action_1"
|
||||
call.arguments = '{"param1": "value1", "param2": "value2"}'
|
||||
|
||||
tools_dict = {
|
||||
"1": {
|
||||
"name": "custom_tool",
|
||||
"config": {},
|
||||
"actions": [
|
||||
{
|
||||
"name": "test_action",
|
||||
"description": "Test",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"param1": {"type": "string"},
|
||||
"param2": {"type": "string"},
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
results = list(agent._execute_tool_action(tools_dict, call))
|
||||
|
||||
assert results[-1]["data"]["status"] == "completed"
|
||||
assert results[-1]["data"]["arguments"]["param1"] == "value1"
|
||||
|
||||
def test_get_truncated_tool_calls(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
agent.tool_calls = [
|
||||
{
|
||||
"tool_name": "test_tool",
|
||||
"call_id": "123",
|
||||
"action_name": "action",
|
||||
"arguments": {},
|
||||
"result": "a" * 100,
|
||||
}
|
||||
]
|
||||
|
||||
truncated = agent._get_truncated_tool_calls()
|
||||
|
||||
assert len(truncated) == 1
|
||||
assert len(truncated[0]["result"]) <= 53
|
||||
assert truncated[0]["result"].endswith("...")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentRetrieverSearch:
|
||||
|
||||
def test_retriever_search(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
results = agent._retriever_search(mock_retriever, "test query", log_context)
|
||||
|
||||
assert len(results) == 2
|
||||
mock_retriever.search.assert_called_once_with("test query")
|
||||
|
||||
def test_retriever_search_logs_context(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
agent._retriever_search(mock_retriever, "test query", log_context)
|
||||
|
||||
assert len(log_context.stacks) == 1
|
||||
assert log_context.stacks[0]["component"] == "retriever"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentLLMGeneration:
|
||||
|
||||
def test_llm_gen_basic(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
messages = [{"role": "user", "content": "test"}]
|
||||
agent._llm_gen(messages, log_context)
|
||||
|
||||
mock_llm.gen_stream.assert_called_once()
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
assert call_args["model"] == agent.gpt_model
|
||||
assert call_args["messages"] == messages
|
||||
|
||||
def test_llm_gen_with_tools(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
agent.tools = [{"type": "function", "function": {"name": "test"}}]
|
||||
|
||||
messages = [{"role": "user", "content": "test"}]
|
||||
agent._llm_gen(messages, log_context)
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
assert "tools" in call_args
|
||||
assert call_args["tools"] == agent.tools
|
||||
|
||||
def test_llm_gen_with_json_schema(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm._supports_structured_output = Mock(return_value=True)
|
||||
mock_llm.prepare_structured_output_format = Mock(
|
||||
return_value={"schema": "test"}
|
||||
)
|
||||
|
||||
agent_base_params["json_schema"] = {"type": "object"}
|
||||
agent_base_params["llm_name"] = "openai"
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
messages = [{"role": "user", "content": "test"}]
|
||||
agent._llm_gen(messages, log_context)
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
assert "response_format" in call_args
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentHandleResponse:
|
||||
|
||||
def test_handle_response_string(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator, log_context
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
response = "Simple string response"
|
||||
results = list(agent._handle_response(response, {}, [], log_context))
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["answer"] == "Simple string response"
|
||||
|
||||
def test_handle_response_with_message(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator, log_context
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
response = Mock()
|
||||
response.message = Mock()
|
||||
response.message.content = "Message content"
|
||||
|
||||
results = list(agent._handle_response(response, {}, [], log_context))
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["answer"] == "Message content"
|
||||
|
||||
def test_handle_response_with_structured_output(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm._supports_structured_output = Mock(return_value=True)
|
||||
agent_base_params["json_schema"] = {"type": "object"}
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
response = "Structured response"
|
||||
results = list(agent._handle_response(response, {}, [], log_context))
|
||||
|
||||
assert results[0]["structured"] is True
|
||||
assert results[0]["schema"] == {"type": "object"}
|
||||
|
||||
def test_handle_response_with_handler(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
def mock_process(*args):
|
||||
yield {"type": "tool_call", "data": {}}
|
||||
yield "Final answer"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_process)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
response = Mock()
|
||||
response.message = None
|
||||
|
||||
results = list(agent._handle_response(response, {}, [], log_context))
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0]["type"] == "tool_call"
|
||||
assert results[1]["answer"] == "Final answer"
|
||||
242
tests/agents/test_classic_agent.py
Normal file
@@ -0,0 +1,242 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.classic_agent import ClassicAgent
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestClassicAgent:
|
||||
|
||||
def test_classic_agent_initialization(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
assert isinstance(agent, ClassicAgent)
|
||||
assert agent.endpoint == agent_base_params["endpoint"]
|
||||
assert agent.llm_name == agent_base_params["llm_name"]
|
||||
|
||||
def test_gen_inner_basic_flow(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
yield "Answer chunk 1"
|
||||
yield "Answer chunk 2"
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=mock_gen_stream())
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed answer"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
assert len(results) >= 2
|
||||
sources = [r for r in results if "sources" in r]
|
||||
tool_calls = [r for r in results if "tool_calls" in r]
|
||||
|
||||
assert len(sources) == 1
|
||||
assert len(tool_calls) == 1
|
||||
|
||||
def test_gen_inner_retrieves_documents(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
mock_retriever.search.assert_called_once_with("Test query")
|
||||
|
||||
def test_gen_inner_uses_user_api_key_tools(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
from application.core.settings import settings
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
tool_id = str(ObjectId())
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["agents"].docs = {
|
||||
"api_key_123": {"key": "api_key_123", "tools": [tool_id]}
|
||||
}
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"].docs = {
|
||||
tool_id: {"_id": ObjectId(tool_id), "name": "test_tool"}
|
||||
}
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent_base_params["user_api_key"] = "api_key_123"
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
assert len(agent.tools) >= 0
|
||||
|
||||
def test_gen_inner_uses_user_tools(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
from application.core.settings import settings
|
||||
|
||||
mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"].docs = {
|
||||
"1": {"_id": "1", "user": "test_user", "name": "tool1", "status": True}
|
||||
}
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
assert len(agent.tools) >= 0
|
||||
|
||||
def test_gen_inner_builds_correct_messages(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
call_kwargs = mock_llm.gen_stream.call_args[1]
|
||||
messages = call_kwargs["messages"]
|
||||
|
||||
assert len(messages) >= 2
|
||||
assert messages[0]["role"] == "system"
|
||||
assert messages[-1]["role"] == "user"
|
||||
assert messages[-1]["content"] == "Test query"
|
||||
|
||||
def test_gen_inner_logs_tool_calls(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
agent.tool_calls = [{"tool": "test", "result": "success"}]
|
||||
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
agent_logs = [s for s in log_context.stacks if s["component"] == "agent"]
|
||||
assert len(agent_logs) == 1
|
||||
assert "tool_calls" in agent_logs[0]["data"]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestClassicAgentIntegration:
|
||||
|
||||
def test_gen_method_with_logging(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
results = list(agent.gen("Test query", mock_retriever))
|
||||
|
||||
assert len(results) >= 1
|
||||
|
||||
def test_gen_method_decorator_applied(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Processed"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
assert hasattr(agent.gen, "__wrapped__")
|
||||
519
tests/agents/test_react_agent.py
Normal file
@@ -0,0 +1,519 @@
|
||||
from unittest.mock import Mock, mock_open, patch
|
||||
|
||||
import pytest
|
||||
from application.agents.react_agent import ReActAgent
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReActAgent:
|
||||
|
||||
def test_react_agent_initialization(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
assert isinstance(agent, ReActAgent)
|
||||
assert agent.plan == ""
|
||||
assert agent.observations == []
|
||||
|
||||
def test_react_agent_inherits_base_properties(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
assert agent.endpoint == agent_base_params["endpoint"]
|
||||
assert agent.llm_name == agent_base_params["llm_name"]
|
||||
assert agent.gpt_model == agent_base_params["gpt_model"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReActAgentContentExtraction:
|
||||
|
||||
def test_extract_content_from_string(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = "Simple string response"
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "Simple string response"
|
||||
|
||||
def test_extract_content_from_message_object(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = Mock()
|
||||
response.message = Mock()
|
||||
response.message.content = "Message content"
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "Message content"
|
||||
|
||||
def test_extract_content_from_openai_response(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = Mock()
|
||||
response.choices = [Mock()]
|
||||
response.choices[0].message = Mock()
|
||||
response.choices[0].message.content = "OpenAI content"
|
||||
response.message = None
|
||||
response.content = None
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "OpenAI content"
|
||||
|
||||
def test_extract_content_from_anthropic_response(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
text_block = Mock()
|
||||
text_block.text = "Anthropic content"
|
||||
|
||||
response = Mock()
|
||||
response.content = [text_block]
|
||||
response.message = None
|
||||
response.choices = None
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "Anthropic content"
|
||||
|
||||
def test_extract_content_from_openai_stream(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
chunk1 = Mock()
|
||||
chunk1.choices = [Mock()]
|
||||
chunk1.choices[0].delta = Mock()
|
||||
chunk1.choices[0].delta.content = "Part 1 "
|
||||
|
||||
chunk2 = Mock()
|
||||
chunk2.choices = [Mock()]
|
||||
chunk2.choices[0].delta = Mock()
|
||||
chunk2.choices[0].delta.content = "Part 2"
|
||||
|
||||
response = iter([chunk1, chunk2])
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "Part 1 Part 2"
|
||||
|
||||
def test_extract_content_from_anthropic_stream(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
chunk1 = Mock()
|
||||
chunk1.type = "content_block_delta"
|
||||
chunk1.delta = Mock()
|
||||
chunk1.delta.text = "Stream 1 "
|
||||
chunk1.choices = []
|
||||
|
||||
chunk2 = Mock()
|
||||
chunk2.type = "content_block_delta"
|
||||
chunk2.delta = Mock()
|
||||
chunk2.delta.text = "Stream 2"
|
||||
chunk2.choices = []
|
||||
|
||||
response = iter([chunk1, chunk2])
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "Stream 1 Stream 2"
|
||||
|
||||
def test_extract_content_from_string_stream(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = iter(["chunk1", "chunk2", "chunk3"])
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == "chunk1chunk2chunk3"
|
||||
|
||||
def test_extract_content_handles_none_content(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = Mock()
|
||||
response.message = Mock()
|
||||
response.message.content = None
|
||||
response.choices = None
|
||||
response.content = None
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
|
||||
assert content == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReActAgentPlanning:
|
||||
|
||||
@patch(
|
||||
"builtins.open",
|
||||
new_callable=mock_open,
|
||||
read_data="Test planning prompt: {query} {summaries} {prompt} {observations}",
|
||||
)
|
||||
def test_create_plan(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
yield "Plan step 1"
|
||||
yield "Plan step 2"
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=mock_gen_stream())
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
agent.observations = ["Observation 1"]
|
||||
|
||||
plan_chunks = list(agent._create_plan("Test query", "Test docs", log_context))
|
||||
|
||||
assert len(plan_chunks) == 2
|
||||
assert plan_chunks[0] == "Plan step 1"
|
||||
assert plan_chunks[1] == "Plan step 2"
|
||||
|
||||
mock_llm.gen_stream.assert_called_once()
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Test: {query}")
|
||||
def test_create_plan_fills_template(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Plan"]))
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
list(agent._create_plan("My query", "Docs", log_context))
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
messages = call_args["messages"]
|
||||
|
||||
assert "My query" in messages[0]["content"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReActAgentFinalAnswer:
|
||||
|
||||
@patch(
|
||||
"builtins.open",
|
||||
new_callable=mock_open,
|
||||
read_data="Final answer for: {query} with {observations}",
|
||||
)
|
||||
def test_create_final_answer(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
yield "Final "
|
||||
yield "answer"
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=mock_gen_stream())
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
observations = ["Obs 1", "Obs 2"]
|
||||
|
||||
answer_chunks = list(
|
||||
agent._create_final_answer("Test query", observations, log_context)
|
||||
)
|
||||
|
||||
assert len(answer_chunks) == 2
|
||||
assert answer_chunks[0] == "Final "
|
||||
assert answer_chunks[1] == "answer"
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Answer: {observations}")
|
||||
def test_create_final_answer_truncates_long_observations(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
long_obs = ["A" * 15000]
|
||||
|
||||
list(agent._create_final_answer("Query", long_obs, log_context))
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
messages = call_args["messages"]
|
||||
|
||||
assert "observations truncated" in messages[0]["content"]
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Test: {query}")
|
||||
def test_create_final_answer_no_tools(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_llm,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
list(agent._create_final_answer("Query", ["Obs"], log_context))
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
|
||||
assert call_args["tools"] is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReActAgentGenInner:
|
||||
|
||||
@patch(
|
||||
"builtins.open", new_callable=mock_open, read_data="Prompt template: {query}"
|
||||
)
|
||||
def test_gen_inner_resets_state(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["SATISFIED"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "SATISFIED"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
agent.plan = "Old plan"
|
||||
agent.observations = ["Old obs"]
|
||||
|
||||
list(agent._gen_inner("New query", mock_retriever, log_context))
|
||||
|
||||
assert agent.plan != "Old plan"
|
||||
assert len(agent.observations) > 0
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Prompt: {query}")
|
||||
def test_gen_inner_stops_on_satisfied(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
iteration_count = 0
|
||||
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
nonlocal iteration_count
|
||||
iteration_count += 1
|
||||
if iteration_count == 1:
|
||||
yield "Plan"
|
||||
else:
|
||||
yield "SATISFIED - done"
|
||||
|
||||
mock_llm.gen_stream = Mock(
|
||||
side_effect=lambda *args, **kwargs: mock_gen_stream(*args, **kwargs)
|
||||
)
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "SATISFIED - done"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
assert any("answer" in r for r in results)
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Prompt: {query}")
|
||||
def test_gen_inner_max_iterations(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
call_count = 0
|
||||
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
yield f"Iteration {call_count}"
|
||||
|
||||
mock_llm.gen_stream = Mock(
|
||||
side_effect=lambda *args, **kwargs: mock_gen_stream(*args, **kwargs)
|
||||
)
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "Continue..."
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
thought_results = [r for r in results if "thought" in r]
|
||||
assert len(thought_results) > 0
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Prompt: {query}")
|
||||
def test_gen_inner_yields_sources(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["SATISFIED"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "SATISFIED"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
sources = [r for r in results if "sources" in r]
|
||||
assert len(sources) >= 1
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Prompt: {query}")
|
||||
def test_gen_inner_yields_tool_calls(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["SATISFIED"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "SATISFIED"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
agent.tool_calls = [{"tool": "test", "result": "A" * 100}]
|
||||
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
tool_call_results = [r for r in results if "tool_calls" in r]
|
||||
if tool_call_results:
|
||||
assert len(tool_call_results[0]["tool_calls"][0]["result"]) <= 53
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Prompt: {query}")
|
||||
def test_gen_inner_logs_observations(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["SATISFIED"]))
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
yield "SATISFIED"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
assert len(agent.observations) > 0
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestReActAgentIntegration:
|
||||
|
||||
@patch(
|
||||
"builtins.open",
|
||||
new_callable=mock_open,
|
||||
read_data="Prompt: {query} {summaries} {prompt} {observations}",
|
||||
)
|
||||
def test_full_react_workflow(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
mock_mongo_db,
|
||||
log_context,
|
||||
):
|
||||
call_sequence = []
|
||||
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
call_sequence.append("gen_stream")
|
||||
if len(call_sequence) <= 2:
|
||||
yield "Planning..."
|
||||
else:
|
||||
yield "SATISFIED final answer"
|
||||
|
||||
mock_llm.gen_stream = Mock(
|
||||
side_effect=lambda *args, **kwargs: mock_gen_stream(*args, **kwargs)
|
||||
)
|
||||
|
||||
def mock_handler(*args, **kwargs):
|
||||
call_sequence.append("handler")
|
||||
yield "SATISFIED final answer"
|
||||
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
results = list(agent._gen_inner("Complex query", mock_retriever, log_context))
|
||||
|
||||
assert len(results) > 0
|
||||
assert any("thought" in r for r in results)
|
||||
assert any("answer" in r for r in results)
|
||||
204
tests/agents/test_tool_action_parser.py
Normal file
@@ -0,0 +1,204 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from application.agents.tools.tool_action_parser import ToolActionParser
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestToolActionParser:
|
||||
|
||||
def test_parser_initialization(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
assert parser.llm_type == "OpenAILLM"
|
||||
assert "OpenAILLM" in parser.parsers
|
||||
assert "GoogleLLM" in parser.parsers
|
||||
|
||||
def test_parse_openai_llm_valid_call(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "get_data_123"
|
||||
call.arguments = '{"param1": "value1", "param2": "value2"}'
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "123"
|
||||
assert action_name == "get_data"
|
||||
assert call_args == {"param1": "value1", "param2": "value2"}
|
||||
|
||||
def test_parse_openai_llm_with_underscore_in_action(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "send_email_notification_456"
|
||||
call.arguments = '{"to": "user@example.com"}'
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "456"
|
||||
assert action_name == "send_email_notification"
|
||||
assert call_args == {"to": "user@example.com"}
|
||||
|
||||
def test_parse_openai_llm_invalid_format_no_underscore(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "invalidtoolname"
|
||||
call.arguments = "{}"
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id is None
|
||||
assert action_name is None
|
||||
assert call_args is None
|
||||
|
||||
def test_parse_openai_llm_non_numeric_tool_id(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "action_notanumber"
|
||||
call.arguments = "{}"
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "notanumber"
|
||||
assert action_name == "action"
|
||||
|
||||
def test_parse_openai_llm_malformed_json(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "action_123"
|
||||
call.arguments = "invalid json"
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id is None
|
||||
assert action_name is None
|
||||
assert call_args is None
|
||||
|
||||
def test_parse_openai_llm_missing_attributes(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock(spec=[])
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id is None
|
||||
assert action_name is None
|
||||
assert call_args is None
|
||||
|
||||
def test_parse_google_llm_valid_call(self):
|
||||
parser = ToolActionParser("GoogleLLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "search_documents_789"
|
||||
call.arguments = {"query": "test query", "limit": 10}
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "789"
|
||||
assert action_name == "search_documents"
|
||||
assert call_args == {"query": "test query", "limit": 10}
|
||||
|
||||
def test_parse_google_llm_with_complex_action_name(self):
|
||||
parser = ToolActionParser("GoogleLLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "create_new_user_account_999"
|
||||
call.arguments = {"username": "test"}
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "999"
|
||||
assert action_name == "create_new_user_account"
|
||||
|
||||
def test_parse_google_llm_invalid_format(self):
|
||||
parser = ToolActionParser("GoogleLLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "nounderscores"
|
||||
call.arguments = {}
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id is None
|
||||
assert action_name is None
|
||||
assert call_args is None
|
||||
|
||||
def test_parse_google_llm_missing_attributes(self):
|
||||
parser = ToolActionParser("GoogleLLM")
|
||||
|
||||
call = Mock(spec=[])
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id is None
|
||||
assert action_name is None
|
||||
assert call_args is None
|
||||
|
||||
def test_parse_unknown_llm_type_defaults_to_openai(self):
|
||||
parser = ToolActionParser("UnknownLLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "action_123"
|
||||
call.arguments = '{"key": "value"}'
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "123"
|
||||
assert action_name == "action"
|
||||
assert call_args == {"key": "value"}
|
||||
|
||||
def test_parse_args_empty_arguments_openai(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "action_123"
|
||||
call.arguments = "{}"
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "123"
|
||||
assert action_name == "action"
|
||||
assert call_args == {}
|
||||
|
||||
def test_parse_args_empty_arguments_google(self):
|
||||
parser = ToolActionParser("GoogleLLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "action_456"
|
||||
call.arguments = {}
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "456"
|
||||
assert action_name == "action"
|
||||
assert call_args == {}
|
||||
|
||||
def test_parse_args_with_special_characters(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "send_message_123"
|
||||
call.arguments = '{"message": "Hello, World! 你好"}'
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "123"
|
||||
assert action_name == "send_message"
|
||||
assert call_args["message"] == "Hello, World! 你好"
|
||||
|
||||
def test_parse_args_with_nested_objects(self):
|
||||
parser = ToolActionParser("OpenAILLM")
|
||||
|
||||
call = Mock()
|
||||
call.name = "create_record_123"
|
||||
call.arguments = '{"data": {"name": "John", "age": 30}}'
|
||||
|
||||
tool_id, action_name, call_args = parser.parse_args(call)
|
||||
|
||||
assert tool_id == "123"
|
||||
assert action_name == "create_record"
|
||||
assert call_args["data"]["name"] == "John"
|
||||
assert call_args["data"]["age"] == 30
|
||||