mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
feat: add agent webhook endpoint and implement related functionality
This commit is contained in:
@@ -4,6 +4,7 @@ import math
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import secrets
|
||||
|
||||
from bson.binary import Binary, UuidRepresentation
|
||||
from bson.dbref import DBRef
|
||||
@@ -14,7 +15,12 @@ from werkzeug.utils import secure_filename
|
||||
|
||||
from application.agents.tools.tool_manager import ToolManager
|
||||
|
||||
from application.api.user.tasks import ingest, ingest_remote, store_attachment
|
||||
from application.api.user.tasks import (
|
||||
ingest,
|
||||
ingest_remote,
|
||||
store_attachment,
|
||||
process_agent_webhook,
|
||||
)
|
||||
from application.core.mongo_db import MongoDB
|
||||
from application.core.settings import settings
|
||||
from application.extensions import api
|
||||
@@ -1329,6 +1335,88 @@ class DeleteAgent(Resource):
|
||||
return make_response(jsonify({"id": deleted_id}), 200)
|
||||
|
||||
|
||||
@user_ns.route("/api/agent_webhook")
|
||||
class AgentWebhook(Resource):
|
||||
@api.doc(
|
||||
params={"id": "ID of the agent"},
|
||||
description="Generate webhook URL for the agent",
|
||||
)
|
||||
def get(self):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
user = decoded_token.get("sub")
|
||||
agent_id = request.args.get("id")
|
||||
if not agent_id:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "ID is required"}), 400
|
||||
)
|
||||
|
||||
try:
|
||||
agent = agents_collection.find_one(
|
||||
{"_id": ObjectId(agent_id), "user": user}
|
||||
)
|
||||
if not agent:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Agent not found"}), 404
|
||||
)
|
||||
|
||||
webhook_token = agent.get("incoming_webhook_token")
|
||||
if not webhook_token:
|
||||
webhook_token = secrets.token_urlsafe(32)
|
||||
agents_collection.update_one(
|
||||
{"_id": ObjectId(agent_id), "user": user},
|
||||
{"$set": {"incoming_webhook_token": webhook_token}},
|
||||
)
|
||||
base_url = settings.API_URL.rstrip("/")
|
||||
full_webhook_url = f"{base_url}/api/webhooks/agents/{webhook_token}"
|
||||
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error generating webhook URL: {err}")
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error generating webhook URL"}),
|
||||
400,
|
||||
)
|
||||
return make_response(
|
||||
jsonify({"success": True, "webhook_url": full_webhook_url}), 200
|
||||
)
|
||||
|
||||
|
||||
@user_ns.route(f"/api/webhooks/agents/<string:webhook_token>")
|
||||
class AgentWebhookListener(Resource):
|
||||
@api.doc(description="Webhook listener for agent events")
|
||||
def post(self, webhook_token):
|
||||
agent = agents_collection.find_one(
|
||||
{"incoming_webhook_token": webhook_token}, {"_id": 1}
|
||||
)
|
||||
if not agent:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Agent not found"}), 404
|
||||
)
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "No data provided"}), 400
|
||||
)
|
||||
|
||||
agent_id_str = str(agent["_id"])
|
||||
current_app.logger.info(
|
||||
f"Incoming webhook received for agent {agent_id_str}. Enqueuing task."
|
||||
)
|
||||
|
||||
try:
|
||||
task = process_agent_webhook.delay(
|
||||
agent_id=agent_id_str,
|
||||
payload=data,
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error processing webhook: {err}")
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Error processing webhook"}), 400
|
||||
)
|
||||
return make_response(jsonify({"success": True, "task_id": task.id}), 200)
|
||||
|
||||
|
||||
@user_ns.route("/api/share")
|
||||
class ShareConversation(Resource):
|
||||
share_conversation_model = api.model(
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from application.celery_init import celery
|
||||
from application.worker import ingest_worker, remote_worker, sync_worker, attachment_worker
|
||||
from application.worker import (
|
||||
agent_webhook_worker,
|
||||
attachment_worker,
|
||||
ingest_worker,
|
||||
remote_worker,
|
||||
sync_worker,
|
||||
)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
@@ -28,6 +34,12 @@ def store_attachment(self, directory, saved_files, user):
|
||||
return resp
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def process_agent_webhook(self, agent_id, payload):
|
||||
resp = agent_webhook_worker(self, agent_id, payload)
|
||||
return resp
|
||||
|
||||
|
||||
@celery.on_after_configure.connect
|
||||
def setup_periodic_tasks(sender, **kwargs):
|
||||
sender.add_periodic_task(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
@@ -7,15 +8,20 @@ from collections import Counter
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from bson.dbref import DBRef
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from application.agents.agent_creator import AgentCreator
|
||||
from application.api.answer.routes import get_prompt
|
||||
|
||||
from application.core.mongo_db import MongoDB
|
||||
from application.core.settings import settings
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
from application.parser.chunking import Chunker
|
||||
from application.parser.embedding_pipeline import embed_and_store_documents
|
||||
from application.parser.file.bulk import SimpleDirectoryReader
|
||||
from application.parser.remote.remote_creator import RemoteCreator
|
||||
from application.parser.schema.base import Document
|
||||
from application.parser.chunking import Chunker
|
||||
from application.retriever.retriever_creator import RetrieverCreator
|
||||
from application.utils import count_tokens_docs
|
||||
|
||||
mongo = MongoDB.get_client()
|
||||
@@ -27,18 +33,22 @@ MIN_TOKENS = 150
|
||||
MAX_TOKENS = 1250
|
||||
RECURSION_DEPTH = 2
|
||||
|
||||
|
||||
# Define a function to extract metadata from a given filename.
|
||||
def metadata_from_filename(title):
|
||||
return {"title": title}
|
||||
|
||||
|
||||
# Define a function to generate a random string of a given length.
|
||||
def generate_random_string(length):
|
||||
return "".join([string.ascii_letters[i % 52] for i in range(length)])
|
||||
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
|
||||
def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
|
||||
"""
|
||||
Recursively extract zip files with a limit on recursion depth.
|
||||
@@ -69,6 +79,7 @@ def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
|
||||
file_path = os.path.join(root, file)
|
||||
extract_zip_recursive(file_path, root, current_depth + 1, max_depth)
|
||||
|
||||
|
||||
def download_file(url, params, dest_path):
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
@@ -79,6 +90,7 @@ def download_file(url, params, dest_path):
|
||||
logging.error(f"Error downloading file: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def upload_index(full_path, file_data):
|
||||
try:
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
@@ -87,7 +99,9 @@ def upload_index(full_path, file_data):
|
||||
"file_pkl": open(full_path + "/index.pkl", "rb"),
|
||||
}
|
||||
response = requests.post(
|
||||
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
|
||||
urljoin(settings.API_URL, "/api/upload_index"),
|
||||
files=files,
|
||||
data=file_data,
|
||||
)
|
||||
else:
|
||||
response = requests.post(
|
||||
@@ -102,6 +116,75 @@ def upload_index(full_path, file_data):
|
||||
for file in files.values():
|
||||
file.close()
|
||||
|
||||
|
||||
def run_agent_logic(agent_config, input_data):
|
||||
try:
|
||||
source = agent_config.get("source")
|
||||
retriever = agent_config.get("retriever", "classic")
|
||||
if isinstance(source, DBRef):
|
||||
source_doc = db.dereference(source)
|
||||
source = str(source_doc["_id"])
|
||||
retriever = source_doc.get("retriever", agent_config.get("retriever"))
|
||||
else:
|
||||
source = {}
|
||||
source = {"active_docs": source}
|
||||
chunks = int(agent_config.get("chunks", 2))
|
||||
prompt_id = agent_config.get("prompt_id", "default")
|
||||
user_api_key = agent_config["key"]
|
||||
agent_type = agent_config.get("agent_type", "classic")
|
||||
decoded_token = {"sub": agent_config.get("user")}
|
||||
prompt = get_prompt(prompt_id)
|
||||
agent = AgentCreator.create_agent(
|
||||
agent_type,
|
||||
endpoint="webhook",
|
||||
llm_name=settings.LLM_NAME,
|
||||
gpt_model=settings.MODEL_NAME,
|
||||
api_key=settings.API_KEY,
|
||||
user_api_key=user_api_key,
|
||||
prompt=prompt,
|
||||
chat_history=[],
|
||||
decoded_token=decoded_token,
|
||||
attachments=[],
|
||||
)
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever,
|
||||
source=source,
|
||||
chat_history=[],
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=settings.DEFAULT_MAX_HISTORY,
|
||||
gpt_model=settings.MODEL_NAME,
|
||||
user_api_key=user_api_key,
|
||||
decoded_token=decoded_token,
|
||||
)
|
||||
answer = agent.gen(query=input_data, retriever=retriever)
|
||||
response_full = ""
|
||||
thought = ""
|
||||
source_log_docs = []
|
||||
tool_calls = []
|
||||
|
||||
for line in answer:
|
||||
if "answer" in line:
|
||||
response_full += str(line["answer"])
|
||||
elif "sources" in line:
|
||||
source_log_docs.extend(line["sources"])
|
||||
elif "tool_calls" in line:
|
||||
tool_calls.extend(line["tool_calls"])
|
||||
elif "thought" in line:
|
||||
thought += line["thought"]
|
||||
|
||||
result = {
|
||||
"answer": response_full,
|
||||
"sources": source_log_docs,
|
||||
"tool_calls": tool_calls,
|
||||
"thought": thought,
|
||||
}
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.error(f"Error in run_agent_logic: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
# Define the main function for ingesting and processing documents.
|
||||
def ingest_worker(
|
||||
self, directory, formats, name_job, filename, user, retriever="classic"
|
||||
@@ -133,7 +216,11 @@ def ingest_worker(
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
os.makedirs(full_path)
|
||||
download_file(urljoin(settings.API_URL, "/api/download"), file_data, os.path.join(full_path, filename))
|
||||
download_file(
|
||||
urljoin(settings.API_URL, "/api/download"),
|
||||
file_data,
|
||||
os.path.join(full_path, filename),
|
||||
)
|
||||
|
||||
# check if file is .zip and extract it
|
||||
if filename.endswith(".zip"):
|
||||
@@ -157,7 +244,7 @@ def ingest_worker(
|
||||
chunking_strategy="classic_chunk",
|
||||
max_tokens=MAX_TOKENS,
|
||||
min_tokens=MIN_TOKENS,
|
||||
duplicate_headers=False
|
||||
duplicate_headers=False,
|
||||
)
|
||||
raw_docs = chunker.chunk(documents=raw_docs)
|
||||
|
||||
@@ -172,12 +259,14 @@ def ingest_worker(
|
||||
for i in range(min(5, len(raw_docs))):
|
||||
logging.info(f"Sample document {i}: {raw_docs[i]}")
|
||||
|
||||
file_data.update({
|
||||
"tokens": tokens,
|
||||
"retriever": retriever,
|
||||
"id": str(id),
|
||||
"type": "local",
|
||||
})
|
||||
file_data.update(
|
||||
{
|
||||
"tokens": tokens,
|
||||
"retriever": retriever,
|
||||
"id": str(id),
|
||||
"type": "local",
|
||||
}
|
||||
)
|
||||
upload_index(full_path, file_data)
|
||||
|
||||
# delete local
|
||||
@@ -192,6 +281,7 @@ def ingest_worker(
|
||||
"limited": False,
|
||||
}
|
||||
|
||||
|
||||
def remote_worker(
|
||||
self,
|
||||
source_data,
|
||||
@@ -203,7 +293,7 @@ def remote_worker(
|
||||
sync_frequency="never",
|
||||
operation_mode="upload",
|
||||
doc_id=None,
|
||||
):
|
||||
):
|
||||
full_path = os.path.join(directory, user, name_job)
|
||||
if not os.path.exists(full_path):
|
||||
os.makedirs(full_path)
|
||||
@@ -218,7 +308,7 @@ def remote_worker(
|
||||
chunking_strategy="classic_chunk",
|
||||
max_tokens=MAX_TOKENS,
|
||||
min_tokens=MIN_TOKENS,
|
||||
duplicate_headers=False
|
||||
duplicate_headers=False,
|
||||
)
|
||||
docs = chunker.chunk(documents=raw_docs)
|
||||
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
|
||||
@@ -260,6 +350,7 @@ def remote_worker(
|
||||
logging.info("remote_worker task completed successfully")
|
||||
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}
|
||||
|
||||
|
||||
def sync(
|
||||
self,
|
||||
source_data,
|
||||
@@ -289,6 +380,7 @@ def sync(
|
||||
return {"status": "error", "error": str(e)}
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
def sync_worker(self, frequency):
|
||||
sync_counts = Counter()
|
||||
sources = sources_collection.find()
|
||||
@@ -313,84 +405,137 @@ def sync_worker(self, frequency):
|
||||
for key in ["total_sync_count", "sync_success", "sync_failure"]
|
||||
}
|
||||
|
||||
|
||||
def attachment_worker(self, directory, file_info, user):
|
||||
"""
|
||||
Process and store a single attachment without vectorization.
|
||||
|
||||
|
||||
Args:
|
||||
self: Reference to the instance of the task.
|
||||
directory (str): Base directory for storing files.
|
||||
file_info (dict): Dictionary with folder and filename info.
|
||||
user (str): User identifier.
|
||||
|
||||
|
||||
Returns:
|
||||
dict: Information about processed attachment.
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
from application.utils import num_tokens_from_string
|
||||
|
||||
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
attachments_collection = db["attachments"]
|
||||
|
||||
|
||||
filename = file_info["filename"]
|
||||
attachment_id = file_info["attachment_id"]
|
||||
|
||||
logging.info(f"Processing attachment: {attachment_id}/{filename}", extra={"user": user})
|
||||
|
||||
|
||||
logging.info(
|
||||
f"Processing attachment: {attachment_id}/{filename}", extra={"user": user}
|
||||
)
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 10})
|
||||
|
||||
|
||||
file_path = os.path.join(directory, filename)
|
||||
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
logging.warning(f"File not found: {file_path}", extra={"user": user})
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
|
||||
try:
|
||||
reader = SimpleDirectoryReader(
|
||||
input_files=[file_path]
|
||||
)
|
||||
reader = SimpleDirectoryReader(input_files=[file_path])
|
||||
documents = reader.load_data()
|
||||
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 50})
|
||||
|
||||
|
||||
if documents:
|
||||
content = documents[0].text
|
||||
token_count = num_tokens_from_string(content)
|
||||
|
||||
|
||||
file_path_relative = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{attachment_id}/{filename}"
|
||||
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||
|
||||
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
||||
|
||||
doc_id = ObjectId(attachment_id)
|
||||
attachments_collection.insert_one({
|
||||
"_id": doc_id,
|
||||
"user": user,
|
||||
"path": file_path_relative,
|
||||
"content": content,
|
||||
"token_count": token_count,
|
||||
"mime_type": mime_type,
|
||||
"date": datetime.datetime.now(),
|
||||
})
|
||||
|
||||
logging.info(f"Stored attachment with ID: {attachment_id}",
|
||||
extra={"user": user})
|
||||
|
||||
attachments_collection.insert_one(
|
||||
{
|
||||
"_id": doc_id,
|
||||
"user": user,
|
||||
"path": file_path_relative,
|
||||
"content": content,
|
||||
"token_count": token_count,
|
||||
"mime_type": mime_type,
|
||||
"date": datetime.datetime.now(),
|
||||
}
|
||||
)
|
||||
|
||||
logging.info(
|
||||
f"Stored attachment with ID: {attachment_id}", extra={"user": user}
|
||||
)
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 100})
|
||||
|
||||
|
||||
return {
|
||||
"filename": filename,
|
||||
"path": file_path_relative,
|
||||
"token_count": token_count,
|
||||
"attachment_id": attachment_id,
|
||||
"mime_type": mime_type
|
||||
"mime_type": mime_type,
|
||||
}
|
||||
else:
|
||||
logging.warning("No content was extracted from the file",
|
||||
extra={"user": user})
|
||||
logging.warning(
|
||||
"No content was extracted from the file", extra={"user": user}
|
||||
)
|
||||
raise ValueError("No content was extracted from the file")
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing file {filename}: {e}", extra={"user": user}, exc_info=True)
|
||||
logging.error(
|
||||
f"Error processing file {filename}: {e}",
|
||||
extra={"user": user},
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def agent_webhook_worker(self, agent_id, payload):
|
||||
"""
|
||||
Process the webhook payload for an agent.
|
||||
|
||||
Args:
|
||||
self: Reference to the instance of the task.
|
||||
agent_id (str): Unique identifier for the agent.
|
||||
payload (dict): The payload data from the webhook.
|
||||
|
||||
Returns:
|
||||
dict: Information about the processed webhook.
|
||||
"""
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
agents_collection = db["agents"]
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 1})
|
||||
try:
|
||||
agent_oid = ObjectId(agent_id)
|
||||
agent_config = agents_collection.find_one({"_id": agent_oid})
|
||||
if not agent_config:
|
||||
raise ValueError(f"Agent with ID {agent_id} not found.")
|
||||
input_data = payload.get("query", "")
|
||||
if input_data is None or not isinstance(input_data, str):
|
||||
input_data = json.dumps(payload)
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing agent webhook: {e}", exc_info=True)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 50})
|
||||
try:
|
||||
result = run_agent_logic(agent_config, input_data)
|
||||
except Exception as e:
|
||||
logging.error(f"Error running agent logic: {e}", exc_info=True)
|
||||
return {"status": "error", "error": str(e)}
|
||||
finally:
|
||||
self.update_state(state="PROGRESS", meta={"current": 100})
|
||||
logging.info(
|
||||
f"Webhook processed for agent {agent_id}", extra={"agent_id": agent_id}
|
||||
)
|
||||
return {"status": "success", "result": result}
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
setModalStateDeleteConv,
|
||||
setSelectedAgent,
|
||||
setAgents,
|
||||
selectAgents,
|
||||
} from './preferences/preferenceSlice';
|
||||
import Upload from './upload/Upload';
|
||||
|
||||
@@ -63,6 +64,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
const conversations = useSelector(selectConversations);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
||||
const agents = useSelector(selectAgents);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
|
||||
const { isMobile } = useMediaQuery();
|
||||
@@ -76,6 +78,31 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
|
||||
const navRef = useRef(null);
|
||||
|
||||
async function fetchRecentAgents() {
|
||||
try {
|
||||
let recentAgents: Agent[] = [];
|
||||
if (!agents) {
|
||||
const response = await userService.getAgents(token);
|
||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||
const data: Agent[] = await response.json();
|
||||
dispatch(setAgents(data));
|
||||
recentAgents = data;
|
||||
} else recentAgents = agents;
|
||||
setRecentAgents(
|
||||
recentAgents
|
||||
.filter((agent: Agent) => agent.status === 'published')
|
||||
.sort(
|
||||
(a: Agent, b: Agent) =>
|
||||
new Date(b.last_used_at ?? 0).getTime() -
|
||||
new Date(a.last_used_at ?? 0).getTime(),
|
||||
)
|
||||
.slice(0, 3),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch recent agents: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchConversations() {
|
||||
dispatch(setConversations({ ...conversations, loading: true }));
|
||||
return await getConversations(token)
|
||||
@@ -88,25 +115,11 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
});
|
||||
}
|
||||
|
||||
async function getAgents() {
|
||||
const response = await userService.getAgents(token);
|
||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||
const data: Agent[] = await response.json();
|
||||
dispatch(setAgents(data));
|
||||
setRecentAgents(
|
||||
data
|
||||
.filter((agent: Agent) => agent.status === 'published')
|
||||
.sort(
|
||||
(a: Agent, b: Agent) =>
|
||||
new Date(b.last_used_at ?? 0).getTime() -
|
||||
new Date(a.last_used_at ?? 0).getTime(),
|
||||
)
|
||||
.slice(0, 3),
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (token) fetchRecentAgents();
|
||||
}, [agents, token, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (recentAgents.length === 0) getAgents();
|
||||
if (!conversations?.data) fetchConversations();
|
||||
if (queries.length === 0) resetConversation();
|
||||
}, [conversations?.data, dispatch]);
|
||||
|
||||
@@ -141,6 +141,7 @@ export default function AgentPreview() {
|
||||
loading={status === 'loading'}
|
||||
showSourceButton={selectedAgent ? false : true}
|
||||
showToolButton={selectedAgent ? false : true}
|
||||
autoFocus={false}
|
||||
/>
|
||||
<p className="w-full self-center bg-transparent pt-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline">
|
||||
This is a preview of the agent. You can publish it to start using it
|
||||
|
||||
@@ -155,9 +155,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
const data = await response.json();
|
||||
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
|
||||
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
|
||||
if (effectiveMode === 'new') {
|
||||
setAgentDetails('ACTIVE');
|
||||
if (effectiveMode === 'new' || effectiveMode === 'draft') {
|
||||
setEffectiveMode('edit');
|
||||
setAgent((prev) => ({ ...prev, status: 'published' }));
|
||||
setAgentDetails('ACTIVE');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -408,7 +409,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
agent.prompt_id
|
||||
? prompts.filter(
|
||||
(prompt) => prompt.id === agent.prompt_id,
|
||||
)[0].name || null
|
||||
)[0]?.name || null
|
||||
: null
|
||||
}
|
||||
onSelect={(option: { label: string; value: string }) =>
|
||||
@@ -532,7 +533,7 @@ function AgentPreviewArea() {
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
return (
|
||||
<div className="h-full w-full rounded-[30px] border border-[#F6F6F6] bg-white dark:border-[#7E7E7E] dark:bg-[#222327] max-[1180px]:h-[48rem]">
|
||||
{selectedAgent?.id ? (
|
||||
{selectedAgent?.status === 'published' ? (
|
||||
<div className="flex h-full w-full flex-col justify-end overflow-auto rounded-[30px]">
|
||||
<AgentPreview />
|
||||
</div>
|
||||
@@ -540,7 +541,7 @@ function AgentPreviewArea() {
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
|
||||
<p className="text-xs text-[#18181B] dark:text-[#949494]">
|
||||
Published agents can be previewd here
|
||||
Published agents can be previewed here
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,13 @@ import ThreeDots from '../assets/three-dots.svg';
|
||||
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { selectToken, setSelectedAgent } from '../preferences/preferenceSlice';
|
||||
import {
|
||||
selectToken,
|
||||
setSelectedAgent,
|
||||
setAgents,
|
||||
selectAgents,
|
||||
selectSelectedAgent,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import AgentLogs from './AgentLogs';
|
||||
import NewAgent from './NewAgent';
|
||||
import { Agent } from './types';
|
||||
@@ -31,9 +37,12 @@ export default function Agents() {
|
||||
|
||||
function AgentsList() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
const agents = useSelector(selectAgents);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
|
||||
const [userAgents, setUserAgents] = useState<Agent[]>([]);
|
||||
const [userAgents, setUserAgents] = useState<Agent[]>(agents || []);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const getAgents = async () => {
|
||||
@@ -43,6 +52,7 @@ function AgentsList() {
|
||||
if (!response.ok) throw new Error('Failed to fetch agents');
|
||||
const data = await response.json();
|
||||
setUserAgents(data);
|
||||
dispatch(setAgents(data));
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
@@ -52,6 +62,7 @@ function AgentsList() {
|
||||
|
||||
useEffect(() => {
|
||||
getAgents();
|
||||
if (selectedAgent) dispatch(setSelectedAgent(null));
|
||||
}, [token]);
|
||||
return (
|
||||
<div className="p-4 md:p-12">
|
||||
@@ -62,6 +73,7 @@ function AgentsList() {
|
||||
Discover and create custom versions of DocsGPT that combine
|
||||
instructions, extra knowledge, and any combination of skills.
|
||||
</p>
|
||||
{/* Premade agents section */}
|
||||
{/* <div className="mt-6">
|
||||
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
||||
Premade by DocsGPT
|
||||
@@ -200,8 +212,10 @@ function AgentCard({
|
||||
];
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(setSelectedAgent(agent));
|
||||
navigate(`/`);
|
||||
if (agent.status === 'published') {
|
||||
dispatch(setSelectedAgent(agent));
|
||||
navigate(`/`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (agentId: string) => {
|
||||
@@ -214,8 +228,11 @@ function AgentCard({
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-44 w-48 cursor-pointer flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 dark:bg-[#383838]"
|
||||
onClick={(e) => handleClick()}
|
||||
className={`relative flex h-44 w-48 flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 hover:bg-[#ECECEC] dark:bg-[#383838] hover:dark:bg-[#383838]/80 ${agent.status === 'published' && 'cursor-pointer'}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClick();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={menuRef}
|
||||
|
||||
@@ -13,6 +13,7 @@ const endpoints = {
|
||||
CREATE_AGENT: '/api/create_agent',
|
||||
UPDATE_AGENT: (agent_id: string) => `/api/update_agent/${agent_id}`,
|
||||
DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`,
|
||||
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
|
||||
PROMPTS: '/api/get_prompts',
|
||||
CREATE_PROMPT: '/api/create_prompt',
|
||||
DELETE_PROMPT: '/api/delete_prompt',
|
||||
|
||||
@@ -31,6 +31,8 @@ const userService = {
|
||||
apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
|
||||
deleteAgent: (id: string, token: string | null): Promise<any> =>
|
||||
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
|
||||
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
|
||||
getPrompts: (token: string | null): Promise<any> =>
|
||||
apiClient.get(endpoints.USER.PROMPTS, token),
|
||||
createPrompt: (data: any, token: string | null): Promise<any> =>
|
||||
|
||||
@@ -36,15 +36,7 @@ type MessageInputProps = {
|
||||
loading: boolean;
|
||||
showSourceButton?: boolean;
|
||||
showToolButton?: boolean;
|
||||
};
|
||||
|
||||
type UploadState = {
|
||||
taskId: string;
|
||||
fileName: string;
|
||||
progress: number;
|
||||
attachment_id?: string;
|
||||
token_count?: number;
|
||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||
autoFocus?: boolean;
|
||||
};
|
||||
|
||||
export default function MessageInput({
|
||||
@@ -54,6 +46,7 @@ export default function MessageInput({
|
||||
loading,
|
||||
showSourceButton = true,
|
||||
showToolButton = true,
|
||||
autoFocus = true,
|
||||
}: MessageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
@@ -235,7 +228,7 @@ export default function MessageInput({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
if (autoFocus) inputRef.current?.focus();
|
||||
handleInput();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Agent } from '../agents/types';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import WrapperModal from './WrapperModal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import userService from '../api/services/userService';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import Spinner from '../components/Spinner';
|
||||
|
||||
type AgentDetailsModalProps = {
|
||||
agent: Agent;
|
||||
@@ -16,13 +21,41 @@ export default function AgentDetailsModal({
|
||||
modalState,
|
||||
setModalState,
|
||||
}: AgentDetailsModalProps) {
|
||||
const navigate = useNavigate();
|
||||
const token = useSelector(selectToken);
|
||||
|
||||
const [publicLink, setPublicLink] = useState<string | null>(null);
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string | null>(null);
|
||||
const [loadingStates, setLoadingStates] = useState({
|
||||
publicLink: false,
|
||||
apiKey: false,
|
||||
webhook: false,
|
||||
});
|
||||
|
||||
const setLoading = (
|
||||
key: 'publicLink' | 'apiKey' | 'webhook',
|
||||
state: boolean,
|
||||
) => {
|
||||
setLoadingStates((prev) => ({ ...prev, [key]: state }));
|
||||
};
|
||||
|
||||
const handleGenerateWebhook = async () => {
|
||||
setLoading('webhook', true);
|
||||
const response = await userService.getAgentWebhook(agent.id ?? '', token);
|
||||
if (!response.ok) {
|
||||
setLoading('webhook', false);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
setWebhookUrl(data.webhook_url);
|
||||
setLoading('webhook', false);
|
||||
};
|
||||
|
||||
if (modalState !== 'ACTIVE') return null;
|
||||
return (
|
||||
<WrapperModal
|
||||
className="sm:w-[512px]"
|
||||
close={() => {
|
||||
// if (mode === 'new') navigate('/agents');
|
||||
setModalState('INACTIVE');
|
||||
}}
|
||||
>
|
||||
@@ -57,9 +90,23 @@ export default function AgentDetailsModal({
|
||||
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||
Webhooks
|
||||
</h2>
|
||||
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
|
||||
Generate
|
||||
</button>
|
||||
{webhookUrl ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
||||
{webhookUrl}
|
||||
</span>
|
||||
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
|
||||
onClick={handleGenerateWebhook}
|
||||
>
|
||||
{loadingStates.webhook ? <Spinner /> : 'Generate'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,19 +40,23 @@ export default function ConfirmationModal({
|
||||
>
|
||||
<div className="relative">
|
||||
<div>
|
||||
<p className="font-base mb-1 w-[90%] text-lg break-words text-jet dark:text-bright-gray">
|
||||
<p className="font-base mb-1 w-[90%] break-words text-lg text-jet dark:text-bright-gray">
|
||||
{message}
|
||||
</p>
|
||||
<div>
|
||||
<div className="mt-6 flex flex-row-reverse gap-1">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSubmit();
|
||||
}}
|
||||
className={submitButtonClasses}
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setModalState('INACTIVE');
|
||||
handleCancel && handleCancel();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user