Hierarchical Planner Agent with Custom UI (useChat)
This scenario demonstrates a more advanced AgentB setup:
Backend: An AgentB server configured in
hierarchicalPlanner
mode. The primary agent is aPlanningAgent
that uses theDelegateToSpecialistTool
to delegate sub-tasks to "specialist" agents (toolsets).Frontend: A React application with a custom chat UI built using the
useChat
hook from@ulifeai/agentb-ui
. This allows for fine-grained rendering of the complex event flow from a planning agent.
Goal: Illustrate how to handle the events from a planning agent in a custom UI, showing delegation steps and specialist results.
Part 1: Backend Setup (Conceptual Overview)
For this scenario, your AgentB backend server (e.g., server.ts
) would be initialized to support a planning agent.
Key Backend Configuration (server.ts
- simplified):
// server.ts (Simplified for this scenario)
import { AgentB, ToolProviderSourceConfig, ApiInteractionManagerOptions } from '@ulifeai/agentb';
// ... other necessary imports (Express, CORS, etc.)
async function startAdvancedServer() {
// 1. Define ToolProviderSourceConfigs for "Specialists"
const weatherSpecialistConfig: ToolProviderSourceConfig = {
id: 'weatherServiceProvider',
type: 'openapi',
openapiConnectorOptions: {
sourceId: 'weatherServiceProvider', // Matches id
// Assuming a spec that provides a 'getWeather(location: string)' tool
specUrl: 'YOUR_WEATHER_API_SPEC_URL_OR_LOCAL_PATH',
// authentication: { ... if needed ... }
},
toolsetCreationStrategy: 'allInOne', // One toolset for all weather tools
allInOneToolsetName: 'WeatherSpecialistTools',
allInOneToolsetDescription: 'Provides tools to get weather forecasts.'
};
const calendarSpecialistConfig: ToolProviderSourceConfig = {
id: 'calendarServiceProvider',
type: 'openapi',
openapiConnectorOptions: {
sourceId: 'calendarServiceProvider',
// Assuming a spec that provides 'createCalendarEvent(title: string, dateTime: string, location?: string)'
specUrl: 'YOUR_CALENDAR_API_SPEC_URL_OR_LOCAL_PATH',
},
toolsetCreationStrategy: 'allInOne',
allInOneToolsetName: 'CalendarSpecialistTools',
allInOneToolsetDescription: 'Provides tools to manage calendar events.'
};
// 2. Initialize AgentB
// The AgentB facade, when given multiple tool providers, will typically
// default to a mode conducive to planning (like 'hierarchicalPlanner').
AgentB.initialize({
llmProvider: { provider: 'openai', model: 'gpt-4o-mini' }, // Or a more capable model for planning
toolProviders: [weatherSpecialistConfig, calendarSpecialistConfig],
defaultAgentRunConfig: {
// The PlanningAgent will use a prompt like DEFAULT_PLANNER_SYSTEM_PROMPT
// which lists 'WeatherSpecialistTools' and 'CalendarSpecialistTools' as available specialists.
model: 'gpt-4o' // Planners often benefit from more capable models
}
});
// 3. Setup Express App and SSE Endpoint (as in previous tutorials)
const app = express();
// ... app.use(express.json()), app.use(cors()) ...
app.post('/advanced/stream', AgentB.getExpressStreamingHttpHandler({ /* ... options ... */ }));
// ... app.listen() ...
console.log("🚀 Advanced AgentB Server running with Planner configuration.");
}
startAdvancedServer();
Explanation of Backend Setup:
We register two tool providers (Weather and Calendar), each intended to become a "specialist" toolset.
AgentB.initialize
with these providers will lead the internalApiInteractionManager
to likely operate inhierarchicalPlanner
mode.The primary agent will be a
PlanningAgent
. Its main tool will bedelegateToSpecialistAgent
.The system prompt for the
PlanningAgent
will list "WeatherSpecialistTools" and "CalendarSpecialistTools" (IDs derived fromToolProviderSourceConfig.id
orallInOneToolsetName
) as available specialists it can delegate to.
Part 2: Frontend Custom UI with useChat
useChat
Now, let's build a React component that uses useChat
and renders the specific events from our planning agent.
src/PlannerChatInterface.tsx
:
import React, { useState, useEffect, useRef } from 'react';
import { useChat, ChatMessage, AgentEvent, UILLMToolCall } from '@ulifeai/agentb-ui'; // Ensure types are imported
// Helper to render tool call details nicely
const renderToolCall = (toolCall: UILLMToolCall) => (
<div style={{ marginLeft: '10px', borderLeft: '2px solid #eee', paddingLeft: '10px', fontSize: '0.9em' }}>
<strong>Tool:</strong> {toolCall.function.name} <br />
<strong>Args:</strong> <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', backgroundColor: '#f9f9f9', padding: '5px', borderRadius: '4px' }}>{toolCall.function.arguments}</pre>
</div>
);
function PlannerChatInterface() {
const {
messages,
sendMessage,
isLoading,
isStreaming,
error,
threadId,
} = useChat({
backendUrl: 'http://localhost:3001/advanced/stream', // Ensure this matches your advanced server endpoint
});
const [inputText, setInputText] = useState('');
const messagesEndRef = useRef<null | HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!inputText.trim()) return;
sendMessage(inputText);
setInputText('');
};
const renderChatMessage = (msg: ChatMessage) => {
let specialContent = null;
const originalEvent = msg.metadata?.originalEvent as AgentEvent | undefined; // Cast for type safety
// More detailed rendering based on event type or sender
if (msg.sender === 'tool_thought' && originalEvent?.type === 'thread.run.step.tool_call.created') {
const toolCall = (originalEvent.data as any).toolCall as UILLMToolCall;
specialContent = (
<>
{msg.text}
{renderToolCall(toolCall)}
</>
);
} else if (msg.sender === 'tool_result' && originalEvent?.type === 'agent.tool.execution.completed') {
const resultData = (originalEvent.data as any).result;
specialContent = (
<>
{msg.text} (Success: {resultData.success.toString()})
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', backgroundColor: '#f9f9f9', padding: '5px', borderRadius: '4px', fontSize: '0.9em', maxHeight: '150px', overflowY: 'auto' }}>
{resultData.success ? JSON.stringify(resultData.data, null, 2) : resultData.error}
</pre>
</>
);
} else if (msg.sender === 'tool_result' && originalEvent?.type === 'agent.sub_agent.invocation.completed') {
// Handle display for sub_agent.invocation.completed specifically
const subAgentEventData = originalEvent.data as any;
const toolDisplayName = subAgentEventData.result.metadata?.delegatedToolName || `Specialist: ${subAgentEventData.specialistId}`;
specialContent = (
<>
<strong>{toolDisplayName} (via delegate) {subAgentEventData.result.success ? 'completed' : 'failed'}.</strong> <br/>
Run ID: {subAgentEventData.subAgentRunId} <br/>
Result:
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', backgroundColor: '#f9f9f9', padding: '5px', borderRadius: '4px', fontSize: '0.9em', maxHeight: '150px', overflowY: 'auto' }}>
{subAgentEventData.result.success ? JSON.stringify(subAgentEventData.result.data, null, 2) : subAgentEventData.result.error}
</pre>
</>
);
}
return (
<div key={msg.id} style={{ marginBottom: '15px', padding: '8px', borderRadius: '5px', backgroundColor: msg.sender === 'user' ? '#e1f5fe' : '#f1f1f1' }}>
<strong style={{ textTransform: 'capitalize' }}>{msg.sender.replace('_', ' ')}</strong>
{msg.metadata?.toolName && msg.sender !== 'tool_result' && ` (${msg.metadata.toolName})`}:
<div style={{ marginTop: '5px' }}>
{specialContent || msg.text}
</div>
<small style={{ display: 'block', textAlign: 'right', color: '#777', fontSize: '0.75em' }}>
{new Date(msg.timestamp).toLocaleTimeString()}
{msg.status === 'streaming' && ' (streaming...)'}
{msg.status === 'failed' && <span style={{color: 'red'}}> (failed to send)</span>}
</small>
</div>
);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 40px)', maxWidth: '800px', margin: '20px auto', border: '1px solid #ccc', boxShadow: '0 2px 10px rgba(0,0,0,0.1)' }}>
<h2 style={{ textAlign: 'center', padding: '10px 0', borderBottom: '1px solid #eee', margin: 0 }}>Planner Agent Chat</h2>
<div style={{ flexGrow: 1, overflowY: 'auto', padding: '15px' }}>
{messages.map(renderChatMessage)}
<div ref={messagesEndRef} />
</div>
{(isLoading || isStreaming) && <div style={{padding: '0 15px 5px', fontStyle: 'italic', color: '#555'}}>{isLoading ? "Planner is thinking..." : "Planner is responding..."}</div>}
{error && <div style={{ padding: '10px 15px', color: 'red', backgroundColor: '#ffebee' }}>Error: {error}</div>}
<form onSubmit={handleSubmit} style={{ display: 'flex', padding: '15px', borderTop: '1px solid #eee' }}>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Ask the planner (e.g., 'What's the weather in London and add it to my calendar for tomorrow?')"
style={{ flexGrow: 1, padding: '12px', marginRight: '10px', borderRadius: '5px', border: '1px solid #ddd' }}
disabled={isLoading || isStreaming}
/>
<button type="submit" disabled={isLoading || isStreaming} style={{ padding: '12px 20px', borderRadius: '5px', border: 'none', backgroundColor: '#007bff', color: 'white', cursor: 'pointer' }}>
Send
</button>
</form>
{threadId && <p style={{fontSize: '0.8em', textAlign: 'center', color: '#999', paddingBottom: '5px'}}>Thread: {threadId}</p>}
</div>
);
}
export default PlannerChatInterface;
To use this in your React app (e.g. App.tsx
):
import React from 'react';
import PlannerChatInterface from './PlannerChatInterface'; // Adjust path
function App() {
return (
<PlannerChatInterface />
);
}
export default App;
Running the Advanced Scenario
Start your Backend: Ensure your
server.ts
(configured for hierarchical planning with Weather and Calendar specialists as outlined in Part 1) is running.Start your Frontend: Run your React development server (
npm start
oryarn start
).Interact: Open your browser to the React app. Try a prompt like:
"What's the weather like in London today, and can you create a calendar event for me tomorrow at 10 AM titled 'Follow up on weather report'?"
Expected UI Behavior and Event Handling
When you send the complex prompt, your custom UI (thanks to the renderChatMessage
function and useChat
's event processing) should display something like this sequence:
You: "What's the weather..."
Planner is thinking...
Agent (tool_thought): Planner intends to use
delegateToSpecialistAgent
.Tool:
delegateToSpecialistAgent
Args:
{"specialistId": "WeatherSpecialistTools", "subTaskDescription": "Get current weather for London"}
Agent (tool_executing): Executing
delegateToSpecialistAgent
(Weather).(Internally, the
DelegateToSpecialistTool
is now running a worker agent. TheuseChat
hook in the UI doesn't see the worker's internal events by default, but it gets the final result of thedelegateToSpecialistAgent
tool call.)
Agent (tool_result):
DelegateToSpecialistAgent
(Weather) completed.Success: true
Data:
"The weather in London is 15°C and cloudy."
(This is the output from the Weather specialist worker)
Planner is responding... (The planner LLM is now processing the weather result)
Agent (tool_thought): Planner intends to use
delegateToSpecialistAgent
.Tool:
delegateToSpecialistAgent
Args:
{"specialistId": "CalendarSpecialistTools", "subTaskDescription": "Create event: 'Follow up on weather report' tomorrow 10 AM"}
Agent (tool_executing): Executing
delegateToSpecialistAgent
(Calendar).Agent (tool_result):
DelegateToSpecialistAgent
(Calendar) completed.Success: true
Data:
"Event 'Follow up on weather report' created successfully for tomorrow at 10 AM."
Planner is responding...
Agent (ai): "The weather in London is currently 15°C and cloudy. I've also added an event 'Follow up on weather report' to your calendar for tomorrow at 10 AM."
Key aspects demonstrated in PlannerChatInterface.tsx
:
useChat
: Manages the overall flow.renderChatMessage
: Custom function to inspectmsg.sender
andmsg.metadata.originalEvent
to provide richer display for different event types.Displaying Tool Calls: When the planner decides to use
delegateToSpecialistAgent
, the UI shows the intended specialist and sub-task.Displaying Tool Results: The UI shows the outcome from the
delegateToSpecialistAgent
tool, which includes the information retrieved or action performed by the specialist worker agent. Theagent.sub_agent.invocation.completed
event is key here anduseChat
maps this to atool_result
sender type.
Customization and Further Steps
Detailed Sub-Agent View: For even more insight, you could modify
DelegateToSpecialistTool
to stream the internal events of its worker agent back to the planner agent (perhaps as part of its own streaming output or via a side channel). The planner could then emit custom events that the UI could pick up to show a nested view of the specialist's activity. This is significantly more complex.Error Handling: Enhance
renderChatMessage
to display errors from tool executions or sub-agent invocations more prominently.Styling: Apply more sophisticated CSS or a UI library for a polished look.
Loading/Streaming Indicators: The example has basic indicators; you can make these more elaborate.
This advanced scenario showcases the power of combining AgentB's hierarchical planning backend with a custom UI built using useChat
, allowing you to visualize and interact with complex, multi-step agent behaviors.
Last updated