Enhance streaming and grouping functionality in Assistant
- Introduced SendGroupStart and SendGroupEnd methods for better control over message grouping, allowing for automatic ID generation and event handling. - Updated streamState structure to track group IDs, message sequences, and chunk counts, improving message organization during streaming. - Refactored message handling to eliminate the 'done' field, signaling message completion through group_end events instead. - Revised JSAPI documentation to reflect new methods and usage patterns, enhancing clarity for developers. - Improved test cases to validate new group handling features and ensure proper functionality.
This commit is contained in:
parent
afab9c2173
commit
f0495ab990
8 changed files with 433 additions and 164 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
|
@ -17,7 +20,8 @@ func DefaultStreamHandler(ctx *context.Context) message.StreamFunc {
|
||||||
state := &streamState{
|
state := &streamState{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
inGroup: false,
|
inGroup: false,
|
||||||
currentID: "",
|
currentGroupID: "",
|
||||||
|
messageSeq: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(chunkType message.StreamChunkType, data []byte) int {
|
return func(chunkType message.StreamChunkType, data []byte) int {
|
||||||
|
|
@ -66,9 +70,12 @@ func DefaultStreamHandler(ctx *context.Context) message.StreamFunc {
|
||||||
type streamState struct {
|
type streamState struct {
|
||||||
ctx *context.Context
|
ctx *context.Context
|
||||||
inGroup bool
|
inGroup bool
|
||||||
currentID string
|
currentGroupID string // Current group ID (shared by all chunks in the group)
|
||||||
currentType string // Track the current message type (text, thinking, tool_call)
|
currentType string // Track the current message type (text, thinking, tool_call)
|
||||||
buffer []byte
|
buffer []byte
|
||||||
|
chunkCount int // Track number of chunks in current group
|
||||||
|
messageSeq int // Message sequence number (for generating readable IDs)
|
||||||
|
groupStartTime time.Time // Track when group started
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStreamStart handles stream start event
|
// handleStreamStart handles stream start event
|
||||||
|
|
@ -87,9 +94,32 @@ func (s *streamState) handleStreamStart(data []byte) int {
|
||||||
|
|
||||||
// handleGroupStart handles group start event
|
// handleGroupStart handles group start event
|
||||||
func (s *streamState) handleGroupStart(data []byte) int {
|
func (s *streamState) handleGroupStart(data []byte) int {
|
||||||
|
// Parse group start data first to get the group ID
|
||||||
|
var startData message.GroupStartData
|
||||||
|
if err := jsoniter.Unmarshal(data, &startData); err != nil {
|
||||||
|
log.Error("Failed to unmarshal group start data: %v", err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the group ID from the start data, or generate one if not provided
|
||||||
|
groupID := startData.GroupID
|
||||||
|
if groupID == "" {
|
||||||
|
groupID = generateMessageID()
|
||||||
|
startData.GroupID = groupID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize group state with the correct group ID
|
||||||
s.inGroup = true
|
s.inGroup = true
|
||||||
s.currentID = generateMessageID()
|
s.currentGroupID = groupID
|
||||||
s.buffer = []byte{}
|
s.buffer = []byte{}
|
||||||
|
s.chunkCount = 0
|
||||||
|
s.messageSeq = 0 // Reset message sequence for each group
|
||||||
|
s.groupStartTime = time.Now()
|
||||||
|
|
||||||
|
// Send group_start event
|
||||||
|
msg := output.NewEventMessage(message.EventGroupStart, "Group started", startData)
|
||||||
|
s.ctx.Send(msg)
|
||||||
|
|
||||||
return 0 // Continue
|
return 0 // Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,20 +129,20 @@ func (s *streamState) handleText(data []byte) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have a message ID
|
|
||||||
if s.currentID == "" {
|
|
||||||
s.currentID = generateMessageID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track current message type
|
// Track current message type
|
||||||
s.currentType = message.TypeText
|
s.currentType = message.TypeText
|
||||||
|
|
||||||
// Append to buffer
|
// Append to buffer
|
||||||
s.buffer = append(s.buffer, data...)
|
s.buffer = append(s.buffer, data...)
|
||||||
|
s.chunkCount++
|
||||||
|
s.messageSeq++
|
||||||
|
|
||||||
// Send delta message
|
// Send delta message
|
||||||
|
// - ID: Sequential message ID (e.g., msg_001, msg_002) for readability
|
||||||
|
// - GroupID: Same for all chunks of this logical message (frontend merges by group_id)
|
||||||
msg := &message.Message{
|
msg := &message.Message{
|
||||||
ID: s.currentID,
|
ID: s.generateSequentialID(), // Sequential ID for this chunk
|
||||||
|
GroupID: s.currentGroupID, // Group ID for merging (all chunks share this)
|
||||||
Type: message.TypeText,
|
Type: message.TypeText,
|
||||||
Delta: true,
|
Delta: true,
|
||||||
Props: map[string]interface{}{
|
Props: map[string]interface{}{
|
||||||
|
|
@ -134,20 +164,20 @@ func (s *streamState) handleThinking(data []byte) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have a message ID
|
|
||||||
if s.currentID == "" {
|
|
||||||
s.currentID = generateMessageID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track current message type
|
// Track current message type
|
||||||
s.currentType = message.TypeThinking
|
s.currentType = message.TypeThinking
|
||||||
|
|
||||||
// Append to buffer
|
// Append to buffer
|
||||||
s.buffer = append(s.buffer, data...)
|
s.buffer = append(s.buffer, data...)
|
||||||
|
s.chunkCount++
|
||||||
|
s.messageSeq++
|
||||||
|
|
||||||
// Send delta message
|
// Send delta message
|
||||||
|
// - ID: Sequential message ID (e.g., msg_001, msg_002) for readability
|
||||||
|
// - GroupID: Same for all chunks of this logical message (frontend merges by group_id)
|
||||||
msg := &message.Message{
|
msg := &message.Message{
|
||||||
ID: s.currentID,
|
ID: s.generateSequentialID(), // Sequential ID for this chunk
|
||||||
|
GroupID: s.currentGroupID, // Group ID for merging (all chunks share this)
|
||||||
Type: message.TypeThinking,
|
Type: message.TypeThinking,
|
||||||
Delta: true,
|
Delta: true,
|
||||||
Props: map[string]interface{}{
|
Props: map[string]interface{}{
|
||||||
|
|
@ -202,30 +232,38 @@ func (s *streamState) handleGroupEnd(data []byte) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send done message with complete content
|
// Calculate duration
|
||||||
if s.currentID != "" && len(s.buffer) > 0 {
|
durationMs := time.Since(s.groupStartTime).Milliseconds()
|
||||||
|
|
||||||
// Use the tracked message type (thinking, text, tool_call, etc.)
|
// Use the tracked message type (thinking, text, tool_call, etc.)
|
||||||
msgType := s.currentType
|
msgType := s.currentType
|
||||||
if msgType == "" {
|
if msgType == "" {
|
||||||
msgType = message.TypeText // Fallback to text if type not set
|
msgType = message.TypeText // Fallback to text if type not set
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &message.Message{
|
// Build GroupEndData with complete content
|
||||||
ID: s.currentID,
|
endData := message.GroupEndData{
|
||||||
Type: msgType, // Use the actual message type from the group
|
GroupID: s.currentGroupID, // Use the group ID, not message ID
|
||||||
Done: true,
|
Type: msgType,
|
||||||
Props: map[string]interface{}{
|
Timestamp: time.Now().UnixMilli(),
|
||||||
"content": string(s.buffer),
|
DurationMs: durationMs,
|
||||||
|
ChunkCount: s.chunkCount,
|
||||||
|
Status: "completed",
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"content": string(s.buffer), // Include complete content in the event
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send group_end event
|
||||||
|
msg := output.NewEventMessage(message.EventGroupEnd, "Group completed", endData)
|
||||||
s.ctx.Send(msg)
|
s.ctx.Send(msg)
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
s.inGroup = false
|
s.inGroup = false
|
||||||
s.currentID = ""
|
s.currentGroupID = ""
|
||||||
s.currentType = ""
|
s.currentType = ""
|
||||||
s.buffer = []byte{}
|
s.buffer = []byte{}
|
||||||
|
s.chunkCount = 0
|
||||||
|
|
||||||
return 0 // Continue
|
return 0 // Continue
|
||||||
}
|
}
|
||||||
|
|
@ -249,6 +287,13 @@ func (s *streamState) handleStreamEnd(data []byte) int {
|
||||||
return 0 // Continue (stream will end naturally)
|
return 0 // Continue (stream will end naturally)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateSequentialID generates a sequential message ID for better readability
|
||||||
|
func (s *streamState) generateSequentialID() string {
|
||||||
|
// Format: 1, 2, 3, etc.
|
||||||
|
// This makes it easier for developers to track message order in logs
|
||||||
|
return fmt.Sprintf("%d", s.messageSeq)
|
||||||
|
}
|
||||||
|
|
||||||
// generateMessageID generates a unique message ID
|
// generateMessageID generates a unique message ID
|
||||||
func generateMessageID() string {
|
func generateMessageID() string {
|
||||||
// TODO: Implement proper ID generation
|
// TODO: Implement proper ID generation
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Context Output JS API
|
# Context Output JS API
|
||||||
|
|
||||||
The Context object provides `Send`, `SendGroup`, and `Flush` methods for sending messages to clients from JavaScript within Agent Hook functions.
|
The Context object provides `Send`, `SendGroup`, `SendGroupStart`, and `SendGroupEnd` methods for sending messages to clients from JavaScript within Agent Hook functions.
|
||||||
|
|
||||||
## Hook Functions Overview
|
## Hook Functions Overview
|
||||||
|
|
||||||
|
|
@ -23,16 +23,14 @@ Agent Hook functions are lifecycle callbacks that allow you to customize the beh
|
||||||
* Create hook - send initial messages to client
|
* Create hook - send initial messages to client
|
||||||
*/
|
*/
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
// Send welcome message (string shorthand)
|
// Send welcome message (string shorthand, auto-flushes)
|
||||||
ctx.Send("Welcome! Let me help you with that...");
|
ctx.Send("Welcome! Let me help you with that...");
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
// Send loading indicator
|
// Send loading indicator (auto-flushes)
|
||||||
ctx.Send({
|
ctx.Send({
|
||||||
type: "loading",
|
type: "loading",
|
||||||
props: { message: "Analyzing your request..." },
|
props: { message: "Analyzing your request..." },
|
||||||
});
|
});
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
// Continue with normal processing
|
// Continue with normal processing
|
||||||
return { messages };
|
return { messages };
|
||||||
|
|
@ -154,13 +152,43 @@ ctx.SendGroup({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### ctx.Flush()
|
### ctx.SendGroupStart(type?, id?)
|
||||||
|
|
||||||
Flush the output buffer to ensure messages are sent immediately.
|
Start a message group and return the group ID. Messages sent after this should include the returned `group_id`.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `type` (optional): Group type (`"text"`, `"thinking"`, `"tool_call"`, `"mixed"`), defaults to `"mixed"`
|
||||||
|
- `id` (optional): Custom group ID, auto-generates if not provided
|
||||||
|
|
||||||
|
**Returns:** Group ID (string)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
ctx.Send("Processing...");
|
// Auto-generate ID with default type
|
||||||
ctx.Flush(); // Send immediately to client
|
const groupId = ctx.SendGroupStart();
|
||||||
|
|
||||||
|
// Specify type, auto-generate ID
|
||||||
|
const groupId = ctx.SendGroupStart("text");
|
||||||
|
|
||||||
|
// Specify both type and custom ID
|
||||||
|
const groupId = ctx.SendGroupStart("thinking", "my-group-123");
|
||||||
|
```
|
||||||
|
|
||||||
|
### ctx.SendGroupEnd(id, chunkCount?)
|
||||||
|
|
||||||
|
End a message group.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `id` (required): Group ID returned from `SendGroupStart`
|
||||||
|
- `chunkCount` (optional): Number of messages in the group
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Basic usage
|
||||||
|
ctx.SendGroupEnd(groupId);
|
||||||
|
|
||||||
|
// With chunk count
|
||||||
|
ctx.SendGroupEnd(groupId, 5);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Complete Hook Examples
|
## Complete Hook Examples
|
||||||
|
|
@ -172,9 +200,8 @@ ctx.Flush(); // Send immediately to client
|
||||||
* Send welcome message when conversation starts
|
* Send welcome message when conversation starts
|
||||||
*/
|
*/
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
// Send welcome message
|
// Send welcome message (auto-flushes)
|
||||||
ctx.Send("Welcome to AI Assistant! How can I help you today?");
|
ctx.Send("Welcome to AI Assistant! How can I help you today?");
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
// Return messages to continue processing
|
// Return messages to continue processing
|
||||||
return { messages };
|
return { messages };
|
||||||
|
|
@ -409,16 +436,15 @@ function Create(ctx, messages) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9. Message Groups for Related Content
|
### 9. Message Groups for Related Content (High-level API)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/**
|
/**
|
||||||
* Send groups of related messages together
|
* Send groups of related messages together using SendGroup (auto-handles events)
|
||||||
*/
|
*/
|
||||||
function Before(ctx, messages, response) {
|
function Before(ctx, messages, response) {
|
||||||
// Send a group of context information
|
// SendGroup automatically sends group_start and group_end events
|
||||||
ctx.SendGroup({
|
ctx.SendGroup({
|
||||||
id: "context_info",
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -442,12 +468,86 @@ function Before(ctx, messages, response) {
|
||||||
type: "context",
|
type: "context",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
return { response };
|
return { response };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 10. Manual Group Control (Low-level API)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Manually control group boundaries with SendGroupStart and SendGroupEnd
|
||||||
|
*/
|
||||||
|
function Create(ctx, messages) {
|
||||||
|
// Start a text group
|
||||||
|
const groupId = ctx.SendGroupStart("text");
|
||||||
|
|
||||||
|
// Send messages with group_id
|
||||||
|
ctx.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "First message in group" },
|
||||||
|
group_id: groupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "Second message in group" },
|
||||||
|
group_id: groupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// End the group
|
||||||
|
ctx.SendGroupEnd(groupId, 2);
|
||||||
|
|
||||||
|
return { messages };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11. Streaming with Groups
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Stream delta updates within a group
|
||||||
|
*/
|
||||||
|
function Create(ctx, messages) {
|
||||||
|
// Start thinking group
|
||||||
|
const thinkingId = ctx.SendGroupStart("thinking");
|
||||||
|
|
||||||
|
// Stream thinking process
|
||||||
|
const steps = ["Analyzing", "Processing", "Generating"];
|
||||||
|
const msgId = "thinking_msg";
|
||||||
|
|
||||||
|
steps.forEach((step, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
// First message
|
||||||
|
ctx.Send({
|
||||||
|
type: "thinking",
|
||||||
|
props: { content: step },
|
||||||
|
id: msgId,
|
||||||
|
group_id: thinkingId,
|
||||||
|
delta: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Delta updates
|
||||||
|
ctx.Send({
|
||||||
|
type: "thinking",
|
||||||
|
props: { content: ` → ${step}` },
|
||||||
|
id: msgId,
|
||||||
|
group_id: thinkingId,
|
||||||
|
delta: true,
|
||||||
|
delta_path: "content",
|
||||||
|
delta_action: "append",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// End thinking group
|
||||||
|
ctx.SendGroupEnd(thinkingId, steps.length);
|
||||||
|
|
||||||
|
return { messages };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Message Types
|
## Message Types
|
||||||
|
|
||||||
Built-in message types supported:
|
Built-in message types supported:
|
||||||
|
|
@ -617,9 +717,8 @@ ctx.Send({
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
ctx.Send("Starting processing...");
|
ctx.Send("Starting processing..."); // Auto-flushes
|
||||||
ctx.Flush();
|
// Continue processing immediately
|
||||||
// No need to wait, continue processing
|
|
||||||
return { messages };
|
return { messages };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -630,8 +729,7 @@ function Create(ctx, messages) {
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
const stages = ["validate", "analyze", "prepare"];
|
const stages = ["validate", "analyze", "prepare"];
|
||||||
stages.forEach((stage) => {
|
stages.forEach((stage) => {
|
||||||
ctx.Send({ type: "loading", props: { message: `${stage}...` } });
|
ctx.Send({ type: "loading", props: { message: `${stage}...` } }); // Auto-flushes
|
||||||
ctx.Flush();
|
|
||||||
performStage(stage);
|
performStage(stage);
|
||||||
});
|
});
|
||||||
return { messages };
|
return { messages };
|
||||||
|
|
@ -647,8 +745,7 @@ function Before(ctx, messages, response) {
|
||||||
ctx.Send({
|
ctx.Send({
|
||||||
type: "thinking",
|
type: "thinking",
|
||||||
props: { content: "Analyzing complex query..." },
|
props: { content: "Analyzing complex query..." },
|
||||||
});
|
}); // Auto-flushes
|
||||||
ctx.Flush();
|
|
||||||
}
|
}
|
||||||
return { response };
|
return { response };
|
||||||
}
|
}
|
||||||
|
|
@ -659,8 +756,7 @@ function Before(ctx, messages, response) {
|
||||||
```javascript
|
```javascript
|
||||||
function Error(ctx, messages, error) {
|
function Error(ctx, messages, error) {
|
||||||
if (error.code === "RATE_LIMIT") {
|
if (error.code === "RATE_LIMIT") {
|
||||||
ctx.Send("Service is busy, retrying...");
|
ctx.Send("Service is busy, retrying..."); // Auto-flushes
|
||||||
ctx.Flush();
|
|
||||||
time.Sleep(1000);
|
time.Sleep(1000);
|
||||||
return { retry: true };
|
return { retry: true };
|
||||||
}
|
}
|
||||||
|
|
@ -668,8 +764,7 @@ function Error(ctx, messages, error) {
|
||||||
ctx.Send({
|
ctx.Send({
|
||||||
type: "error",
|
type: "error",
|
||||||
props: { message: "Sorry, something went wrong.", code: error.code },
|
props: { message: "Sorry, something went wrong.", code: error.code },
|
||||||
});
|
}); // Auto-flushes
|
||||||
ctx.Flush();
|
|
||||||
return { error };
|
return { error };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -686,16 +781,16 @@ Each hook receives different parameters:
|
||||||
- `Done(ctx, messages, response)` - Context, messages, and final response
|
- `Done(ctx, messages, response)` - Context, messages, and final response
|
||||||
- `Error(ctx, messages, error)` - Context, messages, and error object
|
- `Error(ctx, messages, error)` - Context, messages, and error object
|
||||||
|
|
||||||
### 2. Always Flush for Real-time Updates
|
### 2. Messages Auto-Flush for Real-time Updates
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Good - user sees message immediately
|
// Messages are automatically flushed after each Send
|
||||||
ctx.Send("Processing...");
|
ctx.Send("Processing..."); // Sent immediately to client
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
// Bad - message buffered until hook returns
|
// Multiple sends work seamlessly
|
||||||
ctx.Send("Processing...");
|
ctx.Send("Step 1"); // Flushed
|
||||||
// ... hook continues ...
|
ctx.Send("Step 2"); // Flushed
|
||||||
|
ctx.Send("Step 3"); // Flushed
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Delta Updates Require Unique IDs
|
### 3. Delta Updates Require Unique IDs
|
||||||
|
|
@ -722,9 +817,10 @@ ctx.Send({
|
||||||
|
|
||||||
### 5. Performance Considerations
|
### 5. Performance Considerations
|
||||||
|
|
||||||
- Use `Flush()` sparingly - only when immediate delivery is needed
|
- Messages auto-flush after each `Send()` for real-time delivery
|
||||||
- Batch related messages with `SendGroup()` when possible
|
- Batch related messages with `SendGroup()` when possible for better performance
|
||||||
- Avoid sending too many small updates (combine them)
|
- Avoid sending too many small updates (combine them when feasible)
|
||||||
|
- Use `SendGroupStart`/`SendGroupEnd` for fine-grained control over grouping
|
||||||
|
|
||||||
### 6. Context Information Available
|
### 6. Context Information Available
|
||||||
|
|
||||||
|
|
@ -757,9 +853,8 @@ function Create(ctx, messages) {
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function Create(ctx, messages) {
|
function Create(ctx, messages) {
|
||||||
ctx.Send("Hello");
|
ctx.Send("Hello"); // Auto-flushes
|
||||||
ctx.SendGroup({ id: "grp1", messages: [...] });
|
ctx.SendGroup({ messages: [...] }); // Auto-handles events and flushing
|
||||||
ctx.Flush();
|
|
||||||
return { messages };
|
return { messages };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -768,17 +863,24 @@ function Create(ctx, messages) {
|
||||||
|
|
||||||
1. **Use String Shorthand**: `ctx.Send("Hello")` is simpler than `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. **Flush After Each Step**: Ensure users see progress in real-time
|
2. **Messages Auto-Flush**: Each `Send()` automatically flushes for real-time delivery - no manual flushing needed
|
||||||
|
|
||||||
3. **Handle Errors Gracefully**: Always provide user-friendly error messages
|
3. **Choose the Right API Level**:
|
||||||
|
|
||||||
4. **Show Progress for Long Operations**: Use loading indicators for better UX
|
- **High-level**: Use `SendGroup()` for simple grouped messages (auto-handles events)
|
||||||
|
- **Low-level**: Use `SendGroupStart()`/`SendGroupEnd()` for fine-grained control
|
||||||
|
|
||||||
5. **Return Hook Results**: Always return required objects from hooks:
|
4. **Handle Errors Gracefully**: Always provide user-friendly error messages
|
||||||
|
|
||||||
|
5. **Show Progress for Long Operations**: Use loading indicators for better UX
|
||||||
|
|
||||||
|
6. **Return Hook Results**: Always return required objects from hooks:
|
||||||
|
|
||||||
- `Create`: `{ messages }`
|
- `Create`: `{ messages }`
|
||||||
- `Before/After`: `{ response }`
|
- `Before/After`: `{ response }`
|
||||||
- `Done`: `{}` or `{ response }`
|
- `Done`: `{}` or `{ response }`
|
||||||
- `Error`: `{ error }` or `{ retry: true }`
|
- `Error`: `{ error }` or `{ retry: true }`
|
||||||
|
|
||||||
6. **Test with Different Clients**: Verify behavior with both OpenAI and CUI clients
|
7. **Test with Different Clients**: Verify behavior with both OpenAI and CUI clients
|
||||||
|
|
||||||
|
8. **Group Related Messages**: Use groups to organize related content for better frontend rendering
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/runtime/v8/bridge"
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
||||||
|
"github.com/yaoapp/yao/agent/output"
|
||||||
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
|
traceJsapi "github.com/yaoapp/yao/trace/jsapi"
|
||||||
"rogchap.com/v8go"
|
"rogchap.com/v8go"
|
||||||
)
|
)
|
||||||
|
|
@ -47,7 +51,8 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) {
|
||||||
jsObject.Set("Trace", ctx.traceMethod(v8ctx.Isolate()))
|
jsObject.Set("Trace", ctx.traceMethod(v8ctx.Isolate()))
|
||||||
jsObject.Set("Send", ctx.sendMethod(v8ctx.Isolate()))
|
jsObject.Set("Send", ctx.sendMethod(v8ctx.Isolate()))
|
||||||
jsObject.Set("SendGroup", ctx.sendGroupMethod(v8ctx.Isolate()))
|
jsObject.Set("SendGroup", ctx.sendGroupMethod(v8ctx.Isolate()))
|
||||||
jsObject.Set("Flush", ctx.flushMethod(v8ctx.Isolate()))
|
jsObject.Set("SendGroupStart", ctx.sendGroupStartMethod(v8ctx.Isolate()))
|
||||||
|
jsObject.Set("SendGroupEnd", ctx.sendGroupEndMethod(v8ctx.Isolate()))
|
||||||
|
|
||||||
// Create instance
|
// Create instance
|
||||||
instance, err := jsObject.NewInstance(v8ctx)
|
instance, err := jsObject.NewInstance(v8ctx)
|
||||||
|
|
@ -164,6 +169,7 @@ func (ctx *Context) traceMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
// sendMethod implements ctx.Send(message)
|
// sendMethod implements ctx.Send(message)
|
||||||
// Usage: ctx.Send({ type: "text", props: { content: "Hello" } })
|
// Usage: ctx.Send({ type: "text", props: { content: "Hello" } })
|
||||||
// Usage: ctx.Send("Hello") // shorthand for text message
|
// Usage: ctx.Send("Hello") // shorthand for text message
|
||||||
|
// Automatically generates ID and flushes output
|
||||||
func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
v8ctx := info.Context()
|
v8ctx := info.Context()
|
||||||
|
|
@ -179,17 +185,28 @@ func (ctx *Context) sendMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
return bridge.JsException(v8ctx, "invalid message: "+err.Error())
|
return bridge.JsException(v8ctx, "invalid message: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate unique ID if not provided
|
||||||
|
if msg.ID == "" {
|
||||||
|
msg.ID = output.GenerateID()
|
||||||
|
}
|
||||||
|
|
||||||
// Call ctx.Send
|
// Call ctx.Send
|
||||||
if err := ctx.Send(msg); err != nil {
|
if err := ctx.Send(msg); err != nil {
|
||||||
return bridge.JsException(v8ctx, "Send failed: "+err.Error())
|
return bridge.JsException(v8ctx, "Send failed: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically flush after sending
|
||||||
|
if err := ctx.Flush(); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return v8go.Undefined(iso)
|
return v8go.Undefined(iso)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendGroupMethod implements ctx.SendGroup(group)
|
// sendGroupMethod implements ctx.SendGroup(group)
|
||||||
// Usage: ctx.SendGroup({ id: "group1", messages: [...] })
|
// Usage: ctx.SendGroup({ id: "group1", messages: [...] })
|
||||||
|
// Automatically generates IDs, sends group_start/group_end events, and flushes output
|
||||||
func (ctx *Context) sendGroupMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
func (ctx *Context) sendGroupMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
v8ctx := info.Context()
|
v8ctx := info.Context()
|
||||||
|
|
@ -205,24 +222,159 @@ func (ctx *Context) sendGroupMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
return bridge.JsException(v8ctx, "invalid group: "+err.Error())
|
return bridge.JsException(v8ctx, "invalid group: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate group ID if not provided
|
||||||
|
if group.ID == "" {
|
||||||
|
group.ID = output.GenerateID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send group_start event
|
||||||
|
startTime := time.Now()
|
||||||
|
startEvent := output.NewEventMessage(
|
||||||
|
message.EventGroupStart,
|
||||||
|
"Group started",
|
||||||
|
message.GroupStartData{
|
||||||
|
GroupID: group.ID,
|
||||||
|
Type: "mixed", // Mixed types in group
|
||||||
|
Timestamp: startTime.UnixMilli(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err := ctx.Send(startEvent); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Failed to send group_start event: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := ctx.Flush(); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Flush failed after group_start: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate IDs for messages and set group_id
|
||||||
|
for _, msg := range group.Messages {
|
||||||
|
if msg.ID == "" {
|
||||||
|
msg.ID = output.GenerateID()
|
||||||
|
}
|
||||||
|
if msg.GroupID == "" {
|
||||||
|
msg.GroupID = group.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Call ctx.SendGroup
|
// Call ctx.SendGroup
|
||||||
if err := ctx.SendGroup(group); err != nil {
|
if err := ctx.SendGroup(group); err != nil {
|
||||||
return bridge.JsException(v8ctx, "SendGroup failed: "+err.Error())
|
return bridge.JsException(v8ctx, "SendGroup failed: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return v8go.Undefined(iso)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// flushMethod implements ctx.Flush()
|
|
||||||
// Usage: ctx.Flush()
|
|
||||||
func (ctx *Context) flushMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
||||||
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
||||||
v8ctx := info.Context()
|
|
||||||
|
|
||||||
// Call ctx.Flush
|
|
||||||
if err := ctx.Flush(); err != nil {
|
if err := ctx.Flush(); err != nil {
|
||||||
return bridge.JsException(v8ctx, "Flush failed: "+err.Error())
|
return bridge.JsException(v8ctx, "Flush failed after SendGroup: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send group_end event
|
||||||
|
endEvent := output.NewEventMessage(
|
||||||
|
message.EventGroupEnd,
|
||||||
|
"Group completed",
|
||||||
|
message.GroupEndData{
|
||||||
|
GroupID: group.ID,
|
||||||
|
Type: "mixed",
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
DurationMs: time.Since(startTime).Milliseconds(),
|
||||||
|
ChunkCount: len(group.Messages),
|
||||||
|
Status: "completed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err := ctx.Send(endEvent); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Failed to send group_end event: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := ctx.Flush(); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Flush failed after group_end: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return v8go.Undefined(iso)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendGroupStartMethod implements ctx.SendGroupStart(type?, id?)
|
||||||
|
// Usage: const groupId = ctx.SendGroupStart() // type="mixed", auto-generate ID
|
||||||
|
// Usage: const groupId = ctx.SendGroupStart("text") // type="text", auto-generate ID
|
||||||
|
// Usage: const groupId = ctx.SendGroupStart("text", "my-group-id") // type="text", use provided ID
|
||||||
|
// Returns the group ID (generated or provided)
|
||||||
|
func (ctx *Context) sendGroupStartMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
v8ctx := info.Context()
|
||||||
|
args := info.Args()
|
||||||
|
|
||||||
|
// Get type (default: "mixed")
|
||||||
|
groupType := "mixed"
|
||||||
|
if len(args) > 0 && args[0].IsString() {
|
||||||
|
groupType = args[0].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or generate group ID
|
||||||
|
var groupID string
|
||||||
|
if len(args) > 1 && args[1].IsString() {
|
||||||
|
groupID = args[1].String()
|
||||||
|
} else {
|
||||||
|
groupID = output.GenerateID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send group_start event
|
||||||
|
startEvent := output.NewEventMessage(
|
||||||
|
message.EventGroupStart,
|
||||||
|
"Group started",
|
||||||
|
message.GroupStartData{
|
||||||
|
GroupID: groupID,
|
||||||
|
Type: groupType,
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err := ctx.Send(startEvent); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Failed to send group_start event: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := ctx.Flush(); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Flush failed after group_start: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the group ID
|
||||||
|
groupIDVal, err := v8go.NewValue(iso, groupID)
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Failed to create return value: "+err.Error())
|
||||||
|
}
|
||||||
|
return groupIDVal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendGroupEndMethod implements ctx.SendGroupEnd(id, chunkCount?)
|
||||||
|
// Usage: ctx.SendGroupEnd(groupId)
|
||||||
|
// Usage: ctx.SendGroupEnd(groupId, 10) // With chunk count
|
||||||
|
func (ctx *Context) sendGroupEndMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
v8ctx := info.Context()
|
||||||
|
args := info.Args()
|
||||||
|
|
||||||
|
// Group ID is required
|
||||||
|
if len(args) < 1 || !args[0].IsString() {
|
||||||
|
return bridge.JsException(v8ctx, "SendGroupEnd requires a group ID (string) as first argument")
|
||||||
|
}
|
||||||
|
groupID := args[0].String()
|
||||||
|
|
||||||
|
// Optional chunk count
|
||||||
|
chunkCount := 0
|
||||||
|
if len(args) > 1 && args[1].IsNumber() {
|
||||||
|
chunkCount = int(args[1].Integer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send group_end event
|
||||||
|
endEvent := output.NewEventMessage(
|
||||||
|
message.EventGroupEnd,
|
||||||
|
"Group completed",
|
||||||
|
message.GroupEndData{
|
||||||
|
GroupID: groupID,
|
||||||
|
Type: "mixed",
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
DurationMs: 0, // Duration not tracked at this level
|
||||||
|
ChunkCount: chunkCount,
|
||||||
|
Status: "completed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err := ctx.Send(endEvent); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Failed to send group_end event: "+err.Error())
|
||||||
|
}
|
||||||
|
if err := ctx.Flush(); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, "Flush failed after group_end: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return v8go.Undefined(iso)
|
return v8go.Undefined(iso)
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ func parseMessage(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Message, e
|
||||||
if delta, ok := msgMap["delta"].(bool); ok {
|
if delta, ok := msgMap["delta"].(bool); ok {
|
||||||
msg.Delta = delta
|
msg.Delta = delta
|
||||||
}
|
}
|
||||||
if done, ok := msgMap["done"].(bool); ok {
|
|
||||||
msg.Done = done
|
|
||||||
}
|
|
||||||
if deltaPath, ok := msgMap["delta_path"].(string); ok {
|
if deltaPath, ok := msgMap["delta_path"].(string); ok {
|
||||||
msg.DeltaPath = deltaPath
|
msg.DeltaPath = deltaPath
|
||||||
}
|
}
|
||||||
|
|
@ -73,12 +70,6 @@ func parseMessage(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Message, e
|
||||||
if groupID, ok := msgMap["group_id"].(string); ok {
|
if groupID, ok := msgMap["group_id"].(string); ok {
|
||||||
msg.GroupID = groupID
|
msg.GroupID = groupID
|
||||||
}
|
}
|
||||||
if groupStart, ok := msgMap["group_start"].(bool); ok {
|
|
||||||
msg.GroupStart = groupStart
|
|
||||||
}
|
|
||||||
if groupEnd, ok := msgMap["group_end"].(bool); ok {
|
|
||||||
msg.GroupEnd = groupEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata (optional)
|
// Metadata (optional)
|
||||||
if metadataMap, ok := msgMap["metadata"].(map[string]interface{}); ok {
|
if metadataMap, ok := msgMap["metadata"].(map[string]interface{}); ok {
|
||||||
|
|
@ -158,9 +149,6 @@ func parseGroup(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Group, error
|
||||||
if delta, ok := msgMap["delta"].(bool); ok {
|
if delta, ok := msgMap["delta"].(bool); ok {
|
||||||
msg.Delta = delta
|
msg.Delta = delta
|
||||||
}
|
}
|
||||||
if done, ok := msgMap["done"].(bool); ok {
|
|
||||||
msg.Done = done
|
|
||||||
}
|
|
||||||
if deltaPath, ok := msgMap["delta_path"].(string); ok {
|
if deltaPath, ok := msgMap["delta_path"].(string); ok {
|
||||||
msg.DeltaPath = deltaPath
|
msg.DeltaPath = deltaPath
|
||||||
}
|
}
|
||||||
|
|
@ -173,12 +161,6 @@ func parseGroup(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Group, error
|
||||||
if groupID, ok := msgMap["group_id"].(string); ok {
|
if groupID, ok := msgMap["group_id"].(string); ok {
|
||||||
msg.GroupID = groupID
|
msg.GroupID = groupID
|
||||||
}
|
}
|
||||||
if groupStart, ok := msgMap["group_start"].(bool); ok {
|
|
||||||
msg.GroupStart = groupStart
|
|
||||||
}
|
|
||||||
if groupEnd, ok := msgMap["group_end"].(bool); ok {
|
|
||||||
msg.GroupEnd = groupEnd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata (optional)
|
// Metadata (optional)
|
||||||
if metadataMap, ok := msgMap["metadata"].(map[string]interface{}); ok {
|
if metadataMap, ok := msgMap["metadata"].(map[string]interface{}); ok {
|
||||||
|
|
|
||||||
|
|
@ -163,8 +163,8 @@ func TestJsValueSendGroup(t *testing.T) {
|
||||||
assert.Equal(t, true, result["success"], "SendGroup should succeed")
|
assert.Equal(t, true, result["success"], "SendGroup should succeed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestJsValueFlush test the Flush method on Context
|
// TestJsValueSendGroupStartEnd test the SendGroupStart and SendGroupEnd methods
|
||||||
func TestJsValueFlush(t *testing.T) {
|
func TestJsValueSendGroupStartEnd(t *testing.T) {
|
||||||
|
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
@ -181,13 +181,17 @@ func TestJsValueFlush(t *testing.T) {
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
try {
|
try {
|
||||||
// Send a message
|
// Start a group with auto-generated ID
|
||||||
ctx.Send("Processing...");
|
const groupId = ctx.SendGroupStart("text");
|
||||||
|
|
||||||
// Flush output
|
// Send messages in the group
|
||||||
ctx.Flush();
|
ctx.Send({ type: "text", props: { content: "Message 1" }, group_id: groupId });
|
||||||
|
ctx.Send({ type: "text", props: { content: "Message 2" }, group_id: groupId });
|
||||||
|
|
||||||
return { success: true };
|
// End the group
|
||||||
|
ctx.SendGroupEnd(groupId, 2);
|
||||||
|
|
||||||
|
return { success: true, groupId: groupId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +204,8 @@ func TestJsValueFlush(t *testing.T) {
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Expected map result, got %T", res)
|
t.Fatalf("Expected map result, got %T", res)
|
||||||
}
|
}
|
||||||
assert.Equal(t, true, result["success"], "Flush should succeed")
|
assert.Equal(t, true, result["success"], "SendGroupStart/End should succeed")
|
||||||
|
assert.NotEmpty(t, result["groupId"], "Should return group ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestJsValueSendDeltaUpdates test delta updates in Send
|
// TestJsValueSendDeltaUpdates test delta updates in Send
|
||||||
|
|
@ -239,13 +244,7 @@ func TestJsValueSendDeltaUpdates(t *testing.T) {
|
||||||
delta_action: "append"
|
delta_action: "append"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mark as complete
|
// Send completion (no done field needed)
|
||||||
ctx.Send({
|
|
||||||
type: "text",
|
|
||||||
props: {},
|
|
||||||
id: "msg_1",
|
|
||||||
done: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -329,8 +328,6 @@ func TestJsValueSendMultipleTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
|
|
@ -467,7 +464,6 @@ func TestJsValueSendWithCUIAccept(t *testing.T) {
|
||||||
type: "text",
|
type: "text",
|
||||||
props: { content: "Hello CUI" }
|
props: { content: "Hello CUI" }
|
||||||
});
|
});
|
||||||
ctx.Flush();
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
|
|
@ -566,15 +562,11 @@ func TestJsValueSendChainedCalls(t *testing.T) {
|
||||||
res, err := v8.Call(v8.CallOptions{}, `
|
res, err := v8.Call(v8.CallOptions{}, `
|
||||||
function test(ctx) {
|
function test(ctx) {
|
||||||
try {
|
try {
|
||||||
// Multiple sequential sends
|
// Multiple sequential sends (each auto-flushes)
|
||||||
ctx.Send("Step 1");
|
ctx.Send("Step 1");
|
||||||
ctx.Send("Step 2");
|
ctx.Send("Step 2");
|
||||||
ctx.Send("Step 3");
|
ctx.Send("Step 3");
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
// Send after flush
|
|
||||||
ctx.Send("Step 4");
|
ctx.Send("Step 4");
|
||||||
ctx.Flush();
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ type Message struct {
|
||||||
// Streaming control
|
// Streaming control
|
||||||
ID string `json:"id,omitempty"` // Unique message ID (for merging in streaming)
|
ID string `json:"id,omitempty"` // Unique message ID (for merging in streaming)
|
||||||
Delta bool `json:"delta,omitempty"` // Whether this is an incremental update
|
Delta bool `json:"delta,omitempty"` // Whether this is an incremental update
|
||||||
Done bool `json:"done,omitempty"` // Whether the message is complete
|
|
||||||
|
|
||||||
// Delta update control (for incremental props updates)
|
// Delta update control (for incremental props updates)
|
||||||
DeltaPath string `json:"delta_path,omitempty"` // Which field to update (e.g., "content", "items.0.name")
|
DeltaPath string `json:"delta_path,omitempty"` // Which field to update (e.g., "content", "items.0.name")
|
||||||
|
|
@ -85,11 +84,7 @@ type Message struct {
|
||||||
- `true`: Append/update to existing message with same ID
|
- `true`: Append/update to existing message with same ID
|
||||||
- `false`: Complete message (default)
|
- `false`: Complete message (default)
|
||||||
- Used for streaming LLM responses
|
- Used for streaming LLM responses
|
||||||
|
- Message completion is signaled via `group_end` event instead
|
||||||
- **`Done`** (optional): Marks message as complete
|
|
||||||
- `true`: No more updates will come for this message ID
|
|
||||||
- `false`: More updates may follow
|
|
||||||
- Typically sent as final message in a delta sequence
|
|
||||||
|
|
||||||
#### Delta Update Control
|
#### Delta Update Control
|
||||||
|
|
||||||
|
|
@ -379,11 +374,22 @@ msg := &message.Message{
|
||||||
}
|
}
|
||||||
output.Send(ctx, msg)
|
output.Send(ctx, msg)
|
||||||
|
|
||||||
// Mark as complete
|
// Send more delta updates...
|
||||||
msg.Delta = false
|
msg.Props["content"] = " world"
|
||||||
msg.Done = true
|
|
||||||
msg.Props["content"] = "Hello world!" // Full content
|
|
||||||
output.Send(ctx, msg)
|
output.Send(ctx, msg)
|
||||||
|
|
||||||
|
// Mark completion with group_end event
|
||||||
|
endData := message.GroupEndData{
|
||||||
|
GroupID: "msg_123",
|
||||||
|
Type: "text",
|
||||||
|
Status: "completed",
|
||||||
|
ChunkCount: 2,
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"content": "Hello world!", // Full content
|
||||||
|
},
|
||||||
|
}
|
||||||
|
eventMsg := output.NewEventMessage(message.EventGroupEnd, "Group completed", endData)
|
||||||
|
output.Send(ctx, eventMsg)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Writers
|
### Custom Writers
|
||||||
|
|
|
||||||
|
|
@ -90,13 +90,6 @@ func (a *Adapter) Adapt(msg *message.Message) ([]interface{}, error) {
|
||||||
return []interface{}{}, nil // Return empty array, nothing to send
|
return []interface{}{}, nil // Return empty array, nothing to send
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip "done" messages that are not delta updates
|
|
||||||
// These are final confirmation messages for CUI clients
|
|
||||||
// OpenAI clients don't need them - they use finish_reason instead
|
|
||||||
if msg.Done && !msg.Delta {
|
|
||||||
return []interface{}{}, nil // Return empty array, nothing to send
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get converter for this message type
|
// Get converter for this message type
|
||||||
converter, exists := a.registry.GetConverter(msg.Type)
|
converter, exists := a.registry.GetConverter(msg.Type)
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,8 @@ type Message struct {
|
||||||
Props map[string]interface{} `json:"props,omitempty"` // Message properties (passed to frontend component)
|
Props map[string]interface{} `json:"props,omitempty"` // Message properties (passed to frontend component)
|
||||||
|
|
||||||
// Streaming control
|
// Streaming control
|
||||||
ID string `json:"id,omitempty"` // Message ID (used for merging messages in streaming scenarios)
|
ID string `json:"id,omitempty"` // Unique chunk/message ID (each chunk has unique ID; use group_id for merging)
|
||||||
Delta bool `json:"delta,omitempty"` // Whether this is an incremental update
|
Delta bool `json:"delta,omitempty"` // Whether this is an incremental update
|
||||||
Done bool `json:"done,omitempty"` // Whether the message is complete
|
|
||||||
|
|
||||||
// Delta update control
|
// Delta update control
|
||||||
DeltaPath string `json:"delta_path,omitempty"` // Update path (e.g., "content", "data", "items.0.name")
|
DeltaPath string `json:"delta_path,omitempty"` // Update path (e.g., "content", "data", "items.0.name")
|
||||||
|
|
@ -49,9 +48,7 @@ type Message struct {
|
||||||
TypeChange bool `json:"type_change,omitempty"` // Marks this as a type correction message
|
TypeChange bool `json:"type_change,omitempty"` // Marks this as a type correction message
|
||||||
|
|
||||||
// Message group
|
// Message group
|
||||||
GroupID string `json:"group_id,omitempty"` // Parent message group ID
|
GroupID string `json:"group_id,omitempty"` // Group ID (all delta chunks of same logical message share this; used for merging)
|
||||||
GroupStart bool `json:"group_start,omitempty"` // Marks the start of a message group
|
|
||||||
GroupEnd bool `json:"group_end,omitempty"` // Marks the end of a message group
|
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
Metadata *Metadata `json:"metadata,omitempty"` // Additional metadata
|
Metadata *Metadata `json:"metadata,omitempty"` // Additional metadata
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue