From f5d8e0c9cf3a80613be8eff8ad7743be932d8413 Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Wed, 23 Apr 2025 14:11:52 +0100 Subject: [PATCH] feat(ui): Add summary details of each observable Signed-off-by: Richard Palethorpe --- core/agent/agent.go | 6 + core/types/observable.go | 1 + .../src/components/CollapsibleRawSections.jsx | 103 +++++++ webui/react-ui/src/pages/AgentStatus.jsx | 272 +++++++++++++----- 4 files changed, 316 insertions(+), 66 deletions(-) create mode 100644 webui/react-ui/src/components/CollapsibleRawSections.jsx diff --git a/core/agent/agent.go b/core/agent/agent.go index 1aa8fdf..3954651 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -180,6 +180,12 @@ func (a *Agent) Execute(j *types.Job) *types.JobResult { }() if j.Obs != nil { + if len(j.ConversationHistory) > 0 { + m := j.ConversationHistory[len(j.ConversationHistory)-1] + j.Obs.Creation = &types.Creation{ ChatCompletionMessage: &m } + a.observer.Update(*j.Obs) + } + j.Result.AddFinalizer(func(ccm []openai.ChatCompletionMessage) { j.Obs.Completion = &types.Completion{ Conversation: ccm, diff --git a/core/types/observable.go b/core/types/observable.go index 74dd348..48844e9 100644 --- a/core/types/observable.go +++ b/core/types/observable.go @@ -6,6 +6,7 @@ import ( ) type Creation struct { + ChatCompletionMessage *openai.ChatCompletionMessage `json:"chat_completion_message,omitempty"` ChatCompletionRequest *openai.ChatCompletionRequest `json:"chat_completion_request,omitempty"` FunctionDefinition *openai.FunctionDefinition `json:"function_definition,omitempty"` FunctionParams ActionParams `json:"function_params,omitempty"` diff --git a/webui/react-ui/src/components/CollapsibleRawSections.jsx b/webui/react-ui/src/components/CollapsibleRawSections.jsx new file mode 100644 index 0000000..559c9cb --- /dev/null +++ b/webui/react-ui/src/components/CollapsibleRawSections.jsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +import hljs from 'highlight.js/lib/core'; +import json from 'highlight.js/lib/languages/json'; +import 'highlight.js/styles/monokai.css'; + +hljs.registerLanguage('json', json); + +export default function CollapsibleRawSections({ container }) { + const [showCreation, setShowCreation] = useState(false); + const [showProgress, setShowProgress] = useState(false); + const [showCompletion, setShowCompletion] = useState(false); + const [copied, setCopied] = useState({ creation: false, progress: false, completion: false }); + + const handleCopy = (section, data) => { + navigator.clipboard.writeText(JSON.stringify(data, null, 2)); + setCopied(prev => ({ ...prev, [section]: true })); + setTimeout(() => setCopied(prev => ({ ...prev, [section]: false })), 1200); + }; + + return ( +
+ {/* Creation Section */} + {container.creation && ( +
+

+ setShowCreation(v => !v)} + > + + Creation + + +

+ {showCreation && ( +

+              
+
+ )} +
+ )} + {/* Progress Section */} + {container.progress && container.progress.length > 0 && ( +
+

+ setShowProgress(v => !v)} + > + + Progress + + +

+ {showProgress && ( +

+              
+
+ )} +
+ )} + {/* Completion Section */} + {container.completion && ( +
+

+ setShowCompletion(v => !v)} + > + + Completion + + +

+ {showCompletion && ( +

+              
+
+ )} +
+ )} +
+ ); +} + diff --git a/webui/react-ui/src/pages/AgentStatus.jsx b/webui/react-ui/src/pages/AgentStatus.jsx index ae787b6..9d98ae6 100644 --- a/webui/react-ui/src/pages/AgentStatus.jsx +++ b/webui/react-ui/src/pages/AgentStatus.jsx @@ -1,4 +1,181 @@ import { useState, useEffect } from 'react'; +import CollapsibleRawSections from '../components/CollapsibleRawSections'; + +function ObservableSummary({ observable }) { + // --- CREATION SUMMARIES --- + const creation = observable?.creation || {}; + // ChatCompletionRequest summary + let creationChatMsg = ''; + // Prefer chat_completion_message if present (for jobs/top-level containers) + if (creation?.chat_completion_message && creation.chat_completion_message.content) { + creationChatMsg = creation.chat_completion_message.content; + } else { + const messages = creation?.chat_completion_request?.messages; + if (Array.isArray(messages) && messages.length > 0) { + const lastMsg = messages[messages.length - 1]; + creationChatMsg = lastMsg?.content || ''; + } + } + // FunctionDefinition summary + let creationFunctionDef = ''; + if (creation?.function_definition?.name) { + creationFunctionDef = `Function: ${creation.function_definition.name}`; + } + // FunctionParams summary + let creationFunctionParams = ''; + if (creation?.function_params && Object.keys(creation.function_params).length > 0) { + creationFunctionParams = `Params: ${JSON.stringify(creation.function_params)}`; + } + + // --- COMPLETION SUMMARIES --- + const completion = observable?.completion || {}; + // ChatCompletionResponse summary + let completionChatMsg = ''; + const chatCompletion = completion?.chat_completion_response; + if ( + chatCompletion && + Array.isArray(chatCompletion.choices) && + chatCompletion.choices.length > 0 + ) { + const lastChoice = chatCompletion.choices[chatCompletion.choices.length - 1]; + // Prefer tool_call summary if present + let toolCallSummary = ''; + const toolCalls = lastChoice?.message?.tool_calls; + if (Array.isArray(toolCalls) && toolCalls.length > 0) { + toolCallSummary = toolCalls.map(tc => { + let args = ''; + // For OpenAI-style, arguments are in tc.function.arguments, function name in tc.function.name + if (tc.function && tc.function.arguments) { + try { + args = typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments); + } catch (e) { + args = '[Unserializable arguments]'; + } + } + const toolName = tc.function?.name || tc.name || 'unknown'; + return `Tool call: ${toolName}(${args})`; + }).join('\n'); + } + completionChatMsg = lastChoice?.message?.content || ''; + // Attach toolCallSummary to completionChatMsg for rendering + if (toolCallSummary) { + completionChatMsg = { toolCallSummary, message: completionChatMsg }; + } + // Else, it's just a string + + } + // Conversation summary + let completionConversation = ''; + if (Array.isArray(completion?.conversation) && completion.conversation.length > 0) { + const lastConv = completion.conversation[completion.conversation.length - 1]; + completionConversation = lastConv?.content ? `${lastConv.content}` : ''; + } + // ActionResult summary + let completionActionResult = ''; + if (completion?.action_result) { + completionActionResult = `Action Result: ${String(completion.action_result).slice(0, 100)}`; + } + // AgentState summary + let completionAgentState = ''; + if (completion?.agent_state) { + completionAgentState = `Agent State: ${JSON.stringify(completion.agent_state)}`; + } + // Error summary + let completionError = ''; + if (completion?.error) { + completionError = `Error: ${completion.error}`; + } + + // Only show if any summary is present + if (!creationChatMsg && !creationFunctionDef && !creationFunctionParams && + !completionChatMsg && !completionConversation && !completionActionResult && !completionAgentState && !completionError) { + return null; + } + + return ( +
+ {/* CREATION */} + {creationChatMsg && ( +
+ + {creationChatMsg} +
+ )} + {creationFunctionDef && ( +
+ + {creationFunctionDef} +
+ )} + {creationFunctionParams && ( +
+ + {creationFunctionParams} +
+ )} + {/* COMPLETION */} + {/* COMPLETION: Tool call summary if present */} + {completionChatMsg && typeof completionChatMsg === 'object' && completionChatMsg.toolCallSummary && ( +
+ + {completionChatMsg.toolCallSummary} +
+ )} + {/* COMPLETION: Message content if present */} + {completionChatMsg && ((typeof completionChatMsg === 'object' && completionChatMsg.message) || typeof completionChatMsg === 'string') && ( +
+ + {typeof completionChatMsg === 'object' ? completionChatMsg.message : completionChatMsg} +
+ )} + {completionConversation && ( +
+ + {completionConversation} +
+ )} + {completionActionResult && ( +
+ + {completionActionResult} +
+ )} + {completionAgentState && ( +
+ + {completionAgentState} +
+ )} + {completionError && ( +
+ + {completionError} +
+ )} +
+ ); +} + import { useParams, Link } from 'react-router-dom'; import hljs from 'highlight.js/lib/core'; import json from 'highlight.js/lib/languages/json'; @@ -7,7 +184,7 @@ import 'highlight.js/styles/monokai.css'; hljs.registerLanguage('json', json); function AgentStatus() { - const [showStatus, setShowStatus] = useState(true); + const [showStatus, setShowStatus] = useState(false); const { name } = useParams(); const [statusData, setStatusData] = useState(null); const [loading, setLoading] = useState(true); @@ -94,17 +271,16 @@ function AgentStatus() { setObservableMap(prevMap => { const prev = prevMap[data.id] || {}; const updated = { - ...prev, ...data, - creation: data.creation, - progress: data.progress, - completion: data.completion, + ...prev, }; // Events can be received out of order if (data.creation) updated.creation = data.creation; if (data.completion) updated.completion = data.completion; + if ((data.progress?.length ?? 0) > (prev.progress?.length ?? 0)) + updated.progress = data.progress; if (data.parent_id && !prevMap[data.parent_id]) prevMap[data.parent_id] = { id: data.parent_id, @@ -252,12 +428,17 @@ function AgentStatus() { setExpandedCards(new Map(expandedCards).set(container.id, newExpanded)); }} > -
- - - {container.name}#{container.id} - -
+
+ + +
+ + {container.name}#{container.id} + + +
+
+
-
{ const newExpanded = !expandedCards.get(childKey); setExpandedCards(new Map(expandedCards).set(childKey, newExpanded)); }} > -
- - - {child.name}#{child.id} - -
+
+ + +
+ + {child.name}#{child.id} + + +
+
+
- {child.creation && ( -
-
Creation:
-

-                                          
-
-
- )} - {child.progress && child.progress.length > 0 && ( -
-
Progress:
-

-                                          
-
-
- )} - {child.completion && ( -
-
Completion:
-

-                                          
-
-
- )} +
); })}
)} - {container.creation && ( -
-

Creation:

-

-                              
-
-
- )} - {container.progress && container.progress.length > 0 && ( -
-

Progress:

-

-                              
-
-
- )} - {container.completion && ( -
-

Completion:

-

-                              
-
-
- )} +