From 93c2e2a597c4fc012a9381e1f08c473d318b4ce6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:50:32 +0000 Subject: [PATCH 01/18] build(deps): bump transformers from 4.49.0 to 4.51.3 in /application Bumps [transformers](https://github.com/huggingface/transformers) from 4.49.0 to 4.51.3. - [Release notes](https://github.com/huggingface/transformers/releases) - [Commits](https://github.com/huggingface/transformers/compare/v4.49.0...v4.51.3) --- updated-dependencies: - dependency-name: transformers dependency-version: 4.51.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 56edab9f..e360736a 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -82,7 +82,7 @@ tiktoken==0.8.0 tokenizers==0.21.0 torch==2.5.1 tqdm==4.67.1 -transformers==4.49.0 +transformers==4.51.3 typing-extensions==4.12.2 typing-inspect==0.9.0 tzdata==2024.2 From 07fa656e7ca89783372841c96a7cf7057303ac2b Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Tue, 6 May 2025 16:12:55 +0530 Subject: [PATCH 02/18] feat: implement pinning functionality for agents with UI updates --- application/api/user/routes.py | 214 ++++++++++++++++++----- frontend/package-lock.json | 7 - frontend/src/Navigation.tsx | 92 +++++++--- frontend/src/agents/AgentLogs.tsx | 17 +- frontend/src/agents/index.tsx | 72 ++++---- frontend/src/agents/types/index.ts | 2 + frontend/src/api/endpoints.ts | 2 + frontend/src/api/services/userService.ts | 4 + frontend/src/assets/pin.svg | 1 + frontend/src/assets/unpin.svg | 1 + frontend/src/components/ContextMenu.tsx | 6 +- frontend/src/index.css | 6 +- 12 files changed, 307 insertions(+), 117 deletions(-) create mode 100644 frontend/src/assets/pin.svg create mode 100644 frontend/src/assets/unpin.svg diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 3b3cb21f..93268102 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -110,7 +110,9 @@ class DeleteConversation(Resource): {"_id": ObjectId(conversation_id), "user": decoded_token["sub"]} ) except Exception as err: - current_app.logger.error(f"Error deleting conversation: {err}", exc_info=True) + current_app.logger.error( + f"Error deleting conversation: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -128,7 +130,9 @@ class DeleteAllConversations(Resource): try: conversations_collection.delete_many({"user": user_id}) except Exception as err: - current_app.logger.error(f"Error deleting all conversations: {err}", exc_info=True) + current_app.logger.error( + f"Error deleting all conversations: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -166,7 +170,9 @@ class GetConversations(Resource): for conversation in conversations ] except Exception as err: - current_app.logger.error(f"Error retrieving conversations: {err}", exc_info=True) + current_app.logger.error( + f"Error retrieving conversations: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify(list_conversations), 200) @@ -194,7 +200,9 @@ class GetSingleConversation(Resource): if not conversation: return make_response(jsonify({"status": "not found"}), 404) except Exception as err: - current_app.logger.error(f"Error retrieving conversation: {err}", exc_info=True) + current_app.logger.error( + f"Error retrieving conversation: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) data = { @@ -236,7 +244,9 @@ class UpdateConversationName(Resource): {"$set": {"name": data["name"]}}, ) except Exception as err: - current_app.logger.error(f"Error updating conversation name: {err}", exc_info=True) + current_app.logger.error( + f"Error updating conversation name: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -377,7 +387,9 @@ class DeleteOldIndexes(Resource): except FileNotFoundError: pass except Exception as err: - current_app.logger.error(f"Error deleting old indexes: {err}", exc_info=True) + current_app.logger.error( + f"Error deleting old indexes: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) sources_collection.delete_one({"_id": ObjectId(source_id)}) @@ -577,7 +589,9 @@ class UploadRemote(Resource): loader=data["source"], ) except Exception as err: - current_app.logger.error(f"Error uploading remote source: {err}", exc_info=True) + current_app.logger.error( + f"Error uploading remote source: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True, "task_id": task.id}), 200) @@ -689,7 +703,9 @@ class PaginatedSources(Resource): return make_response(jsonify(response), 200) except Exception as err: - current_app.logger.error(f"Error retrieving paginated sources: {err}", exc_info=True) + current_app.logger.error( + f"Error retrieving paginated sources: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) @@ -996,23 +1012,28 @@ class GetAgent(Resource): return make_response(jsonify({"status": "Not found"}), 404) data = { "id": str(agent["_id"]), - "name": agent["name"], - "description": agent["description"], + "name": agent.get("name", ""), + "description": agent.get("description", ""), "source": ( str(db.dereference(agent["source"])["_id"]) if "source" in agent and isinstance(agent["source"], DBRef) else "" ), - "chunks": agent["chunks"], + "chunks": agent.get("chunks", ""), "retriever": agent.get("retriever", ""), - "prompt_id": agent["prompt_id"], + "prompt_id": agent.get("prompt_id", ""), "tools": agent.get("tools", []), - "agent_type": agent["agent_type"], - "status": agent["status"], - "createdAt": agent["createdAt"], - "updatedAt": agent["updatedAt"], - "lastUsedAt": agent["lastUsedAt"], - "key": f"{agent['key'][:4]}...{agent['key'][-4:]}", + "agent_type": agent.get("agent_type", ""), + "status": agent.get("status", ""), + "created_at": agent.get("createdAt", ""), + "updated_at": agent.get("updatedAt", ""), + "last_used_at": agent.get("lastUsedAt", ""), + "key": ( + f"{agent['key'][:4]}...{agent['key'][-4:]}" + if "key" in agent + else "" + ), + "pinned": agent.get("pinned", False), } except Exception as err: current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True) @@ -1034,23 +1055,28 @@ class GetAgents(Resource): list_agents = [ { "id": str(agent["_id"]), - "name": agent["name"], - "description": agent["description"], + "name": agent.get("name", ""), + "description": agent.get("description", ""), "source": ( str(db.dereference(agent["source"])["_id"]) if "source" in agent and isinstance(agent["source"], DBRef) else "" ), - "chunks": agent["chunks"], + "chunks": agent.get("chunks", ""), "retriever": agent.get("retriever", ""), - "prompt_id": agent["prompt_id"], + "prompt_id": agent.get("prompt_id", ""), "tools": agent.get("tools", []), - "agent_type": agent["agent_type"], - "status": agent["status"], - "created_at": agent["createdAt"], - "updated_at": agent["updatedAt"], - "last_used_at": agent["lastUsedAt"], - "key": f"{agent['key'][:4]}...{agent['key'][-4:]}", + "agent_type": agent.get("agent_type", ""), + "status": agent.get("status", ""), + "created_at": agent.get("createdAt", ""), + "updated_at": agent.get("updatedAt", ""), + "last_used_at": agent.get("lastUsedAt", ""), + "key": ( + f"{agent['key'][:4]}...{agent['key'][-4:]}" + if "key" in agent + else "" + ), + "pinned": agent.get("pinned", False), } for agent in agents if "source" in agent or "retriever" in agent @@ -1196,7 +1222,9 @@ class UpdateAgent(Resource): existing_agent = agents_collection.find_one({"_id": oid, "user": user}) except Exception as err: return make_response( - current_app.logger.error(f"Error finding agent {agent_id}: {err}", exc_info=True), + current_app.logger.error( + f"Error finding agent {agent_id}: {err}", exc_info=True + ), jsonify({"success": False, "message": "Database error finding agent"}), 500, ) @@ -1319,7 +1347,9 @@ class UpdateAgent(Resource): ) except Exception as err: - current_app.logger.error(f"Error updating agent {agent_id}: {err}", exc_info=True) + current_app.logger.error( + f"Error updating agent {agent_id}: {err}", exc_info=True + ) return make_response( jsonify({"success": False, "message": "Database error during update"}), 500, @@ -1368,6 +1398,86 @@ class DeleteAgent(Resource): return make_response(jsonify({"id": deleted_id}), 200) +@user_ns.route("/api/pinned_agents") +class PinnedAgents(Resource): + @api.doc(description="Get pinned agents for the user") + def get(self): + decoded_token = request.decoded_token + if not decoded_token: + return make_response(jsonify({"success": False}), 401) + user = decoded_token.get("sub") + try: + pinned_agents = agents_collection.find({"user": user, "pinned": True}) + list_pinned_agents = [ + { + "id": str(agent["_id"]), + "name": agent.get("name", ""), + "description": agent.get("description", ""), + "source": ( + str(db.dereference(agent["source"])["_id"]) + if "source" in agent and isinstance(agent["source"], DBRef) + else "" + ), + "chunks": agent.get("chunks", ""), + "retriever": agent.get("retriever", ""), + "prompt_id": agent.get("prompt_id", ""), + "tools": agent.get("tools", []), + "agent_type": agent.get("agent_type", ""), + "status": agent.get("status", ""), + "created_at": agent.get("createdAt", ""), + "updated_at": agent.get("updatedAt", ""), + "last_used_at": agent.get("lastUsedAt", ""), + "key": ( + f"{agent['key'][:4]}...{agent['key'][-4:]}" + if "key" in agent + else "" + ), + "pinned": agent.get("pinned", False), + } + for agent in pinned_agents + if "source" in agent or "retriever" in agent + ] + except Exception as err: + current_app.logger.error(f"Error retrieving pinned agents: {err}") + return make_response(jsonify({"success": False}), 400) + return make_response(jsonify(list_pinned_agents), 200) + + +@user_ns.route("/api/pin_agent") +class PinAgent(Resource): + @api.doc(params={"id": "ID of the agent"}, description="Pin or unpin an agent") + def post(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 + ) + + pinned_status = not agent.get("pinned", False) + agents_collection.update_one( + {"_id": ObjectId(agent_id), "user": user}, + {"$set": {"pinned": pinned_status}}, + ) + except Exception as err: + current_app.logger.error(f"Error pinning/unpinning agent: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify({"success": True}), 200) + + @user_ns.route("/api/agent_webhook") class AgentWebhook(Resource): @api.doc( @@ -1405,7 +1515,9 @@ class AgentWebhook(Resource): 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}", exc_info=True) + current_app.logger.error( + f"Error generating webhook URL: {err}", exc_info=True + ) return make_response( jsonify({"success": False, "message": "Error generating webhook URL"}), 400, @@ -1694,7 +1806,9 @@ class ShareConversation(Resource): 201, ) except Exception as err: - current_app.logger.error(f"Error sharing conversation: {err}", exc_info=True) + current_app.logger.error( + f"Error sharing conversation: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) @@ -1750,7 +1864,9 @@ class GetPubliclySharedConversations(Resource): res["api_key"] = shared["api_key"] return make_response(jsonify(res), 200) except Exception as err: - current_app.logger.error(f"Error getting shared conversation: {err}", exc_info=True) + current_app.logger.error( + f"Error getting shared conversation: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) @@ -1870,7 +1986,9 @@ class GetMessageAnalytics(Resource): daily_messages[entry["_id"]] = entry["count"] except Exception as err: - current_app.logger.error(f"Error getting message analytics: {err}", exc_info=True) + current_app.logger.error( + f"Error getting message analytics: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response( @@ -2029,7 +2147,9 @@ class GetTokenAnalytics(Resource): daily_token_usage[entry["_id"]["day"]] = entry["total_tokens"] except Exception as err: - current_app.logger.error(f"Error getting token analytics: {err}", exc_info=True) + current_app.logger.error( + f"Error getting token analytics: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response( @@ -2194,7 +2314,9 @@ class GetFeedbackAnalytics(Resource): } except Exception as err: - current_app.logger.error(f"Error getting feedback analytics: {err}", exc_info=True) + current_app.logger.error( + f"Error getting feedback analytics: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response( @@ -2330,7 +2452,9 @@ class ManageSync(Resource): update_data, ) except Exception as err: - current_app.logger.error(f"Error updating sync frequency: {err}", exc_info=True) + current_app.logger.error( + f"Error updating sync frequency: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -2391,7 +2515,9 @@ class AvailableTools(Resource): } ) except Exception as err: - current_app.logger.error(f"Error getting available tools: {err}", exc_info=True) + current_app.logger.error( + f"Error getting available tools: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True, "data": tools_metadata}), 200) @@ -2595,7 +2721,9 @@ class UpdateToolConfig(Resource): {"$set": {"config": data["config"]}}, ) except Exception as err: - current_app.logger.error(f"Error updating tool config: {err}", exc_info=True) + current_app.logger.error( + f"Error updating tool config: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -2634,7 +2762,9 @@ class UpdateToolActions(Resource): {"$set": {"actions": data["actions"]}}, ) except Exception as err: - current_app.logger.error(f"Error updating tool actions: {err}", exc_info=True) + current_app.logger.error( + f"Error updating tool actions: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) @@ -2671,7 +2801,9 @@ class UpdateToolStatus(Resource): {"$set": {"status": data["status"]}}, ) except Exception as err: - current_app.logger.error(f"Error updating tool status: {err}", exc_info=True) + current_app.logger.error( + f"Error updating tool status: {err}", exc_info=True + ) return make_response(jsonify({"success": False}), 400) return make_response(jsonify({"success": True}), 200) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fa250e66..772753f2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9415,13 +9415,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 53487dd6..c0710b9c 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -13,12 +13,14 @@ import Expand from './assets/expand.svg'; import Github from './assets/github.svg'; import Hamburger from './assets/hamburger.svg'; import openNewChat from './assets/openNewChat.svg'; +import Pin from './assets/pin.svg'; import Robot from './assets/robot.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'; +import UnPin from './assets/unpin.svg'; import Help from './components/Help'; import { handleAbort, @@ -35,16 +37,16 @@ import JWTModal from './modals/JWTModal'; import { ActiveState } from './models/misc'; import { getConversations } from './preferences/preferenceApi'; import { + selectAgents, selectConversationId, selectConversations, selectModalStateDeleteConv, selectSelectedAgent, selectToken, + setAgents, setConversations, setModalStateDeleteConv, setSelectedAgent, - setAgents, - selectAgents, } from './preferences/preferenceSlice'; import Upload from './upload/Upload'; @@ -80,24 +82,35 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { async function fetchRecentAgents() { try { - let recentAgents: Agent[] = []; + const response = await userService.getPinnedAgents(token); + if (!response.ok) throw new Error('Failed to fetch pinned agents'); + const pinnedAgents: Agent[] = await response.json(); + if (pinnedAgents.length >= 3) { + setRecentAgents(pinnedAgents); + return; + } + let tempAgents: 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), - ); + tempAgents = data; + } else tempAgents = agents; + const additionalAgents = tempAgents + .filter( + (agent: Agent) => + agent.status === 'published' && + !pinnedAgents.some((pinned) => pinned.id === agent.id), + ) + .sort( + (a: Agent, b: Agent) => + new Date(b.last_used_at ?? 0).getTime() - + new Date(a.last_used_at ?? 0).getTime(), + ) + .slice(0, 3 - pinnedAgents.length); + setRecentAgents([...pinnedAgents, ...additionalAgents]); + console.log(additionalAgents); } catch (error) { console.error('Failed to fetch recent agents: ', error); } @@ -116,7 +129,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { } useEffect(() => { - if (token) fetchRecentAgents(); + fetchRecentAgents(); }, [agents, token, dispatch]); useEffect(() => { @@ -152,6 +165,17 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { navigate('/'); }; + const handleTogglePin = (agent: Agent) => { + userService.togglePinAgent(agent.id ?? '', token).then((response) => { + if (response.ok) { + const updatedAgents = agents?.map((a) => + a.id === agent.id ? { ...a, pinned: !a.pinned } : a, + ); + dispatch(setAgents(updatedAgents)); + } + }); + }; + const handleConversationClick = (index: string) => { conversationService .getConversation(index, token) @@ -336,23 +360,39 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { {recentAgents.map((agent, idx) => (
handleAgentClick(agent)} > -
- agent-logo +
+
+ agent-logo +
+

