From 4979e1ac9a70a1b86de280b04fd03d2115bb1782 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Mon, 28 Apr 2025 14:18:28 +0530 Subject: [PATCH] feat: add clsx dependency, enhance logging in agent logic, and improve agent logs component --- application/agents/base.py | 4 +- application/agents/classic_agent.py | 10 +- application/agents/react_agent.py | 4 + application/logging.py | 6 +- application/worker.py | 1 + frontend/package-lock.json | 17 ++ frontend/package.json | 1 + frontend/src/agents/AgentLogs.tsx | 53 +++++- frontend/src/agents/NewAgent.tsx | 8 +- frontend/src/agents/index.tsx | 4 + frontend/src/assets/monitoring-purple.svg | 3 + frontend/src/assets/monitoring-white.svg | 3 + frontend/src/components/CopyButton.tsx | 160 +++++++++++++----- .../src/conversation/ConversationBubble.tsx | 20 +-- frontend/src/modals/AgentDetailsModal.tsx | 37 ++-- frontend/src/settings/Analytics.tsx | 41 +---- frontend/src/settings/Logs.tsx | 3 +- 17 files changed, 254 insertions(+), 121 deletions(-) create mode 100644 frontend/src/assets/monitoring-purple.svg create mode 100644 frontend/src/assets/monitoring-white.svg diff --git a/application/agents/base.py b/application/agents/base.py index 64fac17b..b3797fc6 100644 --- a/application/agents/base.py +++ b/application/agents/base.py @@ -255,7 +255,7 @@ class BaseAgent(ABC): model=self.gpt_model, messages=messages, tools=self.tools ) if log_context: - data = build_stack_data(self.llm) + data = build_stack_data(self.llm, exclude_attributes=["client"]) log_context.stacks.append({"component": "llm", "data": data}) return resp @@ -271,6 +271,6 @@ class BaseAgent(ABC): self, resp, tools_dict, messages, attachments ) if log_context: - data = build_stack_data(self.llm_handler) + data = build_stack_data(self.llm_handler, exclude_attributes=["tool_calls"]) log_context.stacks.append({"component": "llm_handler", "data": data}) return resp diff --git a/application/agents/classic_agent.py b/application/agents/classic_agent.py index bf472cd0..b96a77fc 100644 --- a/application/agents/classic_agent.py +++ b/application/agents/classic_agent.py @@ -48,15 +48,13 @@ class ClassicAgent(BaseAgent): ): yield {"answer": resp.message.content} else: - # completion = self.llm.gen_stream( - # model=self.gpt_model, messages=messages, tools=self.tools - # ) - # log type of resp - logger.info(f"Response type: {type(resp)}") - logger.info(f"Response: {resp}") for line in resp: if isinstance(line, str): yield {"answer": line} + log_context.stacks.append( + {"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}} + ) + yield {"sources": retrieved_data} yield {"tool_calls": self.tool_calls.copy()} diff --git a/application/agents/react_agent.py b/application/agents/react_agent.py index 3fae1fda..a5d47850 100644 --- a/application/agents/react_agent.py +++ b/application/agents/react_agent.py @@ -82,6 +82,10 @@ class ReActAgent(BaseAgent): if isinstance(line, str): self.observations.append(line) + log_context.stacks.append( + {"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}} + ) + yield {"sources": retrieved_data} yield {"tool_calls": self.tool_calls.copy()} diff --git a/application/logging.py b/application/logging.py index 1dd0d557..eaf43d7c 100644 --- a/application/logging.py +++ b/application/logging.py @@ -29,6 +29,8 @@ def build_stack_data( exclude_attributes: List[str] = None, custom_data: Dict = None, ) -> Dict: + if obj is None: + raise ValueError("The 'obj' parameter cannot be None") data = {} if include_attributes is None: include_attributes = [] @@ -56,8 +58,8 @@ def build_stack_data( data[attr_name] = [str(item) for item in attr_value] elif isinstance(attr_value, dict): data[attr_name] = {k: str(v) for k, v in attr_value.items()} - else: - data[attr_name] = str(attr_value) + except AttributeError as e: + logging.warning(f"AttributeError while accessing {attr_name}: {e}") except AttributeError: pass if custom_data: diff --git a/application/worker.py b/application/worker.py index 4782a83b..537206b7 100755 --- a/application/worker.py +++ b/application/worker.py @@ -179,6 +179,7 @@ def run_agent_logic(agent_config, input_data): "tool_calls": tool_calls, "thought": thought, } + logging.info(f"Agent response: {result}") return result except Exception as e: logging.error(f"Error in run_agent_logic: {e}", exc_info=True) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 043bbf58..fa250e66 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@reduxjs/toolkit": "^2.5.1", "chart.js": "^4.4.4", + "clsx": "^2.1.1", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.2", "prop-types": "^15.8.1", @@ -2751,6 +2752,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", @@ -9405,6 +9415,13 @@ "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/package.json b/frontend/package.json index 45058e98..62afaad3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "dependencies": { "@reduxjs/toolkit": "^2.5.1", "chart.js": "^4.4.4", + "clsx": "^2.1.1", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.2", "prop-types": "^15.8.1", diff --git a/frontend/src/agents/AgentLogs.tsx b/frontend/src/agents/AgentLogs.tsx index 3773e54f..864a85fe 100644 --- a/frontend/src/agents/AgentLogs.tsx +++ b/frontend/src/agents/AgentLogs.tsx @@ -1,12 +1,40 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; +import userService from '../api/services/userService'; import ArrowLeft from '../assets/arrow-left.svg'; +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() { const navigate = useNavigate(); const { agentId } = useParams(); + const token = useSelector(selectToken); + + const [agent, setAgent] = useState(); + const [loadingAgent, setLoadingAgent] = useState(true); + + const fetchAgent = async (agentId: string) => { + setLoadingAgent(true); + try { + const response = await userService.getAgent(agentId ?? '', token); + if (!response.ok) throw new Error('Failed to fetch Chatbots'); + const agent = await response.json(); + setAgent(agent); + } catch (error) { + console.error(error); + } finally { + setLoadingAgent(false); + } + }; + + useEffect(() => { + if (agentId) fetchAgent(agentId); + }, [agentId, token]); return (
@@ -25,8 +53,29 @@ export default function AgentLogs() { Agent Logs
- - +
+

+ Agent Name +

+ {agent && ( +

{agent.name}

+ )} +
+ {loadingAgent ? ( +
+ +
+ ) : ( + agent && + )} + {loadingAgent ? ( +
+ {' '} + +
+ ) : ( + agent && + )}
); } diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index 3aa1bf7d..0cccfbe6 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -11,10 +11,7 @@ import AgentDetailsModal from '../modals/AgentDetailsModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, Prompt } from '../models/misc'; import { - selectSelectedAgent, - selectSourceDocs, - selectToken, - setSelectedAgent, + selectSelectedAgent, selectSourceDocs, selectToken, setSelectedAgent } from '../preferences/preferenceSlice'; import PromptsModal from '../preferences/PromptsModal'; import { UserToolType } from '../settings/types'; @@ -287,9 +284,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { )} {modeConfig[effectiveMode].showAccessDetails && ( )} diff --git a/frontend/src/agents/index.tsx b/frontend/src/agents/index.tsx index 0ceef669..c2edb34a 100644 --- a/frontend/src/agents/index.tsx +++ b/frontend/src/agents/index.tsx @@ -138,6 +138,7 @@ function AgentsList() { )) @@ -160,9 +161,11 @@ function AgentsList() { function AgentCard({ agent, + agents, setUserAgents, }: { agent: Agent; + agents: Agent[]; setUserAgents: React.Dispatch>; }) { const navigate = useNavigate(); @@ -225,6 +228,7 @@ function AgentCard({ setUserAgents((prevAgents) => prevAgents.filter((prevAgent) => prevAgent.id !== data.id), ); + dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id))); }; return (
+ + diff --git a/frontend/src/assets/monitoring-white.svg b/frontend/src/assets/monitoring-white.svg new file mode 100644 index 00000000..b015eeee --- /dev/null +++ b/frontend/src/assets/monitoring-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx index c430603f..0afbbe82 100644 --- a/frontend/src/components/CopyButton.tsx +++ b/frontend/src/components/CopyButton.tsx @@ -1,58 +1,136 @@ +import clsx from 'clsx'; import copy from 'copy-to-clipboard'; -import { useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import CheckMark from '../assets/checkmark.svg?react'; -import Copy from '../assets/copy.svg?react'; +import CopyIcon from '../assets/copy.svg?react'; + +type CopyButtonProps = { + textToCopy: string; + bgColorLight?: string; + bgColorDark?: string; + hoverBgColorLight?: string; + hoverBgColorDark?: string; + iconSize?: string; + padding?: string; + showText?: boolean; + copiedDuration?: number; + className?: string; + iconWrapperClassName?: string; + textClassName?: string; +}; + +const DEFAULT_ICON_SIZE = 'w-4 h-4'; +const DEFAULT_PADDING = 'p-2'; +const DEFAULT_COPIED_DURATION = 2000; +const DEFAULT_BG_LIGHT = '#FFFFFF'; +const DEFAULT_BG_DARK = 'transparent'; +const DEFAULT_HOVER_BG_LIGHT = '#EEEEEE'; +const DEFAULT_HOVER_BG_DARK = '#4A4A4A'; export default function CopyButton({ - text, - colorLight, - colorDark, + textToCopy, + bgColorLight = DEFAULT_BG_LIGHT, + bgColorDark = DEFAULT_BG_DARK, + hoverBgColorLight = DEFAULT_HOVER_BG_LIGHT, + hoverBgColorDark = DEFAULT_HOVER_BG_DARK, + iconSize = DEFAULT_ICON_SIZE, + padding = DEFAULT_PADDING, showText = false, -}: { - text: string; - colorLight?: string; - colorDark?: string; - showText?: boolean; -}) { + copiedDuration = DEFAULT_COPIED_DURATION, + className, + iconWrapperClassName, + textClassName, +}: CopyButtonProps) { const { t } = useTranslation(); - const [copied, setCopied] = useState(false); - const [isCopyHovered, setIsCopyHovered] = useState(false); + const [isCopied, setIsCopied] = useState(false); + const timeoutIdRef = useRef(null); - const handleCopyClick = (text: string) => { - copy(text); - setCopied(true); - setTimeout(() => { - setCopied(false); - }, 3000); - }; + const iconWrapperClasses = clsx( + 'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out', + padding, + `bg-[${bgColorLight}] dark:bg-[${bgColorDark}]`, + `hover:bg-[${hoverBgColorLight}] dark:hover:bg-[${hoverBgColorDark}]`, + { + 'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900': + isCopied, + }, + iconWrapperClassName, + ); + const rootButtonClasses = clsx( + 'flex items-center gap-2 group', + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 rounded-full', + className, + ); + + const textSpanClasses = clsx( + 'text-xs text-gray-600 dark:text-gray-400 transition-opacity duration-150 ease-in-out', + { 'opacity-75': isCopied }, + textClassName, + ); + + const IconComponent = isCopied ? CheckMark : CopyIcon; + const iconClasses = clsx(iconSize, { + 'stroke-green-600 dark:stroke-green-400': isCopied, + 'fill-none text-gray-700 dark:text-gray-300': !isCopied, + }); + + const buttonTitle = isCopied + ? t('conversation.copied') + : t('conversation.copy'); + const displayedText = isCopied + ? t('conversation.copied') + : t('conversation.copy'); + + const handleCopy = useCallback(() => { + if (isCopied) return; + + try { + const success = copy(textToCopy); + if (success) { + setIsCopied(true); + + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + } + + timeoutIdRef.current = setTimeout(() => { + setIsCopied(false); + timeoutIdRef.current = null; + }, copiedDuration); + } else { + console.warn('Copy command failed.'); + } + } catch (error) { + console.error('Failed to copy text:', error); + } + }, [textToCopy, copiedDuration, isCopied]); + + useEffect(() => { + return () => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + } + }; + }, []); return ( ); } diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index a241b2d3..a7c8467d 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -5,10 +5,7 @@ import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import { useSelector } from 'react-redux'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { - oneLight, - vscDarkPlus, -} from 'react-syntax-highlighter/dist/cjs/styles/prism'; +import { oneLight, vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import rehypeKatex from 'rehype-katex'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; @@ -29,10 +26,7 @@ import CopyButton from '../components/CopyButton'; import Sidebar from '../components/Sidebar'; import SpeakButton from '../components/TextToSpeechButton'; import { useDarkTheme, useOutsideAlerter } from '../hooks'; -import { - selectChunks, - selectSelectedDocs, -} from '../preferences/preferenceSlice'; +import { selectChunks, selectSelectedDocs } from '../preferences/preferenceSlice'; import classes from './ConversationBubble.module.css'; import { FEEDBACK, MESSAGE_TYPE } from './conversationModels'; import { ToolCallsType } from './types'; @@ -377,7 +371,7 @@ const ConversationBubble = forwardRef< {language}
- +
{' '}

@@ -689,7 +683,7 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) { Response {' '}

@@ -766,7 +760,7 @@ function Thought({ {language}

-

- Webhooks -

+
+

+ Webhook URL +

+ {webhookUrl && ( +
+ +
+ )} +
{webhookUrl ? ( -
- +
+

{webhookUrl} - - +

) : ( )}
diff --git a/frontend/src/settings/Analytics.tsx b/frontend/src/settings/Analytics.tsx index 04bec5c2..535200ef 100644 --- a/frontend/src/settings/Analytics.tsx +++ b/frontend/src/settings/Analytics.tsx @@ -1,11 +1,5 @@ import { - BarElement, - CategoryScale, - Chart as ChartJS, - Legend, - LinearScale, - Title, - Tooltip, + BarElement, CategoryScale, Chart as ChartJS, Legend, LinearScale, Title, Tooltip } from 'chart.js'; import { useEffect, useState } from 'react'; import { Bar } from 'react-chartjs-2'; @@ -71,7 +65,6 @@ export default function Analytics({ agentId }: AnalyticsProps) { string, { positive: number; negative: number } > | null>(null); - const [agent, setAgent] = useState(); const [messagesFilter, setMessagesFilter] = useState<{ label: string; value: string; @@ -97,21 +90,6 @@ export default function Analytics({ agentId }: AnalyticsProps) { const [loadingMessages, setLoadingMessages] = useLoaderState(true); const [loadingTokens, setLoadingTokens] = useLoaderState(true); const [loadingFeedback, setLoadingFeedback] = useLoaderState(true); - const [loadingAgent, setLoadingAgent] = useLoaderState(true); - - const fetchAgent = async (agentId: string) => { - setLoadingAgent(true); - try { - const response = await userService.getAgent(agentId ?? '', token); - if (!response.ok) throw new Error('Failed to fetch Chatbots'); - const agent = await response.json(); - setAgent(agent); - } catch (error) { - console.error(error); - } finally { - setLoadingAgent(false); - } - }; const fetchMessagesData = async (agent_id?: string, filter?: string) => { setLoadingMessages(true); @@ -174,27 +152,22 @@ export default function Analytics({ agentId }: AnalyticsProps) { }; useEffect(() => { - if (agentId) fetchAgent(agentId); - }, []); - - useEffect(() => { - const id = agent?.id; + const id = agentId; const filter = messagesFilter; fetchMessagesData(id, filter?.value); - }, [agent, messagesFilter]); + }, [agentId, messagesFilter]); useEffect(() => { - const id = agent?.id; + const id = agentId; const filter = tokenUsageFilter; fetchTokenData(id, filter?.value); - }, [agent, tokenUsageFilter]); + }, [agentId, tokenUsageFilter]); useEffect(() => { - const id = agent?.id; + const id = agentId; const filter = feedbackFilter; fetchFeedbackData(id, filter?.value); - }, [agent, feedbackFilter]); - + }, [agentId, feedbackFilter]); return (
{/* Messages Analytics */} diff --git a/frontend/src/settings/Logs.tsx b/frontend/src/settings/Logs.tsx index a14f5966..50d67b54 100644 --- a/frontend/src/settings/Logs.tsx +++ b/frontend/src/settings/Logs.tsx @@ -181,8 +181,7 @@ function Log({