Update JSAPI documentation and examples for enhanced clarity and usage guidance
- Revised the JSAPI_OUTPUT.md to provide a comprehensive overview of Agent Hook functions and their usage. - Added detailed examples for each hook function, demonstrating real-time messaging and error handling. - Improved the structure and clarity of the documentation, ensuring users can easily understand how to implement and utilize the Context object methods. - Removed deprecated sections and streamlined the content for better readability.
This commit is contained in:
parent
673e36a7c9
commit
afab9c2173
3 changed files with 1076 additions and 177 deletions
|
|
@ -1,58 +1,120 @@
|
|||
# Context Output JS API
|
||||
|
||||
The Context object now provides `Send`, `SendGroup`, and `Flush` methods directly for sending messages to clients from JavaScript.
|
||||
The Context object provides `Send`, `SendGroup`, and `Flush` methods for sending messages to clients from JavaScript within Agent Hook functions.
|
||||
|
||||
## Usage
|
||||
## Hook Functions Overview
|
||||
|
||||
Agent Hook functions are lifecycle callbacks that allow you to customize the behavior of AI assistants. The Context object passed to these hooks includes output methods for real-time communication with clients.
|
||||
|
||||
### Available Hooks
|
||||
|
||||
- `Create(ctx, messages)` - Called before the assistant processes messages
|
||||
- `Before(ctx, messages, response)` - Called before sending LLM response
|
||||
- `After(ctx, messages, response)` - Called after receiving LLM response
|
||||
- `Done(ctx, messages, response)` - Called after assistant completes
|
||||
- `Error(ctx, messages, error)` - Called when an error occurs
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage in Create Hook
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Create hook - send initial messages to client
|
||||
*/
|
||||
function Create(ctx, messages) {
|
||||
// Send welcome message (string shorthand)
|
||||
ctx.Send("Welcome! Let me help you with that...");
|
||||
ctx.Flush();
|
||||
|
||||
// Send loading indicator
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: { message: "Analyzing your request..." },
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// Continue with normal processing
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Updates Example
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Create hook - demonstrate streaming updates
|
||||
*/
|
||||
function Create(ctx, messages) {
|
||||
// Send initial message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "Processing" },
|
||||
id: "status_msg",
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
time.Sleep(500); // Simulate work
|
||||
|
||||
// Append to message (delta update)
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "..." },
|
||||
id: "status_msg",
|
||||
delta: true,
|
||||
delta_path: "content",
|
||||
delta_action: "append",
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
time.Sleep(500); // More work
|
||||
|
||||
// Complete the message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: " Done!" },
|
||||
id: "status_msg",
|
||||
delta: true,
|
||||
delta_path: "content",
|
||||
delta_action: "append",
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### ctx.Send(message)
|
||||
|
||||
Send a single message to the client.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `message`: Can be a string (shorthand) or an object
|
||||
|
||||
**String Shorthand:**
|
||||
|
||||
```javascript
|
||||
// Automatically converts to a text message
|
||||
ctx.Send("Hello World");
|
||||
```
|
||||
|
||||
**Object Format:**
|
||||
|
||||
```javascript
|
||||
// Send text message
|
||||
// Text message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: {
|
||||
content: "Hello from JavaScript",
|
||||
},
|
||||
props: { content: "Hello from JavaScript" },
|
||||
});
|
||||
|
||||
// Send loading message
|
||||
// Loading indicator
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: {
|
||||
message: "Processing...",
|
||||
},
|
||||
props: { message: "Processing..." },
|
||||
});
|
||||
|
||||
// Send error message
|
||||
// Error message
|
||||
ctx.Send({
|
||||
type: "error",
|
||||
props: {
|
||||
message: "Something went wrong",
|
||||
code: "ERR_500",
|
||||
},
|
||||
});
|
||||
|
||||
// Send custom message
|
||||
ctx.Send({
|
||||
type: "custom_widget",
|
||||
props: {
|
||||
data: { foo: "bar" },
|
||||
},
|
||||
props: { message: "Something went wrong", code: "ERR_500" },
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -61,17 +123,15 @@ ctx.Send({
|
|||
```javascript
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: {
|
||||
content: "Hello",
|
||||
},
|
||||
id: "msg_123", // Optional: message ID
|
||||
delta: true, // Optional: incremental update
|
||||
done: false, // Optional: whether complete
|
||||
props: { content: "Hello" },
|
||||
id: "msg_123", // Optional: message ID for delta updates
|
||||
delta: true, // Optional: incremental update flag
|
||||
done: false, // Optional: completion flag
|
||||
delta_path: "content", // Optional: update path
|
||||
delta_action: "append", // Optional: update action (append, replace, merge, set)
|
||||
delta_action: "append", // Optional: append, replace, merge, set
|
||||
group_id: "grp_1", // Optional: message group ID
|
||||
metadata: {
|
||||
// Optional: metadata
|
||||
// Optional: custom metadata
|
||||
timestamp: Date.now(),
|
||||
sequence: 1,
|
||||
trace_id: "trace_123",
|
||||
|
|
@ -81,184 +141,310 @@ ctx.Send({
|
|||
|
||||
### ctx.SendGroup(group)
|
||||
|
||||
Send a group of messages to the client.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `group`: Message group object
|
||||
|
||||
**Example:**
|
||||
Send a group of related messages together.
|
||||
|
||||
```javascript
|
||||
ctx.SendGroup({
|
||||
id: "group_123",
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "First message" },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "Second message" },
|
||||
},
|
||||
{ type: "text", props: { content: "First message" } },
|
||||
{ type: "text", props: { content: "Second message" } },
|
||||
],
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
metadata: { timestamp: Date.now() },
|
||||
});
|
||||
```
|
||||
|
||||
### ctx.Flush()
|
||||
|
||||
Flush the output buffer to ensure all messages are sent to the client.
|
||||
|
||||
**Example:**
|
||||
Flush the output buffer to ensure messages are sent immediately.
|
||||
|
||||
```javascript
|
||||
ctx.Send("Processing...");
|
||||
ctx.Flush(); // Send immediately
|
||||
ctx.Flush(); // Send immediately to client
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
## Complete Hook Examples
|
||||
|
||||
### Using in Hook Functions
|
||||
### 1. Create Hook - Welcome Message
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Create hook - called before assistant processes
|
||||
* Send welcome message when conversation starts
|
||||
*/
|
||||
function Create(input, options) {
|
||||
const ctx = input.context;
|
||||
|
||||
function Create(ctx, messages) {
|
||||
// Send welcome message
|
||||
ctx.Send("Welcome to AI Assistant!");
|
||||
ctx.Send("Welcome to AI Assistant! How can I help you today?");
|
||||
ctx.Flush();
|
||||
|
||||
// Send loading indicator
|
||||
// Return messages to continue processing
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create Hook - Progress Updates
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Show progress indicators during preprocessing
|
||||
*/
|
||||
function Create(ctx, messages) {
|
||||
// Step 1: Analyzing
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: {
|
||||
message: "Thinking...",
|
||||
},
|
||||
props: { message: "Analyzing your request..." },
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
return { messages: input.messages };
|
||||
// Perform analysis...
|
||||
const userIntent = analyzeIntent(messages);
|
||||
|
||||
// Step 2: Searching
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: { message: "Searching knowledge base..." },
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// Search knowledge base...
|
||||
const context = searchKnowledgeBase(userIntent);
|
||||
|
||||
// Add context to messages
|
||||
if (context) {
|
||||
messages.unshift({
|
||||
role: "system",
|
||||
content: `Context: ${context}`,
|
||||
});
|
||||
}
|
||||
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Before Hook - Show Thinking Process
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Done hook - called after assistant completes
|
||||
* Display model's reasoning before sending response
|
||||
*/
|
||||
function Done(input, output) {
|
||||
const ctx = input.context;
|
||||
function Before(ctx, messages, response) {
|
||||
// If response includes thinking/reasoning
|
||||
if (response.thinking) {
|
||||
ctx.Send({
|
||||
type: "thinking",
|
||||
props: { content: response.thinking },
|
||||
});
|
||||
ctx.Flush();
|
||||
}
|
||||
|
||||
// Send completion message
|
||||
return { response };
|
||||
}
|
||||
```
|
||||
|
||||
### 4. After Hook - Process Tool Calls
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Handle tool calls and send results
|
||||
*/
|
||||
function After(ctx, messages, response) {
|
||||
// Process tool calls
|
||||
if (response.tool_calls && response.tool_calls.length > 0) {
|
||||
response.tool_calls.forEach((toolCall) => {
|
||||
// Show tool being called
|
||||
ctx.Send({
|
||||
type: "tool_call",
|
||||
props: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
arguments: toolCall.function.arguments,
|
||||
},
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// Execute tool and send result
|
||||
const result = executeTool(toolCall);
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: `Tool result: ${result}` },
|
||||
});
|
||||
ctx.Flush();
|
||||
});
|
||||
}
|
||||
|
||||
return { response };
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Done Hook - Completion Message
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Send completion message and cleanup
|
||||
*/
|
||||
function Done(ctx, messages, response) {
|
||||
// Send completion indicator
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: {
|
||||
content: "Processing completed!",
|
||||
},
|
||||
props: { content: "\n✅ Task completed successfully!" },
|
||||
});
|
||||
|
||||
// Flush output
|
||||
ctx.Flush();
|
||||
|
||||
// Log metrics
|
||||
console.log("Conversation completed:", {
|
||||
chat_id: ctx.chat_id,
|
||||
message_count: messages.length,
|
||||
tokens_used: response.usage?.total_tokens,
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Response Example
|
||||
### 6. Error Hook - Handle Errors Gracefully
|
||||
|
||||
```javascript
|
||||
function StreamingResponse(input) {
|
||||
const ctx = input.context;
|
||||
/**
|
||||
* Send user-friendly error messages
|
||||
*/
|
||||
function Error(ctx, messages, error) {
|
||||
console.error("Assistant error:", error);
|
||||
|
||||
// Send initial message
|
||||
// Send error message to user
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "Starting process" },
|
||||
id: "msg_1",
|
||||
delta: false,
|
||||
type: "error",
|
||||
props: {
|
||||
message: "I encountered an issue while processing your request.",
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
details:
|
||||
process.env.YAO_ENV === "development" ? error.message : undefined,
|
||||
},
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// Return error to be logged
|
||||
return { error };
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Multi-Step Process with Progress
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Complex processing with multiple steps
|
||||
*/
|
||||
function Create(ctx, messages) {
|
||||
const steps = [
|
||||
{ name: "Validating input", duration: 500 },
|
||||
{ name: "Loading context", duration: 1000 },
|
||||
{ name: "Preparing response", duration: 800 },
|
||||
];
|
||||
|
||||
// Create progress message
|
||||
const progressId = "progress_" + Date.now();
|
||||
|
||||
steps.forEach((step, index) => {
|
||||
// Update progress
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: {
|
||||
message: `${step.name}... (${index + 1}/${steps.length})`,
|
||||
},
|
||||
id: progressId,
|
||||
delta: index > 0,
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// Simulate work
|
||||
time.Sleep(step.duration);
|
||||
});
|
||||
|
||||
// Send incremental updates
|
||||
// Clear progress indicator
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "..." },
|
||||
id: "msg_1",
|
||||
delta: true,
|
||||
delta_path: "content",
|
||||
delta_action: "append",
|
||||
});
|
||||
|
||||
// Send completion marker
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "" },
|
||||
id: "msg_1",
|
||||
delta: false,
|
||||
type: "loading",
|
||||
props: { message: "" },
|
||||
id: progressId,
|
||||
done: true,
|
||||
});
|
||||
|
||||
ctx.Flush();
|
||||
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Example
|
||||
### 8. Real-time Streaming Updates
|
||||
|
||||
```javascript
|
||||
function ProcessWithErrorHandling(input) {
|
||||
const ctx = input.context;
|
||||
/**
|
||||
* Send streaming updates as processing progresses
|
||||
*/
|
||||
function Create(ctx, messages) {
|
||||
const messageId = "stream_" + Date.now();
|
||||
|
||||
try {
|
||||
// Processing logic
|
||||
ctx.Send("Processing...");
|
||||
|
||||
// Simulate error
|
||||
throw new Error("Something went wrong");
|
||||
} catch (error) {
|
||||
// Send error message
|
||||
ctx.Send({
|
||||
type: "error",
|
||||
props: {
|
||||
message: error.message,
|
||||
code: "ERR_PROCESSING",
|
||||
},
|
||||
});
|
||||
|
||||
ctx.Flush();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-step Process Example
|
||||
|
||||
```javascript
|
||||
function MultiStepProcess(input) {
|
||||
const ctx = input.context;
|
||||
|
||||
// Step 1
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: { message: "Step 1: Analyzing input..." },
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// ... processing ...
|
||||
|
||||
// Step 2
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: { message: "Step 2: Generating response..." },
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// ... processing ...
|
||||
|
||||
// Final result
|
||||
// Start message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "Process completed successfully!" },
|
||||
props: { content: "Processing" },
|
||||
id: messageId,
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
// Simulate incremental processing
|
||||
const updates = [".", ".", ".", " analyzing", ".", ".", ".", " complete!"];
|
||||
|
||||
updates.forEach((update) => {
|
||||
time.Sleep(200);
|
||||
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: update },
|
||||
id: messageId,
|
||||
delta: true,
|
||||
delta_path: "content",
|
||||
delta_action: "append",
|
||||
});
|
||||
ctx.Flush();
|
||||
});
|
||||
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Message Groups for Related Content
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Send groups of related messages together
|
||||
*/
|
||||
function Before(ctx, messages, response) {
|
||||
// Send a group of context information
|
||||
ctx.SendGroup({
|
||||
id: "context_info",
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "**Context Information:**" },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
props: { content: `User: ${ctx.authorized?.user_id || "Anonymous"}` },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
props: { content: `Session: ${ctx.chat_id}` },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
props: { content: `Locale: ${ctx.locale}` },
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
type: "context",
|
||||
},
|
||||
});
|
||||
ctx.Flush();
|
||||
|
||||
return { response };
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -425,53 +611,174 @@ ctx.Send({
|
|||
- `merge` - Merge objects
|
||||
- `set` - Set new field
|
||||
|
||||
## Notes
|
||||
## Hook Function Patterns
|
||||
|
||||
1. **No Separate Output API Needed**: The previous `const output = new Output(ctx)` approach is deprecated. Now use `ctx.Send()` methods directly.
|
||||
### Pattern 1: Fire-and-Forget Notifications
|
||||
|
||||
2. **Automatic Client Handling**: Messages are automatically converted to the appropriate format based on `ctx.accept`:
|
||||
```javascript
|
||||
function Create(ctx, messages) {
|
||||
ctx.Send("Starting processing...");
|
||||
ctx.Flush();
|
||||
// No need to wait, continue processing
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
- `standard` → OpenAI format
|
||||
- `cui-web`/`cui-native`/`cui-desktop` → CUI native format
|
||||
### Pattern 2: Progress Tracking
|
||||
|
||||
3. **Performance Optimization**: Output objects are automatically cached and managed, no manual management needed.
|
||||
```javascript
|
||||
function Create(ctx, messages) {
|
||||
const stages = ["validate", "analyze", "prepare"];
|
||||
stages.forEach((stage) => {
|
||||
ctx.Send({ type: "loading", props: { message: `${stage}...` } });
|
||||
ctx.Flush();
|
||||
performStage(stage);
|
||||
});
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
4. **Error Handling**: All methods throw JavaScript exceptions on failure, which can be caught with try-catch.
|
||||
### Pattern 3: Conditional Messaging
|
||||
|
||||
5. **Streaming Support**: Use delta updates with unique message IDs for real-time streaming scenarios.
|
||||
```javascript
|
||||
function Before(ctx, messages, response) {
|
||||
// Only show reasoning for complex queries
|
||||
if (messages[messages.length - 1].content.length > 100) {
|
||||
ctx.Send({
|
||||
type: "thinking",
|
||||
props: { content: "Analyzing complex query..." },
|
||||
});
|
||||
ctx.Flush();
|
||||
}
|
||||
return { response };
|
||||
}
|
||||
```
|
||||
|
||||
6. **Metadata**: Optional metadata can be attached to messages for tracking, debugging, or custom processing.
|
||||
### Pattern 4: Error Recovery
|
||||
|
||||
```javascript
|
||||
function Error(ctx, messages, error) {
|
||||
if (error.code === "RATE_LIMIT") {
|
||||
ctx.Send("Service is busy, retrying...");
|
||||
ctx.Flush();
|
||||
time.Sleep(1000);
|
||||
return { retry: true };
|
||||
}
|
||||
|
||||
ctx.Send({
|
||||
type: "error",
|
||||
props: { message: "Sorry, something went wrong.", code: error.code },
|
||||
});
|
||||
ctx.Flush();
|
||||
return { error };
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### 1. Hook Function Signatures
|
||||
|
||||
Each hook receives different parameters:
|
||||
|
||||
- `Create(ctx, messages)` - Context and input messages
|
||||
- `Before(ctx, messages, response)` - Context, messages, and LLM response
|
||||
- `After(ctx, messages, response)` - Context, messages, and LLM response
|
||||
- `Done(ctx, messages, response)` - Context, messages, and final response
|
||||
- `Error(ctx, messages, error)` - Context, messages, and error object
|
||||
|
||||
### 2. Always Flush for Real-time Updates
|
||||
|
||||
```javascript
|
||||
// Good - user sees message immediately
|
||||
ctx.Send("Processing...");
|
||||
ctx.Flush();
|
||||
|
||||
// Bad - message buffered until hook returns
|
||||
ctx.Send("Processing...");
|
||||
// ... hook continues ...
|
||||
```
|
||||
|
||||
### 3. Delta Updates Require Unique IDs
|
||||
|
||||
```javascript
|
||||
// Initial message
|
||||
ctx.Send({ type: "text", props: { content: "Step 1" }, id: "progress" });
|
||||
|
||||
// Update same message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: ", Step 2" },
|
||||
id: "progress",
|
||||
delta: true,
|
||||
delta_path: "content",
|
||||
delta_action: "append",
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Message Types and Client Support
|
||||
|
||||
- **OpenAI Client** (`ctx.accept === "standard"`): Supports `text`, `thinking`, `tool_call`, `image`, `audio`, `video`
|
||||
- **CUI Client** (`ctx.accept === "cui-web"` etc.): Supports all types including `loading`, `error`, `action`, `event`
|
||||
|
||||
### 5. Performance Considerations
|
||||
|
||||
- Use `Flush()` sparingly - only when immediate delivery is needed
|
||||
- Batch related messages with `SendGroup()` when possible
|
||||
- Avoid sending too many small updates (combine them)
|
||||
|
||||
### 6. Context Information Available
|
||||
|
||||
The `ctx` object provides access to:
|
||||
|
||||
```javascript
|
||||
ctx.chat_id; // Chat session ID
|
||||
ctx.assistant_id; // Assistant ID
|
||||
ctx.locale; // User locale (e.g., "en", "zh-cn")
|
||||
ctx.authorized; // User authorization info
|
||||
ctx.metadata; // Custom metadata
|
||||
ctx.client; // Client information (type, user_agent, ip)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Old Output API
|
||||
|
||||
**Before (Deprecated):**
|
||||
|
||||
```javascript
|
||||
// Old way - no longer needed
|
||||
const output = new Output(ctx)
|
||||
output.Send("Hello")
|
||||
output.SendGroup({ id: "grp1", messages: [...] })
|
||||
function Create(ctx, messages) {
|
||||
const output = new Output(ctx);
|
||||
output.Send("Hello");
|
||||
output.SendGroup({ id: "grp1", messages: [...] });
|
||||
}
|
||||
```
|
||||
|
||||
**After (Current):**
|
||||
|
||||
```javascript
|
||||
// New way - simpler and cleaner
|
||||
ctx.Send("Hello")
|
||||
ctx.SendGroup({ id: "grp1", messages: [...] })
|
||||
ctx.Flush()
|
||||
function Create(ctx, messages) {
|
||||
ctx.Send("Hello");
|
||||
ctx.SendGroup({ id: "grp1", messages: [...] });
|
||||
ctx.Flush();
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use String Shorthand for Simple Messages**: `ctx.Send("Hello")` instead of `ctx.Send({ type: "text", props: { content: "Hello" } })`
|
||||
1. **Use String Shorthand**: `ctx.Send("Hello")` is simpler than `ctx.Send({ type: "text", props: { content: "Hello" } })`
|
||||
|
||||
2. **Always Flush After Important Messages**: Use `ctx.Flush()` to ensure messages are sent immediately
|
||||
2. **Flush After Each Step**: Ensure users see progress in real-time
|
||||
|
||||
3. **Use Unique IDs for Delta Updates**: Assign unique IDs to messages that will receive incremental updates
|
||||
3. **Handle Errors Gracefully**: Always provide user-friendly error messages
|
||||
|
||||
4. **Handle Errors Gracefully**: Wrap Send operations in try-catch blocks for robust error handling
|
||||
4. **Show Progress for Long Operations**: Use loading indicators for better UX
|
||||
|
||||
5. **Use Loading Indicators**: Show loading messages for long-running operations to improve UX
|
||||
5. **Return Hook Results**: Always return required objects from hooks:
|
||||
|
||||
6. **Group Related Messages**: Use `SendGroup` for semantically related messages that should be displayed together
|
||||
- `Create`: `{ messages }`
|
||||
- `Before/After`: `{ response }`
|
||||
- `Done`: `{}` or `{ response }`
|
||||
- `Error`: `{ error }` or `{ retry: true }`
|
||||
|
||||
6. **Test with Different Clients**: Verify behavior with both OpenAI and CUI clients
|
||||
|
|
|
|||
|
|
@ -218,4 +218,3 @@ func parseGroup(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Group, error
|
|||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
593
agent/context/jsapi_output_test.go
Normal file
593
agent/context/jsapi_output_test.go
Normal file
|
|
@ -0,0 +1,593 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||
"github.com/yaoapp/yao/config"
|
||||
"github.com/yaoapp/yao/test"
|
||||
)
|
||||
|
||||
// mockResponseWriter is a mock implementation of http.ResponseWriter for testing
|
||||
type mockResponseWriter struct {
|
||||
headers http.Header
|
||||
buffer *bytes.Buffer
|
||||
status int
|
||||
}
|
||||
|
||||
func newMockResponseWriter() *mockResponseWriter {
|
||||
return &mockResponseWriter{
|
||||
headers: make(http.Header),
|
||||
buffer: &bytes.Buffer{},
|
||||
status: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Header() http.Header {
|
||||
return m.headers
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Write(b []byte) (int, error) {
|
||||
return m.buffer.Write(b)
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) WriteHeader(statusCode int) {
|
||||
m.status = statusCode
|
||||
}
|
||||
|
||||
// TestJsValueSend test the Send method on Context
|
||||
func TestJsValueSend(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
// Test sending string shorthand
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Send simple string
|
||||
ctx.Send("Hello World");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Send string should succeed")
|
||||
|
||||
// Test sending message object
|
||||
res, err = v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Send message object
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: {
|
||||
content: "Hello from JavaScript"
|
||||
},
|
||||
id: "msg_123",
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
sequence: 1
|
||||
}
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok = res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Send message object should succeed")
|
||||
}
|
||||
|
||||
// TestJsValueSendGroup test the SendGroup method on Context
|
||||
func TestJsValueSendGroup(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Send message group
|
||||
ctx.SendGroup({
|
||||
id: "group_123",
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "First message" }
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "Second message" }
|
||||
},
|
||||
{
|
||||
type: "loading",
|
||||
props: { message: "Processing..." }
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
sequence: 1
|
||||
}
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "SendGroup should succeed")
|
||||
}
|
||||
|
||||
// TestJsValueFlush test the Flush method on Context
|
||||
func TestJsValueFlush(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Send a message
|
||||
ctx.Send("Processing...");
|
||||
|
||||
// Flush output
|
||||
ctx.Flush();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Flush should succeed")
|
||||
}
|
||||
|
||||
// TestJsValueSendDeltaUpdates test delta updates in Send
|
||||
func TestJsValueSendDeltaUpdates(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Send initial message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "Hello" },
|
||||
id: "msg_1",
|
||||
delta: false
|
||||
});
|
||||
|
||||
// Send delta update (append)
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: " World" },
|
||||
id: "msg_1",
|
||||
delta: true,
|
||||
delta_path: "content",
|
||||
delta_action: "append"
|
||||
});
|
||||
|
||||
// Mark as complete
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: {},
|
||||
id: "msg_1",
|
||||
done: true
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Delta updates should succeed")
|
||||
}
|
||||
|
||||
// TestJsValueSendMultipleTypes test sending different message types
|
||||
func TestJsValueSendMultipleTypes(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Text message
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "Hello" }
|
||||
});
|
||||
|
||||
// Thinking message
|
||||
ctx.Send({
|
||||
type: "thinking",
|
||||
props: { content: "Let me think..." }
|
||||
});
|
||||
|
||||
// Loading message
|
||||
ctx.Send({
|
||||
type: "loading",
|
||||
props: { message: "Processing..." }
|
||||
});
|
||||
|
||||
// Tool call message
|
||||
ctx.Send({
|
||||
type: "tool_call",
|
||||
props: {
|
||||
id: "call_123",
|
||||
name: "get_weather",
|
||||
arguments: '{"location": "San Francisco"}'
|
||||
}
|
||||
});
|
||||
|
||||
// Error message
|
||||
ctx.Send({
|
||||
type: "error",
|
||||
props: {
|
||||
message: "Something went wrong",
|
||||
code: "ERR_500"
|
||||
}
|
||||
});
|
||||
|
||||
// Image message
|
||||
ctx.Send({
|
||||
type: "image",
|
||||
props: {
|
||||
url: "https://example.com/image.jpg",
|
||||
alt: "Example image",
|
||||
width: 800,
|
||||
height: 600
|
||||
}
|
||||
});
|
||||
|
||||
ctx.Flush();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Multiple message types should succeed")
|
||||
}
|
||||
|
||||
// TestJsValueSendErrorHandling test error handling in Send
|
||||
func TestJsValueSendErrorHandling(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
// Test invalid argument - no arguments
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
ctx.Send();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, false, result["success"], "Send without arguments should fail")
|
||||
assert.Contains(t, result["error"], "Send requires a message argument", "Error should mention missing message")
|
||||
}
|
||||
|
||||
// TestJsValueSendGroupErrorHandling test error handling in SendGroup
|
||||
func TestJsValueSendGroupErrorHandling(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
// Test invalid argument - no arguments
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
ctx.SendGroup();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, false, result["success"], "SendGroup without arguments should fail")
|
||||
assert.Contains(t, result["error"], "SendGroup requires a group argument", "Error should mention missing group")
|
||||
|
||||
// Test invalid group - missing messages
|
||||
res, err = v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
ctx.SendGroup({ id: "grp_1" });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok = res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, false, result["success"], "SendGroup without messages should fail")
|
||||
}
|
||||
|
||||
// TestJsValueSendWithCUIAccept test Send with CUI accept types
|
||||
func TestJsValueSendWithCUIAccept(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
acceptTypes := []string{"cui-web", "cui-native", "cui-desktop"}
|
||||
|
||||
for _, acceptType := range acceptTypes {
|
||||
t.Run(acceptType, func(t *testing.T) {
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: Accept(acceptType),
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
ctx.Send({
|
||||
type: "text",
|
||||
props: { content: "Hello CUI" }
|
||||
});
|
||||
ctx.Flush();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Send with "+acceptType+" should succeed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestJsValueSendGroupWithMetadata test SendGroup with various metadata
|
||||
func TestJsValueSendGroupWithMetadata(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
ctx.SendGroup({
|
||||
id: "group_with_metadata",
|
||||
messages: [
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "Message 1" },
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
sequence: 1,
|
||||
trace_id: "trace_abc"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
props: { content: "Message 2" },
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
sequence: 2,
|
||||
trace_id: "trace_abc"
|
||||
}
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
timestamp: Date.now(),
|
||||
sequence: 1,
|
||||
trace_id: "trace_abc"
|
||||
}
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "SendGroup with metadata should succeed")
|
||||
}
|
||||
|
||||
// TestJsValueSendChainedCalls test chained Send calls
|
||||
func TestJsValueSendChainedCalls(t *testing.T) {
|
||||
|
||||
test.Prepare(t, config.Conf)
|
||||
defer test.Clean()
|
||||
|
||||
cxt := &Context{
|
||||
ChatID: "test-chat-id",
|
||||
AssistantID: "test-assistant-id",
|
||||
Context: context.Background(),
|
||||
Accept: "standard",
|
||||
Locale: "en",
|
||||
Writer: newMockResponseWriter(),
|
||||
}
|
||||
|
||||
res, err := v8.Call(v8.CallOptions{}, `
|
||||
function test(ctx) {
|
||||
try {
|
||||
// Multiple sequential sends
|
||||
ctx.Send("Step 1");
|
||||
ctx.Send("Step 2");
|
||||
ctx.Send("Step 3");
|
||||
ctx.Flush();
|
||||
|
||||
// Send after flush
|
||||
ctx.Send("Step 4");
|
||||
ctx.Flush();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}`, cxt)
|
||||
if err != nil {
|
||||
t.Fatalf("Call failed: %v", err)
|
||||
}
|
||||
|
||||
result, ok := res.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Expected map result, got %T", res)
|
||||
}
|
||||
assert.Equal(t, true, result["success"], "Chained Send calls should succeed")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue