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 ? (

Agents

- {recentAgents?.length > 0 ? ( -
- {recentAgents.map((agent, idx) => ( -
-
- agent-logo -
-

- {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

- Manage Agents + {agent.name}

+ ))} +
+
{ + dispatch(setSelectedAgent(null)); + navigate('/agents'); + }} + > +
+ manage-agents
- ) : ( -
- No agents available. -
- )} +

+ Manage Agents +

+
- } + ) : ( +
navigate('/agents')} + > +
+ manage-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') && ( -
- + {(upload.status === 'uploading' || + upload.status === 'processing') && ( +
+
-
-
- +
+
+ {showSourceButton && ( + + )} - -