+ {agent.name} +

+
+
+
-

- {agent.name} -

))}
diff --git a/frontend/src/agents/AgentLogs.tsx b/frontend/src/agents/AgentLogs.tsx index 864a85fe..12638b8e 100644 --- a/frontend/src/agents/AgentLogs.tsx +++ b/frontend/src/agents/AgentLogs.tsx @@ -4,10 +4,10 @@ import { useNavigate, useParams } from 'react-router-dom'; import userService from '../api/services/userService'; import ArrowLeft from '../assets/arrow-left.svg'; +import Spinner from '../components/Spinner'; import { selectToken } from '../preferences/preferenceSlice'; import Analytics from '../settings/Analytics'; import Logs from '../settings/Logs'; -import Spinner from '../components/Spinner'; import { Agent } from './types'; export default function AgentLogs() { @@ -54,11 +54,16 @@ export default function AgentLogs() {
-

- Agent Name -

{agent && ( -

{agent.name}

+
+

{agent.name}

+

+ {agent.last_used_at + ? 'Last used at ' + + new Date(agent.last_used_at).toLocaleString() + : 'No usage history'} +

+
)}
{loadingAgent ? ( @@ -74,7 +79,7 @@ export default function AgentLogs() { ) : ( - agent && + agent && )} ); diff --git a/frontend/src/agents/index.tsx b/frontend/src/agents/index.tsx index c2edb34a..9034e808 100644 --- a/frontend/src/agents/index.tsx +++ b/frontend/src/agents/index.tsx @@ -1,28 +1,30 @@ -import React, { SyntheticEvent, useEffect, useRef, useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { SyntheticEvent, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { Route, Routes, useNavigate } from 'react-router-dom'; import userService from '../api/services/userService'; import Copy from '../assets/copy-linear.svg'; import Edit from '../assets/edit.svg'; import Monitoring from '../assets/monitoring.svg'; +import Pin from '../assets/pin.svg'; import Trash from '../assets/red-trash.svg'; import Robot from '../assets/robot.svg'; import ThreeDots from '../assets/three-dots.svg'; +import UnPin from '../assets/unpin.svg'; import ContextMenu, { MenuOption } from '../components/ContextMenu'; +import Spinner from '../components/Spinner'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState } from '../models/misc'; import { - selectToken, - setSelectedAgent, - setAgents, selectAgents, selectSelectedAgent, + selectToken, + setAgents, + setSelectedAgent, } from '../preferences/preferenceSlice'; import AgentLogs from './AgentLogs'; import NewAgent from './NewAgent'; import { Agent } from './types'; -import Spinner from '../components/Spinner'; export default function Agents() { return ( @@ -42,7 +44,6 @@ function AgentsList() { const agents = useSelector(selectAgents); const selectedAgent = useSelector(selectSelectedAgent); - const [userAgents, setUserAgents] = useState(agents || []); const [loading, setLoading] = useState(true); const getAgents = async () => { @@ -51,7 +52,6 @@ function AgentsList() { const response = await userService.getAgents(token); if (!response.ok) throw new Error('Failed to fetch agents'); const data = await response.json(); - setUserAgents(data); dispatch(setAgents(data)); setLoading(false); } catch (error) { @@ -133,14 +133,9 @@ function AgentsList() {
- ) : userAgents.length > 0 ? ( - userAgents.map((agent) => ( - + ) : agents && agents.length > 0 ? ( + agents.map((agent) => ( + )) ) : (
@@ -159,15 +154,7 @@ function AgentsList() { ); } -function AgentCard({ - agent, - agents, - setUserAgents, -}: { - agent: Agent; - agents: Agent[]; - setUserAgents: React.Dispatch>; -}) { +function AgentCard({ agent, agents }: { agent: Agent; agents: Agent[] }) { const navigate = useNavigate(); const dispatch = useDispatch(); const token = useSelector(selectToken); @@ -178,6 +165,21 @@ function AgentCard({ const menuRef = useRef(null); + const togglePin = async () => { + try { + const response = await userService.togglePinAgent(agent.id ?? '', token); + if (!response.ok) throw new Error('Failed to pin agent'); + const updatedAgents = agents.map((prevAgent) => { + if (prevAgent.id === agent.id) + return { ...prevAgent, pinned: !prevAgent.pinned }; + return prevAgent; + }); + dispatch(setAgents(updatedAgents)); + } catch (error) { + console.error('Error:', error); + } + }; + const menuOptions: MenuOption[] = [ { icon: Monitoring, @@ -201,6 +203,17 @@ function AgentCard({ iconWidth: 14, iconHeight: 14, }, + { + icon: agent.pinned ? UnPin : Pin, + label: agent.pinned ? 'Unpin' : 'Pin agent', + onClick: (e: SyntheticEvent) => { + e.stopPropagation(); + togglePin(); + }, + variant: 'primary', + iconWidth: 18, + iconHeight: 18, + }, { icon: Trash, label: 'Delete', @@ -209,8 +222,8 @@ function AgentCard({ setDeleteConfirmation('ACTIVE'); }, variant: 'danger', - iconWidth: 12, - iconHeight: 12, + iconWidth: 13, + iconHeight: 13, }, ]; @@ -225,9 +238,6 @@ function AgentCard({ const response = await userService.deleteAgent(agentId, token); if (!response.ok) throw new Error('Failed to delete agent'); const data = await response.json(); - setUserAgents((prevAgents) => - prevAgents.filter((prevAgent) => prevAgent.id !== data.id), - ); dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id))); }; return ( @@ -244,7 +254,7 @@ function AgentCard({ e.stopPropagation(); setIsMenuOpen(true); }} - className="absolute right-4 top-4 z-50 cursor-pointer" + className="absolute right-4 top-4 z-10 cursor-pointer" > {'use-agent'} `/api/update_agent/${agent_id}`, DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`, + PINNED_AGENTS: '/api/pinned_agents', + TOGGLE_PIN_AGENT: (id: string) => `/api/pin_agent?id=${id}`, AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`, PROMPTS: '/api/get_prompts', CREATE_PROMPT: '/api/create_prompt', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 4a0f45d8..ae2bbe8e 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -31,6 +31,10 @@ const userService = { apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token), deleteAgent: (id: string, token: string | null): Promise => apiClient.delete(endpoints.USER.DELETE_AGENT(id), token), + getPinnedAgents: (token: string | null): Promise => + apiClient.get(endpoints.USER.PINNED_AGENTS, token), + togglePinAgent: (id: string, token: string | null): Promise => + apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token), getAgentWebhook: (id: string, token: string | null): Promise => apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token), getPrompts: (token: string | null): Promise => diff --git a/frontend/src/assets/pin.svg b/frontend/src/assets/pin.svg new file mode 100644 index 00000000..4e1d1071 --- /dev/null +++ b/frontend/src/assets/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/unpin.svg b/frontend/src/assets/unpin.svg new file mode 100644 index 00000000..edb46d5d --- /dev/null +++ b/frontend/src/assets/unpin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx index f8359371..762a7828 100644 --- a/frontend/src/components/ContextMenu.tsx +++ b/frontend/src/components/ContextMenu.tsx @@ -104,8 +104,8 @@ export default function ContextMenu({ }} className={`flex items-center justify-start gap-4 p-3 transition-colors duration-200 ease-in-out ${index === 0 ? 'rounded-t-xl' : ''} ${index === options.length - 1 ? 'rounded-b-xl' : ''} ${ option.variant === 'danger' - ? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey' - : 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey' + ? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey/20' + : 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey/20' } `} > {option.icon && ( @@ -115,7 +115,7 @@ export default function ContextMenu({ height={option.iconHeight || 16} src={option.icon} alt={option.label} - className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`} + className={`cursor-pointer ${option.iconClassName || ''}`} />
)} diff --git a/frontend/src/index.css b/frontend/src/index.css index 07760385..4c1bb30f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -50,11 +50,11 @@ body.dark { @layer components { .table-default { - @apply block w-full table-auto justify-center rounded-xl border border-silver dark:border-silver/40 text-center dark:text-bright-gray overflow-auto; + @apply block w-full table-auto justify-center overflow-auto rounded-xl border border-silver text-center dark:border-silver/40 dark:text-bright-gray; } .table-default th { - @apply p-4 font-normal text-gray-400 text-nowrap; + @apply text-nowrap p-4 font-normal text-gray-400; } .table-default th { @@ -66,7 +66,7 @@ body.dark { } .table-default td { - @apply border-t w-full border-silver dark:border-silver/40 px-4 py-2; + @apply w-full border-t border-silver px-4 py-2 dark:border-silver/40; } .table-default td:last-child { From 6520be5b8516aeda63245d7953958ff2e69b1ff9 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Mon, 12 May 2025 06:06:11 +0530 Subject: [PATCH 03/18] feat: shared and pinning agents + fix for streaming tools --- application/api/answer/routes.py | 39 ++- application/api/user/routes.py | 205 ++++++++++++++ frontend/.env.development | 3 +- frontend/src/Navigation.tsx | 36 ++- frontend/src/agents/AgentCard.tsx | 123 +++++++++ frontend/src/agents/SharedAgent.tsx | 253 ++++++++++++++++++ frontend/src/agents/index.tsx | 253 +++++++++++++----- frontend/src/agents/types/index.ts | 3 + frontend/src/api/client.ts | 3 +- frontend/src/api/endpoints.ts | 3 + frontend/src/api/services/userService.ts | 6 + frontend/src/assets/link-gray.svg | 3 + .../src/conversation/ConversationMessages.tsx | 152 +++++++---- .../src/conversation/conversationHandlers.ts | 29 +- .../src/conversation/conversationSlice.ts | 4 +- frontend/src/modals/AgentDetailsModal.tsx | 69 ++++- 16 files changed, 1015 insertions(+), 169 deletions(-) create mode 100644 frontend/src/agents/AgentCard.tsx create mode 100644 frontend/src/agents/SharedAgent.tsx create mode 100644 frontend/src/assets/link-gray.svg diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index abc1f9ba..2d889b34 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -88,19 +88,28 @@ def run_async_chain(chain, question, chat_history): def get_agent_key(agent_id, user_id): if not agent_id: - return None + return None, False, None try: agent = agents_collection.find_one({"_id": ObjectId(agent_id)}) if agent is None: raise Exception("Agent not found", 404) - if agent.get("user") == user_id: + is_owner = agent.get("user") == user_id + + if is_owner: agents_collection.update_one( {"_id": ObjectId(agent_id)}, {"$set": {"lastUsedAt": datetime.datetime.now(datetime.timezone.utc)}}, ) - return str(agent["key"]) + return str(agent["key"]), False, None + + is_shared_with_user = agent.get( + "shared_publicly", False + ) or user_id in agent.get("shared_with", []) + + if is_shared_with_user: + return str(agent["key"]), True, agent.get("shared_token") raise Exception("Unauthorized access to the agent", 403) @@ -153,6 +162,8 @@ def save_conversation( index=None, api_key=None, agent_id=None, + is_shared_usage=False, + shared_token=None, ): current_time = datetime.datetime.now(datetime.timezone.utc) if conversation_id is not None and index is not None: @@ -228,6 +239,9 @@ def save_conversation( if api_key: if agent_id: conversation_data["agent_id"] = agent_id + if is_shared_usage: + conversation_data["is_shared_usage"] = is_shared_usage + conversation_data["shared_token"] = shared_token api_key_doc = agents_collection.find_one({"key": api_key}) if api_key_doc: conversation_data["api_key"] = api_key_doc["key"] @@ -261,6 +275,8 @@ def complete_stream( should_save_conversation=True, attachments=None, agent_id=None, + is_shared_usage=False, + shared_token=None, ): try: response_full, thought, source_log_docs, tool_calls = "", "", [], [] @@ -325,6 +341,8 @@ def complete_stream( index, api_key=user_api_key, agent_id=agent_id, + is_shared_usage=is_shared_usage, + shared_token=shared_token, ) else: conversation_id = None @@ -433,7 +451,9 @@ class Stream(Resource): 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")) + agent_key, is_shared_usage, shared_token = get_agent_key( + agent_id, request.decoded_token.get("sub") + ) if agent_key: data.update({"api_key": agent_key}) @@ -448,7 +468,10 @@ class Stream(Resource): retriever_name = data_key.get("retriever", retriever_name) user_api_key = data["api_key"] agent_type = data_key.get("agent_type", agent_type) - decoded_token = {"sub": data_key.get("user")} + if is_shared_usage: + decoded_token = request.decoded_token + else: + decoded_token = {"sub": data_key.get("user")} elif "active_docs" in data: source = {"active_docs": data["active_docs"]} @@ -514,6 +537,8 @@ class Stream(Resource): index=index, should_save_conversation=save_conv, agent_id=agent_id, + is_shared_usage=is_shared_usage, + shared_token=shared_token, ), mimetype="text/event-stream", ) @@ -881,6 +906,8 @@ def get_attachments_content(attachment_ids, user): if attachment_doc: attachments.append(attachment_doc) except Exception as e: - logger.error(f"Error retrieving attachment {attachment_id}: {e}", exc_info=True) + logger.error( + f"Error retrieving attachment {attachment_id}: {e}", exc_info=True + ) return attachments diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 93268102..2192241c 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -41,6 +41,12 @@ shared_conversations_collections = db["shared_conversations"] user_logs_collection = db["user_logs"] user_tools_collection = db["user_tools"] +agents_collection.create_index( + [("shared", 1)], + name="shared_index", + background=True, +) + user = Blueprint("user", __name__) user_ns = Namespace("user", description="User related operations", path="/") api.add_namespace(user_ns) @@ -166,6 +172,8 @@ class GetConversations(Resource): "id": str(conversation["_id"]), "name": conversation["name"], "agent_id": conversation.get("agent_id", None), + "is_shared_usage": conversation.get("is_shared_usage", False), + "shared_token": conversation.get("shared_token", None), } for conversation in conversations ] @@ -208,6 +216,8 @@ class GetSingleConversation(Resource): data = { "queries": conversation["queries"], "agent_id": conversation.get("agent_id"), + "is_shared_usage": conversation.get("is_shared_usage", False), + "shared_token": conversation.get("shared_token", None), } return make_response(jsonify(data), 200) @@ -1034,6 +1044,9 @@ class GetAgent(Resource): else "" ), "pinned": agent.get("pinned", False), + "shared": agent.get("shared_publicly", False), + "shared_metadata": agent.get("shared_metadata", {}), + "shared_token": agent.get("shared_token", ""), } except Exception as err: current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True) @@ -1077,6 +1090,9 @@ class GetAgents(Resource): else "" ), "pinned": agent.get("pinned", False), + "shared": agent.get("shared_publicly", False), + "shared_metadata": agent.get("shared_metadata", {}), + "shared_token": agent.get("shared_token", ""), } for agent in agents if "source" in agent or "retriever" in agent @@ -1478,6 +1494,195 @@ class PinAgent(Resource): return make_response(jsonify({"success": True}), 200) +@user_ns.route("/api/shared_agent") +class SharedAgent(Resource): + @api.doc( + params={ + "token": "Shared token of the agent", + }, + description="Get a shared agent by token or ID", + ) + def get(self): + shared_token = request.args.get("token") + + if not shared_token: + return make_response( + jsonify({"success": False, "message": "Token or ID is required"}), 400 + ) + + try: + query = {} + query["shared_publicly"] = True + query["shared_token"] = shared_token + + shared_agent = agents_collection.find_one(query) + if not shared_agent: + return make_response( + jsonify({"success": False, "message": "Shared agent not found"}), + 404, + ) + + data = { + "id": str(shared_agent["_id"]), + "user": shared_agent.get("user", ""), + "name": shared_agent.get("name", ""), + "description": shared_agent.get("description", ""), + "tools": shared_agent.get("tools", []), + "agent_type": shared_agent.get("agent_type", ""), + "status": shared_agent.get("status", ""), + "created_at": shared_agent.get("createdAt", ""), + "updated_at": shared_agent.get("updatedAt", ""), + "shared": shared_agent.get("shared_publicly", False), + "shared_token": shared_agent.get("shared_token", ""), + "shared_metadata": shared_agent.get("shared_metadata", {}), + } + + if data["tools"]: + enriched_tools = [] + for tool in data["tools"]: + tool_data = user_tools_collection.find_one({"_id": ObjectId(tool)}) + if tool_data: + enriched_tools.append(tool_data.get("displayName", "")) + data["tools"] = enriched_tools + + except Exception as err: + current_app.logger.error(f"Error retrieving shared agent: {err}") + return make_response(jsonify({"success": False}), 400) + + return make_response(jsonify(data), 200) + + +@user_ns.route("/api/shared_agents") +class SharedAgents(Resource): + @api.doc(description="Get shared agents") + def get(self): + try: + decoded_token = request.decoded_token + if not decoded_token: + return make_response(jsonify({"success": False}), 401) + user = decoded_token.get("sub") + shared_agents = agents_collection.find( + {"shared_publicly": True, "user": {"$ne": user}} + ) + list_shared_agents = [ + { + "id": str(shared_agent["_id"]), + "name": shared_agent.get("name", ""), + "description": shared_agent.get("description", ""), + "tools": shared_agent.get("tools", []), + "agent_type": shared_agent.get("agent_type", ""), + "status": shared_agent.get("status", ""), + "created_at": shared_agent.get("createdAt", ""), + "updated_at": shared_agent.get("updatedAt", ""), + "shared": shared_agent.get("shared_publicly", False), + "shared_token": shared_agent.get("shared_token", ""), + "shared_metadata": shared_agent.get("shared_metadata", {}), + } + for shared_agent in shared_agents + ] + except Exception as err: + current_app.logger.error(f"Error retrieving shared agents: {err}") + return make_response(jsonify({"success": False}), 400) + return make_response(jsonify(list_shared_agents), 200) + + +@user_ns.route("/api/share_agent") +class ShareAgent(Resource): + @api.expect( + api.model( + "ShareAgentModel", + { + "id": fields.String(required=True, description="ID of the agent"), + "shared": fields.Boolean( + required=True, description="Share or unshare the agent" + ), + "username": fields.String( + required=False, description="Name of the user" + ), + }, + ) + ) + @api.doc(description="Share or unshare an agent") + def put(self): + decoded_token = request.decoded_token + if not decoded_token: + return make_response(jsonify({"success": False}), 401) + + user = decoded_token.get("sub") + + data = request.get_json() + if not data: + return make_response( + jsonify({"success": False, "message": "Missing JSON body"}), 400 + ) + + agent_id = data.get("id") + shared = data.get("shared") + username = data.get("username", "") + + if not agent_id: + return make_response( + jsonify({"success": False, "message": "ID is required"}), 400 + ) + + if shared is None: + return make_response( + jsonify( + { + "success": False, + "message": "Shared parameter is required and must be true or false", + } + ), + 400, + ) + + try: + try: + agent_oid = ObjectId(agent_id) + except Exception: + return make_response( + jsonify({"success": False, "message": "Invalid agent ID"}), 400 + ) + + agent = agents_collection.find_one({"_id": agent_oid, "user": user}) + if not agent: + return make_response( + jsonify({"success": False, "message": "Agent not found"}), 404 + ) + + if shared: + shared_metadata = { + "shared_by": username, + "shared_at": datetime.datetime.now(datetime.timezone.utc), + } + shared_token = secrets.token_urlsafe(32) + agents_collection.update_one( + {"_id": agent_oid, "user": user}, + { + "$set": { + "shared_publicly": shared, + "shared_metadata": shared_metadata, + "shared_token": shared_token, + } + }, + ) + else: + agents_collection.update_one( + {"_id": agent_oid, "user": user}, + {"$set": {"shared_publicly": shared, "shared_token": None}}, + {"$unset": {"shared_metadata": ""}}, + ) + + except Exception as err: + current_app.logger.error(f"Error sharing/unsharing agent: {err}") + return make_response(jsonify({"success": False, "error": str(err)}), 400) + + shared_token = shared_token if shared else None + return make_response( + jsonify({"success": True, "shared_token": shared_token}), 200 + ) + + @user_ns.route("/api/agent_webhook") class AgentWebhook(Resource): @api.doc( diff --git a/frontend/.env.development b/frontend/.env.development index 7a87f762..4083d677 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,3 +1,4 @@ # Please put appropriate value -VITE_API_HOST=http://0.0.0.0:7091 +VITE_BASE_URL=http://localhost:5173 +VITE_API_HOST=http://127.0.0.1:7091 VITE_API_STREAMING=true \ No newline at end of file diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index c0710b9c..3fdcf5fd 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -110,7 +110,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { ) .slice(0, 3 - pinnedAgents.length); setRecentAgents([...pinnedAgents, ...additionalAgents]); - console.log(additionalAgents); } catch (error) { console.error('Failed to fetch recent agents: ', error); } @@ -181,22 +180,37 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { .getConversation(index, token) .then((response) => response.json()) .then((data) => { - navigate('/'); + if (data.agent_id) { + if (data.is_shared_usage) { + userService + .getSharedAgent(data.shared_token, token) + .then((response) => { + if (response.ok) { + response.json().then((agent: Agent) => { + navigate(`/agents/shared/${agent.shared_token}`); + }); + } + }); + } else { + userService.getAgent(data.agent_id, token).then((response) => { + if (response.ok) { + response.json().then((agent: Agent) => { + navigate('/'); + dispatch(setSelectedAgent(agent)); + }); + } + }); + } + } else { + navigate('/'); + dispatch(setSelectedAgent(null)); + } 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)); }); }; diff --git a/frontend/src/agents/AgentCard.tsx b/frontend/src/agents/AgentCard.tsx new file mode 100644 index 00000000..33691a52 --- /dev/null +++ b/frontend/src/agents/AgentCard.tsx @@ -0,0 +1,123 @@ +import { useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import userService from '../api/services/userService'; +import Robot from '../assets/robot.svg'; +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, + setAgents, + setSelectedAgent, +} from '../preferences/preferenceSlice'; +import { Agent } from './types'; + +type AgentCardProps = { + agent: Agent; + agents: Agent[]; + menuOptions?: MenuOption[]; + onDelete?: (agentId: string) => void; +}; + +export default function AgentCard({ + agent, + agents, + menuOptions, + onDelete, +}: AgentCardProps) { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const token = useSelector(selectToken); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [deleteConfirmation, setDeleteConfirmation] = + useState('INACTIVE'); + + const menuRef = useRef(null); + + const handleCardClick = () => { + if (agent.status === 'published') { + dispatch(setSelectedAgent(agent)); + navigate('/'); + } + }; + + const defaultDelete = async (agentId: string) => { + const response = await userService.deleteAgent(agentId, token); + if (!response.ok) throw new Error('Failed to delete agent'); + const data = await response.json(); + dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id))); + }; + + return ( +
+
{ + e.stopPropagation(); + setIsMenuOpen(true); + }} + className="absolute right-4 top-4 z-10 cursor-pointer" + > + options + {menuOptions && ( + + )} +
+ +
+
+ {`${agent.name}`} + {agent.status === 'draft' && ( +

+ (Draft) +

+ )} +
+
+

+ {agent.name} +

+

+ {agent.description} +

+
+
+ + { + onDelete ? onDelete(agent.id || '') : defaultDelete(agent.id || ''); + setDeleteConfirmation('INACTIVE'); + }} + cancelLabel="Cancel" + variant="danger" + /> +
+ ); +} diff --git a/frontend/src/agents/SharedAgent.tsx b/frontend/src/agents/SharedAgent.tsx new file mode 100644 index 00000000..65b60e3c --- /dev/null +++ b/frontend/src/agents/SharedAgent.tsx @@ -0,0 +1,253 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; + +import userService from '../api/services/userService'; +import NoFilesDarkIcon from '../assets/no-files-dark.svg'; +import NoFilesIcon from '../assets/no-files.svg'; +import Robot from '../assets/robot.svg'; +import MessageInput from '../components/MessageInput'; +import Spinner from '../components/Spinner'; +import ConversationMessages from '../conversation/ConversationMessages'; +import { Query } from '../conversation/conversationModels'; +import { + addQuery, + fetchAnswer, + resendQuery, + selectQueries, + selectStatus, + updateConversationId, +} from '../conversation/conversationSlice'; +import { useDarkTheme } from '../hooks'; +import { + selectToken, + setConversations, + setSelectedAgent, +} from '../preferences/preferenceSlice'; +import { AppDispatch } from '../store'; +import { Agent } from './types'; + +export default function SharedAgent() { + const { agentId } = useParams(); + const dispatch = useDispatch(); + const [isDarkTheme] = useDarkTheme(); + + const token = useSelector(selectToken); + const queries = useSelector(selectQueries); + const status = useSelector(selectStatus); + + const [sharedAgent, setSharedAgent] = useState(); + const [isLoading, setIsLoading] = useState(true); + const [input, setInput] = useState(''); + const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false); + + const fetchStream = useRef(null); + + const getSharedAgent = async () => { + try { + setIsLoading(true); + const response = await userService.getSharedAgent(agentId ?? '', token); + if (!response.ok) throw new Error('Failed to fetch Shared Agent'); + const agent: Agent = await response.json(); + setSharedAgent(agent); + } catch (error) { + console.error('Error: ', error); + } finally { + setIsLoading(false); + } + }; + + const handleFetchAnswer = useCallback( + ({ question, index }: { question: string; index?: number }) => { + fetchStream.current = dispatch( + fetchAnswer({ question, indx: index, isPreview: false }), + ); + }, + [dispatch], + ); + + const handleQuestion = useCallback( + ({ + question, + isRetry = false, + index = undefined, + }: { + question: string; + isRetry?: boolean; + index?: number; + }) => { + const trimmedQuestion = question.trim(); + if (trimmedQuestion === '') return; + + if (index !== undefined) { + if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion })); + handleFetchAnswer({ question: trimmedQuestion, index }); + } else { + if (!isRetry) { + const newQuery: Query = { prompt: trimmedQuestion }; + dispatch(addQuery(newQuery)); + } + handleFetchAnswer({ question: trimmedQuestion, index: undefined }); + } + }, + [dispatch, handleFetchAnswer], + ); + + const handleQuestionSubmission = ( + updatedQuestion?: string, + updated?: boolean, + indx?: number, + ) => { + if ( + updated === true && + updatedQuestion !== undefined && + indx !== undefined + ) { + handleQuestion({ + question: updatedQuestion, + index: indx, + isRetry: false, + }); + } else if (input.trim() && status !== 'loading') { + const currentInput = input.trim(); + if (lastQueryReturnedErr && queries.length > 0) { + const lastQueryIndex = queries.length - 1; + handleQuestion({ + question: currentInput, + isRetry: true, + index: lastQueryIndex, + }); + } else { + handleQuestion({ + question: currentInput, + isRetry: false, + index: undefined, + }); + } + setInput(''); + } + }; + + useEffect(() => { + if (agentId) getSharedAgent(); + }, [agentId, token]); + + useEffect(() => { + if (sharedAgent) dispatch(setSelectedAgent(sharedAgent)); + }, [sharedAgent, dispatch]); + + if (isLoading) + return ( +
+ +
+ ); + if (!sharedAgent) + return ( +
+
+ No agent found +

+ No agent found. Please ensure the agent is shared. +

+
+
+ ); + return ( +
+
+
+ + +
+ } + /> +
+
+ setInput(e.target.value)} + onSubmit={() => handleQuestionSubmission()} + loading={status === 'loading'} + showSourceButton={sharedAgent ? false : true} + showToolButton={sharedAgent ? false : true} + autoFocus={false} + /> +

+ This is a preview of the agent. You can publish it to start using it + in conversations. +

+
+
+ + ); +} + +function SharedAgentCard({ agent }: { agent: Agent }) { + return ( +
+
+
+ +
+
+

+ {agent.name} +

+

+ {agent.description} +

+
+
+
+ {agent.shared_metadata?.shared_by && ( +

+ by {agent.shared_metadata.shared_by} +

+ )} + {agent.shared_metadata?.shared_at && ( +

+ Shared on{' '} + {new Date(agent.shared_metadata.shared_at).toLocaleString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: true, + })} +

+ )} +
+
+

+ Connected Tools +

+
+ {agent.tools.map((tool, index) => ( + + {tool} + + ))} +
+
+ {/* */} +
+ ); +} diff --git a/frontend/src/agents/index.tsx b/frontend/src/agents/index.tsx index 9034e808..710f810f 100644 --- a/frontend/src/agents/index.tsx +++ b/frontend/src/agents/index.tsx @@ -9,10 +9,15 @@ import Monitoring from '../assets/monitoring.svg'; import Pin from '../assets/pin.svg'; import Trash from '../assets/red-trash.svg'; import Robot from '../assets/robot.svg'; +import Link from '../assets/link-gray.svg'; import ThreeDots from '../assets/three-dots.svg'; import UnPin from '../assets/unpin.svg'; import ContextMenu, { MenuOption } from '../components/ContextMenu'; import Spinner from '../components/Spinner'; +import { + setConversation, + updateConversationId, +} from '../conversation/conversationSlice'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState } from '../models/misc'; import { @@ -24,6 +29,7 @@ import { } from '../preferences/preferenceSlice'; import AgentLogs from './AgentLogs'; import NewAgent from './NewAgent'; +import SharedAgent from './SharedAgent'; import { Agent } from './types'; export default function Agents() { @@ -33,10 +39,26 @@ export default function Agents() { } /> } /> } /> + } /> ); } +const sectionConfig = { + user: { + title: 'By me', + description: 'Agents created or published by you', + showNewAgentButton: true, + emptyStateDescription: 'You don’t have any created agents yet', + }, + shared: { + title: 'Shared with me', + description: 'Agents imported by using a public link', + showNewAgentButton: false, + emptyStateDescription: 'No shared agents found', + }, +}; + function AgentsList() { const navigate = useNavigate(); const dispatch = useDispatch(); @@ -44,24 +66,47 @@ function AgentsList() { const agents = useSelector(selectAgents); const selectedAgent = useSelector(selectSelectedAgent); - const [loading, setLoading] = useState(true); + const [sharedAgents, setSharedAgents] = useState([]); + const [loadingUserAgents, setLoadingUserAgents] = useState(true); + const [loadingSharedAgents, setLoadingSharedAgents] = useState(true); const getAgents = async () => { try { - setLoading(true); + setLoadingUserAgents(true); const response = await userService.getAgents(token); if (!response.ok) throw new Error('Failed to fetch agents'); const data = await response.json(); dispatch(setAgents(data)); - setLoading(false); + setLoadingUserAgents(false); } catch (error) { console.error('Error:', error); - setLoading(false); + setLoadingUserAgents(false); + } + }; + + const getSharedAgents = async () => { + try { + setLoadingSharedAgents(true); + const response = await userService.getSharedAgents(token); + if (!response.ok) throw new Error('Failed to fetch shared agents'); + const data = await response.json(); + setSharedAgents(data); + setLoadingSharedAgents(false); + } catch (error) { + console.error('Error:', error); + setLoadingSharedAgents(false); } }; useEffect(() => { getAgents(); + getSharedAgents(); + dispatch(setConversation([])); + dispatch( + updateConversationId({ + query: { conversationId: null }, + }), + ); if (selectedAgent) dispatch(setSelectedAgent(null)); }, [token]); return ( @@ -71,7 +116,7 @@ function AgentsList() {

Discover and create custom versions of DocsGPT that combine - instructions, extra knowledge, and any combination of skills. + instructions, extra knowledge, and any combination of skills

{/* Premade agents section */} {/*
@@ -116,45 +161,91 @@ function AgentsList() { ))}
*/} -
-
+ + +
+ ); +} + +function AgentSection({ + agents, + loading, + section, +}: { + agents: Agent[]; + loading: boolean; + section: keyof typeof sectionConfig; +}) { + const navigate = useNavigate(); + return ( +
+
+

- Created by You + {sectionConfig[section].title}

+

+ {sectionConfig[section].description} +

+
+ {sectionConfig[section].showNewAgentButton && ( -
-
- {loading ? ( -
- -
- ) : agents && agents.length > 0 ? ( - agents.map((agent) => ( - - )) - ) : ( -
-

You don’t have any created agents yet

+ )} +
+
+ {loading ? ( +
+ +
+ ) : agents && agents.length > 0 ? ( + agents.map((agent) => ( + + )) + ) : ( +
+

{sectionConfig[section].emptyStateDescription}

+ {sectionConfig[section].showNewAgentButton && ( -
- )} -
+ )} +
+ )}
); } -function AgentCard({ agent, agents }: { agent: Agent; agents: Agent[] }) { +function AgentCard({ + agent, + agents, + section, +}: { + agent: Agent; + agents: Agent[]; + section: keyof typeof sectionConfig; +}) { const navigate = useNavigate(); const dispatch = useDispatch(); const token = useSelector(selectToken); @@ -180,57 +271,79 @@ function AgentCard({ agent, agents }: { agent: Agent; agents: Agent[] }) { } }; - const menuOptions: MenuOption[] = [ - { - icon: Monitoring, - label: 'Logs', - onClick: (e: SyntheticEvent) => { - e.stopPropagation(); - navigate(`/agents/logs/${agent.id}`); + const menuOptionsConfig: Record = { + user: [ + { + icon: Monitoring, + label: 'Logs', + onClick: (e: SyntheticEvent) => { + e.stopPropagation(); + navigate(`/agents/logs/${agent.id}`); + }, + variant: 'primary', + iconWidth: 14, + iconHeight: 14, }, - variant: 'primary', - iconWidth: 14, - iconHeight: 14, - }, - { - icon: Edit, - label: 'Edit', - onClick: (e: SyntheticEvent) => { - e.stopPropagation(); - navigate(`/agents/edit/${agent.id}`); + { + icon: Edit, + label: 'Edit', + onClick: (e: SyntheticEvent) => { + e.stopPropagation(); + navigate(`/agents/edit/${agent.id}`); + }, + variant: 'primary', + iconWidth: 14, + iconHeight: 14, }, - variant: 'primary', - iconWidth: 14, - iconHeight: 14, - }, - { - icon: agent.pinned ? UnPin : Pin, - label: agent.pinned ? 'Unpin' : 'Pin agent', - onClick: (e: SyntheticEvent) => { - e.stopPropagation(); - togglePin(); + { + icon: agent.pinned ? UnPin : Pin, + label: agent.pinned ? 'Unpin' : 'Pin agent', + onClick: (e: SyntheticEvent) => { + e.stopPropagation(); + togglePin(); + }, + variant: 'primary', + iconWidth: 18, + iconHeight: 18, }, - variant: 'primary', - iconWidth: 18, - iconHeight: 18, - }, - { - icon: Trash, - label: 'Delete', - onClick: (e: SyntheticEvent) => { - e.stopPropagation(); - setDeleteConfirmation('ACTIVE'); + { + icon: Trash, + label: 'Delete', + onClick: (e: SyntheticEvent) => { + e.stopPropagation(); + setDeleteConfirmation('ACTIVE'); + }, + variant: 'danger', + iconWidth: 13, + iconHeight: 13, }, - variant: 'danger', - iconWidth: 13, - iconHeight: 13, - }, - ]; + ], + shared: [ + { + icon: Link, + label: 'Open', + onClick: (e: SyntheticEvent) => { + e.stopPropagation(); + navigate(`/agents/shared/${agent.shared_token}`); + }, + variant: 'primary', + iconWidth: 14, + iconHeight: 14, + }, + ], + }; + + const menuOptions = menuOptionsConfig[section] || []; const handleClick = () => { - if (agent.status === 'published') { - dispatch(setSelectedAgent(agent)); - navigate(`/`); + if (section === 'user') { + if (agent.status === 'published') { + dispatch(setSelectedAgent(agent)); + navigate(`/`); + } + } + if (section === 'shared') { + navigate(`/agents/shared/${agent.shared_token}`); } }; diff --git a/frontend/src/agents/types/index.ts b/frontend/src/agents/types/index.ts index 5b05e92c..fe3cb418 100644 --- a/frontend/src/agents/types/index.ts +++ b/frontend/src/agents/types/index.ts @@ -13,6 +13,9 @@ export type Agent = { key?: string; incoming_webhook_token?: string; pinned?: boolean; + shared?: boolean; + shared_token?: string; + shared_metadata?: any; created_at?: string; updated_at?: string; last_used_at?: string; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 3db613fc..28707012 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,4 +1,5 @@ -const baseURL = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; +export const baseURL = + import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const defaultHeaders = { 'Content-Type': 'application/json', diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index 20f769c9..248a88cb 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -15,6 +15,9 @@ const endpoints = { DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`, PINNED_AGENTS: '/api/pinned_agents', TOGGLE_PIN_AGENT: (id: string) => `/api/pin_agent?id=${id}`, + SHARED_AGENT: (id: string) => `/api/shared_agent?token=${id}`, + SHARED_AGENTS: '/api/shared_agents', + SHARE_AGENT: `/api/share_agent`, AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`, PROMPTS: '/api/get_prompts', CREATE_PROMPT: '/api/create_prompt', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index ae2bbe8e..f9950835 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -35,6 +35,12 @@ const userService = { apiClient.get(endpoints.USER.PINNED_AGENTS, token), togglePinAgent: (id: string, token: string | null): Promise => apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token), + getSharedAgent: (id: string, token: string | null): Promise => + apiClient.get(endpoints.USER.SHARED_AGENT(id), token), + getSharedAgents: (token: string | null): Promise => + apiClient.get(endpoints.USER.SHARED_AGENTS, token), + shareAgent: (data: any, token: string | null): Promise => + apiClient.put(endpoints.USER.SHARE_AGENT, data, token), getAgentWebhook: (id: string, token: string | null): Promise => apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token), getPrompts: (token: string | null): Promise => diff --git a/frontend/src/assets/link-gray.svg b/frontend/src/assets/link-gray.svg new file mode 100644 index 00000000..75a24932 --- /dev/null +++ b/frontend/src/assets/link-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/conversation/ConversationMessages.tsx b/frontend/src/conversation/ConversationMessages.tsx index d1a9a24e..feb0fac4 100644 --- a/frontend/src/conversation/ConversationMessages.tsx +++ b/frontend/src/conversation/ConversationMessages.tsx @@ -1,4 +1,11 @@ -import { Fragment, useEffect, useRef, useState } from 'react'; +import { + Fragment, + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; import ArrowDown from '../assets/arrow-down.svg'; @@ -8,7 +15,12 @@ import { useDarkTheme } from '../hooks'; import ConversationBubble from './ConversationBubble'; import { FEEDBACK, Query, Status } from './conversationModels'; -interface ConversationMessagesProps { +const SCROLL_THRESHOLD = 10; +const LAST_BUBBLE_MARGIN = 'mb-32'; +const DEFAULT_BUBBLE_MARGIN = 'mb-7'; +const FIRST_QUESTION_BUBBLE_MARGIN_TOP = 'mt-5'; + +type ConversationMessagesProps = { handleQuestion: (params: { question: string; isRetry?: boolean; @@ -24,7 +36,8 @@ interface ConversationMessagesProps { queries: Query[]; status: Status; showHeroOnEmpty?: boolean; -} + headerContent?: ReactNode; +}; export default function ConversationMessages({ handleQuestion, @@ -33,22 +46,23 @@ export default function ConversationMessages({ status, handleFeedback, showHeroOnEmpty = true, + headerContent, }: ConversationMessagesProps) { const [isDarkTheme] = useDarkTheme(); const { t } = useTranslation(); const conversationRef = useRef(null); const [hasScrolledToLast, setHasScrolledToLast] = useState(true); - const [eventInterrupt, setEventInterrupt] = useState(false); + const [userInterruptedScroll, setUserInterruptedScroll] = useState(false); - const handleUserInterruption = () => { - if (!eventInterrupt && status === 'loading') { - setEventInterrupt(true); + const handleUserScrollInterruption = useCallback(() => { + if (!userInterruptedScroll && status === 'loading') { + setUserInterruptedScroll(true); } - }; + }, [userInterruptedScroll, status]); - const scrollIntoView = () => { - if (!conversationRef?.current || eventInterrupt) return; + const scrollConversationToBottom = useCallback(() => { + if (!conversationRef.current || userInterruptedScroll) return; if (status === 'idle' || !queries[queries.length - 1]?.response) { conversationRef.current.scrollTo({ @@ -58,39 +72,65 @@ export default function ConversationMessages({ } else { conversationRef.current.scrollTop = conversationRef.current.scrollHeight; } - }; + }, [userInterruptedScroll, status, queries]); - const checkScroll = () => { + const checkScrollPosition = useCallback(() => { const el = conversationRef.current; if (!el) return; - const isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 10; - setHasScrolledToLast(isBottom); - }; + const isAtBottom = + el.scrollHeight - el.scrollTop - el.clientHeight < SCROLL_THRESHOLD; + setHasScrolledToLast(isAtBottom); + }, [setHasScrolledToLast]); useEffect(() => { - !eventInterrupt && scrollIntoView(); - }, [queries.length, queries[queries.length - 1]]); + if (!userInterruptedScroll) { + scrollConversationToBottom(); + } + }, [ + queries.length, + queries[queries.length - 1]?.response, + queries[queries.length - 1]?.error, + queries[queries.length - 1]?.thought, + userInterruptedScroll, + scrollConversationToBottom, + ]); useEffect(() => { if (status === 'idle') { - setEventInterrupt(false); + setUserInterruptedScroll(false); } }, [status]); useEffect(() => { - conversationRef.current?.addEventListener('scroll', checkScroll); + const currentConversationRef = conversationRef.current; + currentConversationRef?.addEventListener('scroll', checkScrollPosition); return () => { - conversationRef.current?.removeEventListener('scroll', checkScroll); + currentConversationRef?.removeEventListener( + 'scroll', + checkScrollPosition, + ); }; - }, []); + }, [checkScrollPosition]); + + const retryIconProps = { + width: 12, + height: 12, + fill: isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)', + stroke: isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)', + strokeWidth: 10, + }; + + const renderResponseView = (query: Query, index: number) => { + const isLastMessage = index === queries.length - 1; + const bubbleMargin = isLastMessage + ? LAST_BUBBLE_MARGIN + : DEFAULT_BUBBLE_MARGIN; - const prepResponseView = (query: Query, index: number) => { - let responseView; if (query.thought || query.response) { - responseView = ( + return ( ); - } else if (query.error) { - const retryBtn = ( + } + + if (query.error) { + const retryButton = ( ); - responseView = ( + return ( ); } - return responseView; + return null; }; return (
{queries.length > 0 && !hasScrolledToLast && ( )} -
+
+ {headerContent && headerContent} + {queries.length > 0 ? ( queries.map((query, index) => ( - + - {prepResponseView(query, index)} + {renderResponseView(query, index)} )) ) : showHeroOnEmpty ? ( diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts index 2fcb2e28..717a5186 100644 --- a/frontend/src/conversation/conversationHandlers.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -142,6 +142,7 @@ export function handleFetchAnswerSteaming( .then((response) => { if (!response.body) throw Error('No response body'); + let buffer = ''; const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let counterrr = 0; @@ -157,22 +158,24 @@ export function handleFetchAnswerSteaming( counterrr += 1; const chunk = decoder.decode(value); + buffer += chunk; - const lines = chunk.split('\n'); + const events = buffer.split('\n\n'); + buffer = events.pop() ?? ''; - for (let line of lines) { - if (line.trim() == '') { - continue; + for (let event of events) { + if (event.trim().startsWith('data:')) { + const dataLine: string = event + .split('\n') + .map((line: string) => line.replace(/^data:\s?/, '')) + .join(''); + + const messageEvent = new MessageEvent('message', { + data: dataLine.trim(), + }); + + onEvent(messageEvent); } - if (line.startsWith('data:')) { - line = line.substring(5); - } - - const messageEvent: MessageEvent = new MessageEvent('message', { - data: line, - }); - - onEvent(messageEvent); // handle each message } reader.read().then(processStream).catch(reject); diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 261f9db7..961260ea 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -285,9 +285,7 @@ export const conversationSlice = createSlice({ action: PayloadAction<{ index: number; query: Partial }>, ) { const { index, query } = action.payload; - if (!state.queries[index].tool_calls) { - state.queries[index].tool_calls = query?.tool_calls; - } + state.queries[index].tool_calls = query?.tool_calls ?? []; }, updateQuery( state, diff --git a/frontend/src/modals/AgentDetailsModal.tsx b/frontend/src/modals/AgentDetailsModal.tsx index c10a837b..7d47204c 100644 --- a/frontend/src/modals/AgentDetailsModal.tsx +++ b/frontend/src/modals/AgentDetailsModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { Agent } from '../agents/types'; @@ -9,6 +9,8 @@ import { ActiveState } from '../models/misc'; import { selectToken } from '../preferences/preferenceSlice'; import WrapperModal from './WrapperModal'; +const baseURL = import.meta.env.VITE_BASE_URL; + type AgentDetailsModalProps = { agent: Agent; mode: 'new' | 'edit' | 'draft'; @@ -24,7 +26,9 @@ export default function AgentDetailsModal({ }: AgentDetailsModalProps) { const token = useSelector(selectToken); - const [publicLink, setPublicLink] = useState(null); + const [sharedToken, setSharedToken] = useState( + agent.shared_token ?? null, + ); const [apiKey, setApiKey] = useState(null); const [webhookUrl, setWebhookUrl] = useState(null); const [loadingStates, setLoadingStates] = useState({ @@ -40,6 +44,21 @@ export default function AgentDetailsModal({ setLoadingStates((prev) => ({ ...prev, [key]: state })); }; + const handleGeneratePublicLink = async () => { + setLoading('publicLink', true); + const response = await userService.shareAgent( + { id: agent.id ?? '', shared: true }, + token, + ); + if (!response.ok) { + setLoading('publicLink', false); + return; + } + const data = await response.json(); + setSharedToken(data.shared_token); + setLoading('publicLink', false); + }; + const handleGenerateWebhook = async () => { setLoading('webhook', true); const response = await userService.getAgentWebhook(agent.id ?? '', token); @@ -52,6 +71,11 @@ export default function AgentDetailsModal({ setLoading('webhook', false); }; + useEffect(() => { + setSharedToken(agent.shared_token ?? null); + setApiKey(agent.key ?? null); + }, [agent]); + if (modalState !== 'ACTIVE') return null; return (
-

- Public link -

- +
+

+ Public Link +

+ {sharedToken && ( +
+ +
+ )} +
+ {sharedToken ? ( +
+

+ {`${baseURL}/agents/shared/${sharedToken}`} +

+
+ ) : ( + + )}

API Key

- {agent.key ? ( + {apiKey ? ( - {agent.key} + {apiKey} ) : (
-
+
); return ( -
-
+
+
+ agent-logo +

+ {sharedAgent.name} +

+
+
-

- This is a preview of the agent. You can publish it to start using it - in conversations. +

+ {t('tagline')}

@@ -196,28 +202,28 @@ export default function SharedAgent() { function SharedAgentCard({ agent }: { agent: Agent }) { return ( -
+
-

+

{agent.name}

-

+

{agent.description}

{agent.shared_metadata?.shared_by && ( -

+

by {agent.shared_metadata.shared_by}

)} {agent.shared_metadata?.shared_at && ( -

+

Shared on{' '} {new Date(agent.shared_metadata.shared_at).toLocaleString('en-US', { month: 'long', @@ -230,24 +236,28 @@ function SharedAgentCard({ agent }: { agent: Agent }) {

)}
-
-

- Connected Tools -

-
- {agent.tools.map((tool, index) => ( - - {tool} - - ))} + {agent.tools.length > 0 && ( +
+

+ Connected Tools +

+
+ {agent.tools.map((tool, index) => ( + + {`${tool}{' '} + {tool} + + ))} +
-
- {/* */} + )}
); } From 1c7befb8d3f902ffe6844230bd5e7ba80614558b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 12:20:22 +0000 Subject: [PATCH 06/18] build(deps): bump torch from 2.5.1 to 2.7.0 in /application Bumps [torch](https://github.com/pytorch/pytorch) from 2.5.1 to 2.7.0. - [Release notes](https://github.com/pytorch/pytorch/releases) - [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md) - [Commits](https://github.com/pytorch/pytorch/compare/v2.5.1...v2.7.0) --- updated-dependencies: - dependency-name: torch dependency-version: 2.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index e360736a..0c6fb498 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -80,7 +80,7 @@ retry==0.9.2 sentence-transformers==3.3.1 tiktoken==0.8.0 tokenizers==0.21.0 -torch==2.5.1 +torch==2.7.0 tqdm==4.67.1 transformers==4.51.3 typing-extensions==4.12.2 From 44e98748c53887cf5fb15dcdfcb4ce67ee5d1e73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 12:44:30 +0000 Subject: [PATCH 07/18] build(deps): bump portalocker from 2.10.1 to 3.1.1 in /application Bumps [portalocker](https://github.com/wolph/portalocker) from 2.10.1 to 3.1.1. - [Release notes](https://github.com/wolph/portalocker/releases) - [Changelog](https://github.com/wolph/portalocker/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/wolph/portalocker/compare/v2.10.1...v3.1.1) --- updated-dependencies: - dependency-name: portalocker dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 0c6fb498..02476089 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -55,7 +55,7 @@ pandas==2.2.3 openpyxl==3.1.5 pathable==0.4.4 pillow==11.1.0 -portalocker==2.10.1 +portalocker==3.1.1 prance==23.6.21.0 primp==0.14.0 prompt-toolkit==3.0.50 From 39b36b6857e48ad9d352f374b957f604d11caa20 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 13 May 2025 14:03:05 +0100 Subject: [PATCH 08/18] Feat: Add MD gen script, enable Qdrant lazy loading --- application/requirements.txt | 1 - application/vectorstore/qdrant.py | 5 ++-- md-gen.py | 47 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 md-gen.py diff --git a/application/requirements.txt b/application/requirements.txt index 02476089..d68554a3 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -71,7 +71,6 @@ python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-jose==3.4.0 python-pptx==1.0.2 -qdrant-client==1.13.2 redis==5.2.1 referencing==0.30.2 regex==2024.11.6 diff --git a/application/vectorstore/qdrant.py b/application/vectorstore/qdrant.py index 3f94505f..61a9d63d 100644 --- a/application/vectorstore/qdrant.py +++ b/application/vectorstore/qdrant.py @@ -1,11 +1,12 @@ -from langchain_community.vectorstores.qdrant import Qdrant from application.vectorstore.base import BaseVectorStore from application.core.settings import settings -from qdrant_client import models class QdrantStore(BaseVectorStore): def __init__(self, source_id: str = "", embeddings_key: str = "embeddings"): + from qdrant_client import models + from langchain_community.vectorstores.qdrant import Qdrant + self._filter = models.Filter( must=[ models.FieldCondition( diff --git a/md-gen.py b/md-gen.py new file mode 100644 index 00000000..93754475 --- /dev/null +++ b/md-gen.py @@ -0,0 +1,47 @@ +import os + +def create_markdown_from_directory(directory=".", output_file="combined.md"): + """ + Recursively traverses the given directory, reads all files (ignoring files/folders in ignore_list), + and creates a single markdown file containing the contents of each file, prefixed with the + relative path of the file. + + Args: + directory (str): The directory to traverse. Defaults to the current directory. + output_file (str): The name of the output markdown file. Defaults to 'combined.md'. + """ + ignore_list = [ + "node_modules", "__pycache__", ".git", ".DS_Store", "inputs", "indexes", + "model", "models", ".venv", "temp", ".pytest_cache", ".ruff_cache", + "extensions", "dir_tree.py", "map.txt", "signal-desktop-keyring.gpg", + ".husky", ".next", "docs", "index.pkl", "index.faiss", "assets", "fonts", "public", + "yarn.lock", "package-lock.json", + ] + + with open(output_file, "w", encoding="utf-8") as outfile: + for root, dirs, files in os.walk(directory): + # Filter out directories in ignore_list so they won't be traversed + dirs[:] = [d for d in dirs if d not in ignore_list] + + for filename in files: + if filename in ignore_list: + continue + filepath = os.path.join(root, filename) + + try: + with open(filepath, "r", encoding="utf-8") as infile: + content = infile.read() + + # Get a relative path to better indicate file location + rel_path = os.path.relpath(filepath, directory) + outfile.write(f"## File: {rel_path}\n\n") + outfile.write(content) + outfile.write("\n\n---\n\n") # Separator between files + + except Exception as e: + print(f"Error processing file {filepath}: {e}") + + print(f"Successfully created {output_file}") + +if __name__ == "__main__": + create_markdown_from_directory() From fe95f6ad815002afed7974f833b5a4fc57302c3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 13:20:27 +0000 Subject: [PATCH 09/18] build(deps): bump referencing from 0.30.2 to 0.36.2 in /application Bumps [referencing](https://github.com/python-jsonschema/referencing) from 0.30.2 to 0.36.2. - [Release notes](https://github.com/python-jsonschema/referencing/releases) - [Changelog](https://github.com/python-jsonschema/referencing/blob/main/docs/changes.rst) - [Commits](https://github.com/python-jsonschema/referencing/compare/v0.30.2...v0.36.2) --- updated-dependencies: - dependency-name: referencing dependency-version: 0.36.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index d68554a3..bd6a433d 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -72,7 +72,7 @@ python-dotenv==1.0.1 python-jose==3.4.0 python-pptx==1.0.2 redis==5.2.1 -referencing==0.30.2 +referencing==0.36.2 regex==2024.11.6 requests==2.32.3 retry==0.9.2 From 526d340849a9e431db530fce514efd8d9f3a47c5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 13 May 2025 16:03:48 +0100 Subject: [PATCH 10/18] fix: stale deps --- application/requirements.txt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/application/requirements.txt b/application/requirements.txt index bd6a433d..e42d2baf 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -26,9 +26,6 @@ jmespath==1.0.1 joblib==1.4.2 jsonpatch==1.33 jsonpointer==3.0.0 -jsonschema==4.23.0 -jsonschema-spec==0.2.4 -jsonschema-specifications==2023.7.1 kombu==5.4.2 langchain==0.3.20 langchain-community==0.3.19 @@ -41,14 +38,12 @@ lxml==5.3.1 markupsafe==3.0.2 marshmallow==3.26.1 mpmath==1.3.0 -multidict==6.3.2 +multidict==6.4.3 mypy-extensions==1.0.0 networkx==3.4.2 numpy==2.2.1 openai==1.66.3 -openapi-schema-validator==0.6.3 -openapi-spec-validator==0.6.0 -openapi3-parser==1.1.19 +openapi3-parser==1.1.21 orjson==3.10.14 packaging==24.1 pandas==2.2.3 From 9a430f73e220dcb6b157d2aa31c2c88eb71e8db7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 15:10:29 +0000 Subject: [PATCH 11/18] build(deps): bump packaging from 24.1 to 25.0 in /application Bumps [packaging](https://github.com/pypa/packaging) from 24.1 to 25.0. - [Release notes](https://github.com/pypa/packaging/releases) - [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/packaging/compare/24.1...25.0) --- updated-dependencies: - dependency-name: packaging dependency-version: '25.0' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index e42d2baf..26d7c549 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -45,7 +45,7 @@ numpy==2.2.1 openai==1.66.3 openapi3-parser==1.1.21 orjson==3.10.14 -packaging==24.1 +packaging==25.0 pandas==2.2.3 openpyxl==3.1.5 pathable==0.4.4 From 44e524e3c3bead5ff62fd61f6c2fac8b0acaeb99 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 13 May 2025 16:29:24 +0100 Subject: [PATCH 12/18] build(deps): update langchain and openai dependencies --- application/requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/requirements.txt b/application/requirements.txt index 26d7c549..2a6926dd 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -29,10 +29,10 @@ jsonpointer==3.0.0 kombu==5.4.2 langchain==0.3.20 langchain-community==0.3.19 -langchain-core==0.3.45 -langchain-openai==0.3.8 -langchain-text-splitters==0.3.6 -langsmith==0.3.19 +langchain-core==0.3.59 +langchain-openai==0.3.16 +langchain-text-splitters==0.3.8 +langsmith==0.3.42 lazy-object-proxy==1.10.0 lxml==5.3.1 markupsafe==3.0.2 @@ -42,10 +42,10 @@ multidict==6.4.3 mypy-extensions==1.0.0 networkx==3.4.2 numpy==2.2.1 -openai==1.66.3 +openai==1.78.1 openapi3-parser==1.1.21 orjson==3.10.14 -packaging==25.0 +packaging==24.2 pandas==2.2.3 openpyxl==3.1.5 pathable==0.4.4 From 23bfd4683c9a1957994921b2a7a6fc7c236a126e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 20:32:17 +0000 Subject: [PATCH 13/18] build(deps): bump flask from 3.1.0 to 3.1.1 in /application Bumps [flask](https://github.com/pallets/flask) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/3.1.0...3.1.1) --- updated-dependencies: - dependency-name: flask dependency-version: 3.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 2a6926dd..b503b896 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -11,7 +11,7 @@ elasticsearch==8.17.1 escodegen==1.0.11 esprima==4.0.1 esutils==1.0.1 -Flask==3.1.0 +Flask==3.1.1 faiss-cpu==1.9.0.post1 flask-restx==1.3.0 google-genai==1.3.0 From d1d28df8a1f3a7b321719245555b2d31e6ec7c0c Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Wed, 14 May 2025 09:26:36 +0530 Subject: [PATCH 14/18] fix: handle empty chunks and retriever values in agent creation and update --- application/api/user/routes.py | 31 +++++++++++++++++++++++++++++++ frontend/src/agents/NewAgent.tsx | 7 +------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/application/api/user/routes.py b/application/api/user/routes.py index cb2e7b0a..bdc01d87 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -1222,6 +1222,10 @@ class CreateAgent(Resource): "lastUsedAt": None, "key": key, } + if new_agent["chunks"] == "": + new_agent["chunks"] = "0" + if new_agent["source"] == "" and new_agent["retriever"] == "": + new_agent["retriever"] = "classic" resp = agents_collection.insert_one(new_agent) new_id = str(resp.inserted_id) @@ -1334,6 +1338,33 @@ class UpdateAgent(Resource): ) else: update_fields[field] = "" + elif field == "chunks": + chunks_value = data.get("chunks") + if chunks_value == "": + update_fields[field] = "0" + else: + try: + if int(chunks_value) < 0: + return make_response( + jsonify( + { + "success": False, + "message": "Chunks value must be a positive integer", + } + ), + 400, + ) + update_fields[field] = chunks_value + except ValueError: + return make_response( + jsonify( + { + "success": False, + "message": "Invalid chunks value provided", + } + ), + 400, + ) else: update_fields[field] = data[field] diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index 475571cf..464b49fe 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -102,12 +102,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { const isPublishable = () => { return ( - agent.name && - agent.description && - (agent.source || agent.retriever) && - agent.chunks && - agent.prompt_id && - agent.agent_type + agent.name && agent.description && agent.prompt_id && agent.agent_type ); }; From 63c69128413badbca10d2ff456a0bf06119f75c6 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 May 2025 21:45:30 +0100 Subject: [PATCH 15/18] lazy load elasticsearch --- application/requirements.txt | 3 --- application/vectorstore/elasticsearch.py | 9 ++------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/application/requirements.txt b/application/requirements.txt index b503b896..c51c2324 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -6,8 +6,6 @@ dataclasses-json==0.6.7 docx2txt==0.8 duckduckgo-search==7.5.2 ebooklib==0.18 -elastic-transport==8.17.0 -elasticsearch==8.17.1 escodegen==1.0.11 esprima==4.0.1 esutils==1.0.1 @@ -18,7 +16,6 @@ google-genai==1.3.0 google-generativeai==0.8.5 gTTS==2.5.4 gunicorn==23.0.0 -html2text==2024.2.26 javalang==0.13.0 jinja2==3.1.6 jiter==0.8.2 diff --git a/application/vectorstore/elasticsearch.py b/application/vectorstore/elasticsearch.py index e393e4a5..dfa0314f 100644 --- a/application/vectorstore/elasticsearch.py +++ b/application/vectorstore/elasticsearch.py @@ -1,9 +1,6 @@ from application.vectorstore.base import BaseVectorStore from application.core.settings import settings from application.vectorstore.document_class import Document -import elasticsearch - - class ElasticsearchStore(BaseVectorStore): @@ -26,8 +23,7 @@ class ElasticsearchStore(BaseVectorStore): else: raise ValueError("Please provide either elasticsearch_url or cloud_id.") - - + import elasticsearch ElasticsearchStore._es_connection = elasticsearch.Elasticsearch(**connection_params) self.docsearch = ElasticsearchStore._es_connection @@ -155,8 +151,6 @@ class ElasticsearchStore(BaseVectorStore): **kwargs, ): - from elasticsearch.helpers import BulkIndexError, bulk - bulk_kwargs = bulk_kwargs or {} import uuid embeddings = [] @@ -189,6 +183,7 @@ class ElasticsearchStore(BaseVectorStore): if len(requests) > 0: + from elasticsearch.helpers import BulkIndexError, bulk try: success, failed = bulk( self._es_connection, From 0d45c44c6f513528abc3d55a631a0ef641441531 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 20:47:44 +0000 Subject: [PATCH 16/18] build(deps): bump prompt-toolkit from 3.0.50 to 3.0.51 in /application Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.50 to 3.0.51. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/main/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.50...3.0.51) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-version: 3.0.51 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index c51c2324..09c94f0e 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -50,7 +50,7 @@ pillow==11.1.0 portalocker==3.1.1 prance==23.6.21.0 primp==0.14.0 -prompt-toolkit==3.0.50 +prompt-toolkit==3.0.51 protobuf==5.29.3 psycopg2-binary==2.9.10 py==1.11.0 From e7d54a639e07e3c1b158be26216e6372e0e3b186 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 20:50:26 +0000 Subject: [PATCH 17/18] build(deps): bump yarl from 1.18.3 to 1.20.0 in /application Bumps [yarl](https://github.com/aio-libs/yarl) from 1.18.3 to 1.20.0. - [Release notes](https://github.com/aio-libs/yarl/releases) - [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/yarl/compare/v1.18.3...v1.20.0) --- updated-dependencies: - dependency-name: yarl dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- application/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/requirements.txt b/application/requirements.txt index 09c94f0e..3c32ff60 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -81,7 +81,7 @@ urllib3==2.3.0 vine==5.1.0 wcwidth==0.2.13 werkzeug==3.1.3 -yarl==1.18.3 +yarl==1.20.0 markdownify==0.14.1 tldextract==5.1.3 websockets==14.1 From b2809b2e9a59121b3b54603ca05e77500555f2be Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 May 2025 22:03:27 +0100 Subject: [PATCH 18/18] remove old deps --- application/requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/application/requirements.txt b/application/requirements.txt index 3c32ff60..2430a787 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -13,7 +13,6 @@ Flask==3.1.1 faiss-cpu==1.9.0.post1 flask-restx==1.3.0 google-genai==1.3.0 -google-generativeai==0.8.5 gTTS==2.5.4 gunicorn==23.0.0 javalang==0.13.0 @@ -49,7 +48,6 @@ pathable==0.4.4 pillow==11.1.0 portalocker==3.1.1 prance==23.6.21.0 -primp==0.14.0 prompt-toolkit==3.0.51 protobuf==5.29.3 psycopg2-binary==2.9.10