diff --git a/application/agents/base.py b/application/agents/base.py
index 14ecad49..64fac17b 100644
--- a/application/agents/base.py
+++ b/application/agents/base.py
@@ -10,6 +10,7 @@ from application.core.mongo_db import MongoDB
from application.llm.llm_creator import LLMCreator
from application.logging import build_stack_data, log_activity, LogContext
from application.retriever.base import BaseRetriever
+from bson.objectid import ObjectId
class BaseAgent(ABC):
@@ -23,7 +24,7 @@ class BaseAgent(ABC):
prompt: str = "",
chat_history: Optional[List[Dict]] = None,
decoded_token: Optional[Dict] = None,
- attachments: Optional[List[Dict]]=None,
+ attachments: Optional[List[Dict]] = None,
):
self.endpoint = endpoint
self.llm_name = llm_name
@@ -58,6 +59,27 @@ class BaseAgent(ABC):
) -> Generator[Dict, None, None]:
pass
+ def _get_tools(self, api_key: str = None) -> Dict[str, Dict]:
+ mongo = MongoDB.get_client()
+ db = mongo["docsgpt"]
+ agents_collection = db["agents"]
+ tools_collection = db["user_tools"]
+
+ agent_data = agents_collection.find_one({"key": api_key or self.user_api_key})
+ tool_ids = agent_data.get("tools", []) if agent_data else []
+
+ tools = (
+ tools_collection.find(
+ {"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}}
+ )
+ if tool_ids
+ else []
+ )
+ tools = list(tools)
+ tools_by_id = {str(tool["_id"]): tool for tool in tools} if tools else {}
+
+ return tools_by_id
+
def _get_user_tools(self, user="local"):
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
@@ -243,9 +265,11 @@ class BaseAgent(ABC):
tools_dict: Dict,
messages: List[Dict],
log_context: Optional[LogContext] = None,
- attachments: Optional[List[Dict]] = None
+ attachments: Optional[List[Dict]] = None,
):
- resp = self.llm_handler.handle_response(self, resp, tools_dict, messages, attachments)
+ resp = self.llm_handler.handle_response(
+ self, resp, tools_dict, messages, attachments
+ )
if log_context:
data = build_stack_data(self.llm_handler)
log_context.stacks.append({"component": "llm_handler", "data": data})
diff --git a/application/agents/classic_agent.py b/application/agents/classic_agent.py
index f0915747..4f4df5c5 100644
--- a/application/agents/classic_agent.py
+++ b/application/agents/classic_agent.py
@@ -5,21 +5,25 @@ from application.logging import LogContext
from application.retriever.base import BaseRetriever
import logging
+
logger = logging.getLogger(__name__)
+
class ClassicAgent(BaseAgent):
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
retrieved_data = self._retriever_search(retriever, query, log_context)
-
- tools_dict = self._get_user_tools(self.user)
+ if self.user_api_key:
+ tools_dict = self._get_tools(self.user_api_key)
+ else:
+ tools_dict = self._get_user_tools(self.user)
self._prepare_tools(tools_dict)
messages = self._build_messages(self.prompt, query, retrieved_data)
resp = self._llm_gen(messages, log_context)
-
+
attachments = self.attachments
if isinstance(resp, str):
@@ -33,7 +37,7 @@ class ClassicAgent(BaseAgent):
yield {"answer": resp.message.content}
return
- resp = self._llm_handler(resp, tools_dict, messages, log_context,attachments)
+ resp = self._llm_handler(resp, tools_dict, messages, log_context, attachments)
if isinstance(resp, str):
yield {"answer": resp}
diff --git a/application/agents/react_agent.py b/application/agents/react_agent.py
index f721b487..3fae1fda 100644
--- a/application/agents/react_agent.py
+++ b/application/agents/react_agent.py
@@ -30,7 +30,10 @@ class ReActAgent(BaseAgent):
) -> Generator[Dict, None, None]:
retrieved_data = self._retriever_search(retriever, query, log_context)
- tools_dict = self._get_user_tools(self.user)
+ if self.user_api_key:
+ tools_dict = self._get_tools(self.user_api_key)
+ else:
+ tools_dict = self._get_user_tools(self.user)
self._prepare_tools(tools_dict)
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py
index 7a9261bb..704c672c 100644
--- a/application/api/answer/routes.py
+++ b/application/api/answer/routes.py
@@ -86,6 +86,20 @@ def run_async_chain(chain, question, chat_history):
return result
+def get_agent_key(agent_id, user_id):
+ if not agent_id:
+ return None
+
+ agent = agents_collection.find_one({"_id": ObjectId(agent_id)})
+ if agent is None:
+ raise Exception("Agent not found", 404)
+
+ if agent.get("is_public") or agent.get("user") == user_id:
+ return str(agent["key"])
+
+ raise Exception("Unauthorized access to the agent", 403)
+
+
def get_data_from_api_key(api_key):
data = agents_collection.find_one({"key": api_key})
if not data:
@@ -129,6 +143,7 @@ def save_conversation(
decoded_token,
index=None,
api_key=None,
+ agent_id=None,
):
current_time = datetime.datetime.now(datetime.timezone.utc)
if conversation_id is not None and index is not None:
@@ -202,6 +217,8 @@ def save_conversation(
],
}
if api_key:
+ if agent_id:
+ conversation_data["agent_id"] = agent_id
api_key_doc = agents_collection.find_one({"key": api_key})
if api_key_doc:
conversation_data["api_key"] = api_key_doc["key"]
@@ -234,6 +251,7 @@ def complete_stream(
index=None,
should_save_conversation=True,
attachments=None,
+ agent_id=None,
):
try:
response_full, thought, source_log_docs, tool_calls = "", "", [], []
@@ -297,6 +315,7 @@ def complete_stream(
decoded_token,
index,
api_key=user_api_key,
+ agent_id=agent_id,
)
else:
conversation_id = None
@@ -404,7 +423,14 @@ class Stream(Resource):
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
+ agent_id = data.get("agent_id", None)
agent_type = settings.AGENT_NAME
+ agent_key = get_agent_key(agent_id, request.decoded_token.get("sub"))
+
+ if agent_key:
+ data.update({"api_key": agent_key})
+ else:
+ agent_id = None
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
@@ -479,6 +505,7 @@ class Stream(Resource):
isNoneDoc=data.get("isNoneDoc"),
index=index,
should_save_conversation=save_conv,
+ agent_id=agent_id,
),
mimetype="text/event-stream",
)
diff --git a/application/api/user/routes.py b/application/api/user/routes.py
index e3a977ef..04303caa 100644
--- a/application/api/user/routes.py
+++ b/application/api/user/routes.py
@@ -138,14 +138,24 @@ class GetConversations(Resource):
try:
conversations = (
conversations_collection.find(
- {"api_key": {"$exists": False}, "user": decoded_token.get("sub")}
+ {
+ "$or": [
+ {"api_key": {"$exists": False}},
+ {"agent_id": {"$exists": True}},
+ ],
+ "user": decoded_token.get("sub"),
+ }
)
.sort("date", -1)
.limit(30)
)
list_conversations = [
- {"id": str(conversation["_id"]), "name": conversation["name"]}
+ {
+ "id": str(conversation["_id"]),
+ "name": conversation["name"],
+ "agent_id": conversation.get("agent_id", None),
+ }
for conversation in conversations
]
except Exception as err:
@@ -179,7 +189,12 @@ class GetSingleConversation(Resource):
except Exception as err:
current_app.logger.error(f"Error retrieving conversation: {err}")
return make_response(jsonify({"success": False}), 400)
- return make_response(jsonify(conversation["queries"]), 200)
+
+ data = {
+ "queries": conversation["queries"],
+ "agent_id": conversation.get("agent_id"),
+ }
+ return make_response(jsonify(data), 200)
@user_ns.route("/api/update_conversation_name")
diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx
index b8684df7..2e64f6b9 100644
--- a/frontend/src/Navigation.tsx
+++ b/frontend/src/Navigation.tsx
@@ -14,8 +14,8 @@ import Github from './assets/github.svg';
import Hamburger from './assets/hamburger.svg';
import openNewChat from './assets/openNewChat.svg';
import Robot from './assets/robot.svg';
-import Spark from './assets/spark.svg';
import SettingGear from './assets/settingGear.svg';
+import Spark from './assets/spark.svg';
import SpinnerDark from './assets/spinner-dark.svg';
import Spinner from './assets/spinner.svg';
import Twitter from './assets/TwitterX.svg';
@@ -35,13 +35,14 @@ import JWTModal from './modals/JWTModal';
import { ActiveState } from './models/misc';
import { getConversations } from './preferences/preferenceApi';
import {
- selectApiKeyStatus,
selectConversationId,
selectConversations,
selectModalStateDeleteConv,
+ selectSelectedAgent,
selectToken,
setConversations,
setModalStateDeleteConv,
+ setSelectedAgent,
} from './preferences/preferenceSlice';
import Upload from './upload/Upload';
@@ -59,7 +60,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const token = useSelector(selectToken);
const queries = useSelector(selectQueries);
const conversations = useSelector(selectConversations);
+ const conversationId = useSelector(selectConversationId);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
+ const selectedAgent = useSelector(selectSelectedAgent);
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
@@ -84,14 +87,14 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
});
}
- const getAgents = async () => {
+ async function getAgents() {
const response = await userService.getAgents(token);
if (!response.ok) throw new Error('Failed to fetch agents');
const data = await response.json();
setRecentAgents(
data.filter((agent: Agent) => agent.status === 'published'),
);
- };
+ }
useEffect(() => {
if (recentAgents.length === 0) getAgents();
@@ -120,18 +123,34 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
.catch((error) => console.error(error));
};
+ const handleAgentClick = (agent: Agent) => {
+ resetConversation();
+ dispatch(setSelectedAgent(agent));
+ if (isMobile) setNavOpen(!navOpen);
+ navigate('/');
+ };
+
const handleConversationClick = (index: string) => {
conversationService
.getConversation(index, token)
.then((response) => response.json())
.then((data) => {
navigate('/');
- dispatch(setConversation(data));
+ dispatch(setConversation(data.queries));
dispatch(
updateConversationId({
query: { conversationId: index },
}),
);
+ if (data.agent_id) {
+ userService.getAgent(data.agent_id, token).then((response) => {
+ if (response.ok) {
+ response.json().then((agent: Agent) => {
+ dispatch(setSelectedAgent(agent));
+ });
+ }
+ });
+ } else dispatch(setSelectedAgent(null));
});
};
@@ -143,6 +162,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
query: { conversationId: null },
}),
);
+ dispatch(setSelectedAgent(null));
};
const newChat = () => {
@@ -284,55 +304,73 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
)}
- {
+ {recentAgents?.length > 0 ? (
- {recentAgents?.length > 0 ? (
-
- {recentAgents.map((agent, idx) => (
-
-
-

-
-
- {agent.name}
-
-
- ))}
+
+ {recentAgents.map((agent, idx) => (
navigate('/agents')}
+ key={idx}
+ className={`mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal ${
+ agent.id === selectedAgent?.id && !conversationId
+ ? 'bg-bright-gray dark:bg-dark-charcoal'
+ : ''
+ }`}
+ onClick={() => handleAgentClick(agent)}
>
- Manage Agents
+ {agent.name}
+ ))}
+
+
{
+ dispatch(setSelectedAgent(null));
+ navigate('/agents');
+ }}
+ >
+
+
- ) : (
-
- No agents available.
-
- )}
+
+ Manage Agents
+
+
- }
+ ) : (
+
navigate('/agents')}
+ >
+
+

+
+
+ Manage Agents
+
+
+ )}
{conversations?.data && conversations.data.length > 0 ? (
diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx
index 1e3576ee..c6a2d524 100644
--- a/frontend/src/components/MessageInput.tsx
+++ b/frontend/src/components/MessageInput.tsx
@@ -1,43 +1,51 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useDarkTheme } from '../hooks';
-import { useSelector, useDispatch } from 'react-redux';
-import userService from '../api/services/userService';
+import { useDispatch, useSelector } from 'react-redux';
+
import endpoints from '../api/endpoints';
+import userService from '../api/services/userService';
+import ClipIcon from '../assets/clip.svg';
import PaperPlane from '../assets/paper_plane.svg';
import SourceIcon from '../assets/source.svg';
-import ToolIcon from '../assets/tool.svg';
import SpinnerDark from '../assets/spinner-dark.svg';
import Spinner from '../assets/spinner.svg';
+import ToolIcon from '../assets/tool.svg';
+import { setAttachments } from '../conversation/conversationSlice';
+import { useDarkTheme } from '../hooks';
+import { ActiveState } from '../models/misc';
+import {
+ selectSelectedDocs,
+ selectToken,
+} from '../preferences/preferenceSlice';
+import Upload from '../upload/Upload';
import SourcesPopup from './SourcesPopup';
import ToolsPopup from './ToolsPopup';
-import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice';
-import { ActiveState } from '../models/misc';
-import Upload from '../upload/Upload';
-import ClipIcon from '../assets/clip.svg';
-import { setAttachments } from '../conversation/conversationSlice';
-interface MessageInputProps {
+type MessageInputProps = {
value: string;
onChange: (e: React.ChangeEvent
) => void;
onSubmit: () => void;
loading: boolean;
-}
+ showSourceButton?: boolean;
+ showToolButton?: boolean;
+};
-interface UploadState {
+type UploadState = {
taskId: string;
fileName: string;
progress: number;
attachment_id?: string;
token_count?: number;
status: 'uploading' | 'processing' | 'completed' | 'failed';
-}
+};
export default function MessageInput({
value,
onChange,
onSubmit,
loading,
+ showSourceButton = true,
+ showToolButton = true,
}: MessageInputProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
@@ -46,12 +54,13 @@ export default function MessageInput({
const toolButtonRef = useRef(null);
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
- const [uploadModalState, setUploadModalState] = useState('INACTIVE');
+ const [uploadModalState, setUploadModalState] =
+ useState('INACTIVE');
const [uploads, setUploads] = useState([]);
const selectedDocs = useSelector(selectSelectedDocs);
const token = useSelector(selectToken);
-
+
const dispatch = useDispatch();
const handleFileAttachment = (e: React.ChangeEvent) => {
@@ -68,20 +77,20 @@ export default function MessageInput({
taskId: '',
fileName: file.name,
progress: 0,
- status: 'uploading'
+ status: 'uploading',
};
- setUploads(prev => [...prev, uploadState]);
+ setUploads((prev) => [...prev, uploadState]);
const uploadIndex = uploads.length;
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
- setUploads(prev => prev.map((upload, index) =>
- index === uploadIndex
- ? { ...upload, progress }
- : upload
- ));
+ setUploads((prev) =>
+ prev.map((upload, index) =>
+ index === uploadIndex ? { ...upload, progress } : upload,
+ ),
+ );
}
});
@@ -91,28 +100,30 @@ export default function MessageInput({
console.log('File uploaded successfully:', response);
if (response.task_id) {
- setUploads(prev => prev.map((upload, index) =>
- index === uploadIndex
- ? { ...upload, taskId: response.task_id, status: 'processing' }
- : upload
- ));
+ setUploads((prev) =>
+ prev.map((upload, index) =>
+ index === uploadIndex
+ ? { ...upload, taskId: response.task_id, status: 'processing' }
+ : upload,
+ ),
+ );
}
} else {
- setUploads(prev => prev.map((upload, index) =>
- index === uploadIndex
- ? { ...upload, status: 'failed' }
- : upload
- ));
+ setUploads((prev) =>
+ prev.map((upload, index) =>
+ index === uploadIndex ? { ...upload, status: 'failed' } : upload,
+ ),
+ );
console.error('Error uploading file:', xhr.responseText);
}
};
xhr.onerror = () => {
- setUploads(prev => prev.map((upload, index) =>
- index === uploadIndex
- ? { ...upload, status: 'failed' }
- : upload
- ));
+ setUploads((prev) =>
+ prev.map((upload, index) =>
+ index === uploadIndex ? { ...upload, status: 'failed' } : upload,
+ ),
+ );
console.error('Network error during file upload');
};
@@ -126,59 +137,64 @@ export default function MessageInput({
let timeoutIds: number[] = [];
const checkTaskStatus = () => {
- const processingUploads = uploads.filter(upload =>
- upload.status === 'processing' && upload.taskId
+ const processingUploads = uploads.filter(
+ (upload) => upload.status === 'processing' && upload.taskId,
);
- processingUploads.forEach(upload => {
+ processingUploads.forEach((upload) => {
userService
.getTaskStatus(upload.taskId, null)
.then((data) => data.json())
.then((data) => {
console.log('Task status:', data);
- setUploads(prev => prev.map(u => {
- if (u.taskId !== upload.taskId) return u;
+ setUploads((prev) =>
+ prev.map((u) => {
+ if (u.taskId !== upload.taskId) return u;
- if (data.status === 'SUCCESS') {
- return {
- ...u,
- status: 'completed',
- progress: 100,
- attachment_id: data.result?.attachment_id,
- token_count: data.result?.token_count
- };
- } else if (data.status === 'FAILURE') {
- return { ...u, status: 'failed' };
- } else if (data.status === 'PROGRESS' && data.result?.current) {
- return { ...u, progress: data.result.current };
- }
- return u;
- }));
+ if (data.status === 'SUCCESS') {
+ return {
+ ...u,
+ status: 'completed',
+ progress: 100,
+ attachment_id: data.result?.attachment_id,
+ token_count: data.result?.token_count,
+ };
+ } else if (data.status === 'FAILURE') {
+ return { ...u, status: 'failed' };
+ } else if (data.status === 'PROGRESS' && data.result?.current) {
+ return { ...u, progress: data.result.current };
+ }
+ return u;
+ }),
+ );
if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') {
- const timeoutId = window.setTimeout(() => checkTaskStatus(), 2000);
+ const timeoutId = window.setTimeout(
+ () => checkTaskStatus(),
+ 2000,
+ );
timeoutIds.push(timeoutId);
}
})
.catch((error) => {
console.error('Error checking task status:', error);
- setUploads(prev => prev.map(u =>
- u.taskId === upload.taskId
- ? { ...u, status: 'failed' }
- : u
- ));
+ setUploads((prev) =>
+ prev.map((u) =>
+ u.taskId === upload.taskId ? { ...u, status: 'failed' } : u,
+ ),
+ );
});
});
};
- if (uploads.some(upload => upload.status === 'processing')) {
+ if (uploads.some((upload) => upload.status === 'processing')) {
const timeoutId = window.setTimeout(checkTaskStatus, 2000);
timeoutIds.push(timeoutId);
}
return () => {
- timeoutIds.forEach(id => clearTimeout(id));
+ timeoutIds.forEach((id) => clearTimeout(id));
};
}, [uploads]);
@@ -213,29 +229,30 @@ export default function MessageInput({
console.log('Selected document:', doc);
};
-
const handleSubmit = () => {
const completedAttachments = uploads
- .filter(upload => upload.status === 'completed' && upload.attachment_id)
- .map(upload => ({
+ .filter((upload) => upload.status === 'completed' && upload.attachment_id)
+ .map((upload) => ({
fileName: upload.fileName,
- id: upload.attachment_id as string
+ id: upload.attachment_id as string,
}));
-
+
dispatch(setAttachments(completedAttachments));
-
+
onSubmit();
};
return (
-
-
-
+
+
+
{uploads.map((upload, index) => (
-
{upload.fileName}
+
+ {upload.fileName}
+
{upload.status === 'completed' && (
✓
@@ -245,9 +262,10 @@ export default function MessageInput({
✗
)}
- {(upload.status === 'uploading' || upload.status === 'processing') && (
-
-