Merge pull request #1345 from trheyi/main
Register jsapi output agent in main.go for enhanced output handling
This commit is contained in:
commit
12be3bfbc4
7 changed files with 1413 additions and 34 deletions
|
|
@ -8,6 +8,7 @@ Defined in `types.go`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
const (
|
const (
|
||||||
|
TypeUserInput = "user_input" // User input message (frontend display only)
|
||||||
TypeText = "text" // Plain text or Markdown content
|
TypeText = "text" // Plain text or Markdown content
|
||||||
TypeThinking = "thinking" // Reasoning/thinking process
|
TypeThinking = "thinking" // Reasoning/thinking process
|
||||||
TypeLoading = "loading" // Loading/processing indicator
|
TypeLoading = "loading" // Loading/processing indicator
|
||||||
|
|
@ -23,9 +24,100 @@ const (
|
||||||
|
|
||||||
## Standard Props Structures
|
## Standard Props Structures
|
||||||
|
|
||||||
### 1. Text (`text`)
|
### 1. User Input (`user_input`)
|
||||||
|
|
||||||
**Purpose:** Plain text or Markdown content
|
**Purpose:** User input message (for frontend display only)
|
||||||
|
|
||||||
|
**Props Structure:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
type UserInputProps struct {
|
||||||
|
Content interface{} `json:"content"` // User input (text string or multimodal ContentPart[])
|
||||||
|
Role string `json:"role,omitempty"` // User role: "user", "system", "developer" (default: "user")
|
||||||
|
Name string `json:"name,omitempty"` // Optional participant name
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "user_input",
|
||||||
|
"props": {
|
||||||
|
"content": "Hello, can you help me?",
|
||||||
|
"role": "user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multimodal Example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "user_input",
|
||||||
|
"props": {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "What's in this image?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": "https://example.com/photo.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"role": "user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Helper:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Simple text input
|
||||||
|
msg := output.NewUserInputMessage("Hello, can you help me?", "user", "")
|
||||||
|
|
||||||
|
// With name
|
||||||
|
msg := output.NewUserInputMessage("I need assistance", "user", "John")
|
||||||
|
|
||||||
|
// Multimodal content
|
||||||
|
content := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "What's in this image?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": map[string]string{
|
||||||
|
"url": "https://example.com/photo.jpg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msg := output.NewUserInputMessage(content, "user", "")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
|
||||||
|
- **Frontend display only**: This type is used by the frontend to display user input in the chat UI
|
||||||
|
- **Not sent to backend**: User input is sent to backend as `UserMessage` (OpenAI format), not as `Message`
|
||||||
|
- **Preserves role**: Unlike `text` type, preserves the original user role (`user`, `system`, `developer`)
|
||||||
|
- **Supports multimodal**: Can contain text, images, audio, or files
|
||||||
|
|
||||||
|
**Data Flow:**
|
||||||
|
|
||||||
|
```
|
||||||
|
User types → UserMessage (sent to API) → Backend processes → Message types (AI response)
|
||||||
|
↓
|
||||||
|
UserInputMessage (frontend display)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Text (`text`)
|
||||||
|
|
||||||
|
**Purpose:** Plain text or Markdown content (AI responses)
|
||||||
|
|
||||||
**Props Structure:**
|
**Props Structure:**
|
||||||
|
|
||||||
|
|
@ -54,7 +146,7 @@ msg := output.NewTextMessage("Hello **world**!")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Thinking (`thinking`)
|
### 3. Thinking (`thinking`)
|
||||||
|
|
||||||
**Purpose:** Reasoning or thinking process (used by o1 models, DeepSeek R1, etc.)
|
**Purpose:** Reasoning or thinking process (used by o1 models, DeepSeek R1, etc.)
|
||||||
|
|
||||||
|
|
@ -85,7 +177,7 @@ msg := output.NewThinkingMessage("Let me analyze this step by step...")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Loading (`loading`)
|
### 4. Loading (`loading`)
|
||||||
|
|
||||||
**Purpose:** Loading or processing indicator (preprocessing, knowledge base search, data fetching, etc.)
|
**Purpose:** Loading or processing indicator (preprocessing, knowledge base search, data fetching, etc.)
|
||||||
|
|
||||||
|
|
@ -150,7 +242,7 @@ func Create(ctx *context.Context, messages []context.Message) (*context.HookCrea
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Tool Call (`tool_call`)
|
### 5. Tool Call (`tool_call`)
|
||||||
|
|
||||||
**Purpose:** LLM tool or function call
|
**Purpose:** LLM tool or function call
|
||||||
|
|
||||||
|
|
@ -189,7 +281,7 @@ msg := output.NewToolCallMessage(
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Error (`error`)
|
### 6. Error (`error`)
|
||||||
|
|
||||||
**Purpose:** Error message
|
**Purpose:** Error message
|
||||||
|
|
||||||
|
|
@ -224,7 +316,7 @@ msg := output.NewErrorMessage("Connection timeout", "TIMEOUT")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Action (`action`)
|
### 7. Action (`action`)
|
||||||
|
|
||||||
**Purpose:** System-level action/command (not displayed to user, only processed by client)
|
**Purpose:** System-level action/command (not displayed to user, only processed by client)
|
||||||
|
|
||||||
|
|
@ -295,7 +387,7 @@ output.Send(ctx, output.NewTextMessage("I've opened the user details panel for y
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 7. Event (`event`)
|
### 8. Event (`event`)
|
||||||
|
|
||||||
**Purpose:** Lifecycle event messages (stream_start, stream_end, connecting, etc.)
|
**Purpose:** Lifecycle event messages (stream_start, stream_end, connecting, etc.)
|
||||||
|
|
||||||
|
|
@ -383,7 +475,7 @@ output.Send(ctx, output.NewEventMessage("stream_end", "Stream completed", map[st
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 8. Image (`image`)
|
### 9. Image (`image`)
|
||||||
|
|
||||||
**Purpose:** Image content
|
**Purpose:** Image content
|
||||||
|
|
||||||
|
|
@ -426,7 +518,7 @@ msg := output.NewImageMessage("https://example.com/avatar.jpg", "User avatar")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 9. Audio (`audio`)
|
### 10. Audio (`audio`)
|
||||||
|
|
||||||
**Purpose:** Audio content
|
**Purpose:** Audio content
|
||||||
|
|
||||||
|
|
@ -471,7 +563,7 @@ msg := output.NewAudioMessage("https://example.com/audio.mp3", "mp3")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. Video (`video`)
|
### 11. Video (`video`)
|
||||||
|
|
||||||
**Purpose:** Video content
|
**Purpose:** Video content
|
||||||
|
|
||||||
|
|
@ -542,7 +634,8 @@ CUI adapter passes built-in types through without transformation:
|
||||||
OpenAI adapter converts built-in types to OpenAI format:
|
OpenAI adapter converts built-in types to OpenAI format:
|
||||||
|
|
||||||
| Type | OpenAI Format | Field | Note |
|
| Type | OpenAI Format | Field | Note |
|
||||||
| ----------- | ------------------------- | ----------------------------- | -------------------------------------------------------------------- |
|
| ------------ | ------------------------- | ----------------------------- | -------------------------------------------------------------------- |
|
||||||
|
| `user_input` | (not sent) | - | Frontend display only - not sent to OpenAI clients |
|
||||||
| `text` | `delta.content` | `props.content` | |
|
| `text` | `delta.content` | `props.content` | |
|
||||||
| `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) |
|
| `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) |
|
||||||
| `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients |
|
| `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients |
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,24 @@ func init() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewUserInputMessage creates a user input message (for frontend display)
|
||||||
|
// content can be string or []ContentPart for multimodal content
|
||||||
|
func NewUserInputMessage(content interface{}, role, name string) *message.Message {
|
||||||
|
props := map[string]interface{}{
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
if role != "" {
|
||||||
|
props["role"] = role
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
props["name"] = name
|
||||||
|
}
|
||||||
|
return &message.Message{
|
||||||
|
Type: message.TypeUserInput,
|
||||||
|
Props: props,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewTextMessage creates a text message
|
// NewTextMessage creates a text message
|
||||||
func NewTextMessage(content string) *message.Message {
|
func NewTextMessage(content string) *message.Message {
|
||||||
return &message.Message{
|
return &message.Message{
|
||||||
|
|
@ -125,7 +143,7 @@ func NewVideoMessage(url string) *message.Message {
|
||||||
// IsBuiltinType checks if a message type is a built-in type
|
// IsBuiltinType checks if a message type is a built-in type
|
||||||
func IsBuiltinType(msgType string) bool {
|
func IsBuiltinType(msgType string) bool {
|
||||||
switch msgType {
|
switch msgType {
|
||||||
case message.TypeText, message.TypeThinking, message.TypeLoading, message.TypeToolCall, message.TypeError, message.TypeImage, message.TypeAudio, message.TypeVideo, message.TypeAction, message.TypeEvent:
|
case message.TypeUserInput, message.TypeText, message.TypeThinking, message.TypeLoading, message.TypeToolCall, message.TypeError, message.TypeImage, message.TypeAudio, message.TypeVideo, message.TypeAction, message.TypeEvent:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
482
agent/output/jsapi/README.md
Normal file
482
agent/output/jsapi/README.md
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
# Output JSAPI
|
||||||
|
|
||||||
|
The Output JSAPI provides a JavaScript interface for sending output messages to clients from scripts (e.g., hooks, processes). It wraps the Go `output` package functionality and provides a convenient API for sending messages and message groups.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Output object allows you to:
|
||||||
|
|
||||||
|
- Send individual messages to clients in various formats (text, error, loading, etc.)
|
||||||
|
- Send groups of related messages
|
||||||
|
- Support streaming with delta updates
|
||||||
|
- Handle different message types with custom properties
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
### `new Output(ctx)`
|
||||||
|
|
||||||
|
Creates a new Output instance.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `ctx` (Context): The agent context object
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- Output instance
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function Create(ctx, messages) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
// Use output methods...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### `Send(message)`
|
||||||
|
|
||||||
|
Sends a single message to the client.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `message` (string | object): The message to send
|
||||||
|
- If string: Automatically converted to a text message
|
||||||
|
- If object: Must have a `type` field and optional `props` and other fields
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- Output instance (for chaining)
|
||||||
|
|
||||||
|
**Message Object Structure:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: string, // Required: Message type (e.g., "text", "error", "loading")
|
||||||
|
props: object, // Optional: Message properties (type-specific)
|
||||||
|
id: string, // Optional: Message ID (for streaming)
|
||||||
|
delta: boolean, // Optional: Whether this is a delta update
|
||||||
|
done: boolean, // Optional: Whether the message is complete
|
||||||
|
delta_path: string, // Optional: Path for delta updates (e.g., "content")
|
||||||
|
delta_action: string, // Optional: Delta action ("append", "replace", "merge", "set")
|
||||||
|
type_change: boolean, // Optional: Whether this is a type correction
|
||||||
|
group_id: string, // Optional: Parent message group ID
|
||||||
|
group_start: boolean, // Optional: Marks the start of a message group
|
||||||
|
group_end: boolean, // Optional: Marks the end of a message group
|
||||||
|
metadata: { // Optional: Message metadata
|
||||||
|
timestamp: number,
|
||||||
|
sequence: number,
|
||||||
|
trace_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
Send a simple text message (shorthand):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output.Send("Hello, world!");
|
||||||
|
```
|
||||||
|
|
||||||
|
Send a text message (full):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
props: {
|
||||||
|
content: "Hello, world!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Send an error message:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output.Send({
|
||||||
|
type: "error",
|
||||||
|
props: {
|
||||||
|
message: "Something went wrong",
|
||||||
|
code: "ERR_001",
|
||||||
|
details: "Additional error details",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Send a loading indicator:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output.Send({
|
||||||
|
type: "loading",
|
||||||
|
props: {
|
||||||
|
message: "Searching knowledge base...",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Send streaming text with delta updates:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// First chunk
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
id: "msg-1",
|
||||||
|
props: { content: "Hello" },
|
||||||
|
delta: true,
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subsequent chunks
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
id: "msg-1",
|
||||||
|
props: { content: " world" },
|
||||||
|
delta: true,
|
||||||
|
delta_path: "content",
|
||||||
|
delta_action: "append",
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Final chunk
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
id: "msg-1",
|
||||||
|
props: { content: "!" },
|
||||||
|
delta: true,
|
||||||
|
delta_path: "content",
|
||||||
|
delta_action: "append",
|
||||||
|
done: true,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Chain multiple sends:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output
|
||||||
|
.Send("First message")
|
||||||
|
.Send("Second message")
|
||||||
|
.Send({ type: "loading", props: { message: "Processing..." } });
|
||||||
|
```
|
||||||
|
|
||||||
|
### `SendGroup(group)`
|
||||||
|
|
||||||
|
Sends a group of related messages.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
- `group` (object): The message group
|
||||||
|
- `id` (string): Required - Group ID
|
||||||
|
- `messages` (array): Required - Array of message objects
|
||||||
|
- `metadata` (object): Optional - Group metadata
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
|
||||||
|
- Output instance (for chaining)
|
||||||
|
|
||||||
|
**Group Object Structure:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: string, // Required: Message group ID
|
||||||
|
messages: [ // Required: Array of messages
|
||||||
|
{
|
||||||
|
type: string,
|
||||||
|
props: object,
|
||||||
|
// ... other message fields
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: { // Optional: Group metadata
|
||||||
|
timestamp: number,
|
||||||
|
sequence: number,
|
||||||
|
trace_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
Send a simple message group:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output.SendGroup({
|
||||||
|
id: "search-results",
|
||||||
|
messages: [
|
||||||
|
{ type: "text", props: { content: "Found 3 results:" } },
|
||||||
|
{ type: "text", props: { content: "Result 1" } },
|
||||||
|
{ type: "text", props: { content: "Result 2" } },
|
||||||
|
{ type: "text", props: { content: "Result 3" } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Send a group with metadata:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
output.SendGroup({
|
||||||
|
id: "analysis-group",
|
||||||
|
messages: [
|
||||||
|
{ type: "loading", props: { message: "Analyzing data..." } },
|
||||||
|
{ type: "text", props: { content: "Analysis complete" } },
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
sequence: 1,
|
||||||
|
trace_id: "trace-123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Message Types
|
||||||
|
|
||||||
|
The Output JSAPI supports all built-in message types defined in the output package:
|
||||||
|
|
||||||
|
### User Interaction Types
|
||||||
|
|
||||||
|
- **`user_input`**: User input message (frontend display only)
|
||||||
|
```javascript
|
||||||
|
{ type: "user_input", props: { content: "User's message", role: "user" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Types
|
||||||
|
|
||||||
|
- **`text`**: Plain text or Markdown content
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{ type: "text", props: { content: "Hello **world**" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`thinking`**: Reasoning/thinking process (e.g., o1 models)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{ type: "thinking", props: { content: "Let me think about this..." } }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`loading`**: Loading/processing indicator
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{ type: "loading", props: { message: "Processing..." } }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`tool_call`**: LLM tool/function call
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "tool_call",
|
||||||
|
props: {
|
||||||
|
id: "call_123",
|
||||||
|
name: "search",
|
||||||
|
arguments: "{\"query\":\"test\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`error`**: Error message
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
props: {
|
||||||
|
message: "Error occurred",
|
||||||
|
code: "ERR_001",
|
||||||
|
details: "More info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Media Types
|
||||||
|
|
||||||
|
- **`image`**: Image content
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "image",
|
||||||
|
props: {
|
||||||
|
url: "https://example.com/image.jpg",
|
||||||
|
alt: "Description",
|
||||||
|
width: 800,
|
||||||
|
height: 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`audio`**: Audio content
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "audio",
|
||||||
|
props: {
|
||||||
|
url: "https://example.com/audio.mp3",
|
||||||
|
format: "mp3",
|
||||||
|
duration: 120.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`video`**: Video content
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "video",
|
||||||
|
props: {
|
||||||
|
url: "https://example.com/video.mp4",
|
||||||
|
format: "mp4",
|
||||||
|
duration: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Types
|
||||||
|
|
||||||
|
- **`action`**: System action (silent in OpenAI clients)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "action",
|
||||||
|
props: {
|
||||||
|
name: "open_panel",
|
||||||
|
payload: { panel_id: "settings" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`event`**: Lifecycle event (CUI only, silent in OpenAI clients)
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: "event",
|
||||||
|
props: {
|
||||||
|
event: "stream_start",
|
||||||
|
message: "Starting stream..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Hooks
|
||||||
|
|
||||||
|
### Create Hook Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Create hook - Called before sending messages to the LLM
|
||||||
|
* @param {Context} ctx - Agent context
|
||||||
|
* @param {Array} messages - User messages
|
||||||
|
* @returns {Object} Hook response
|
||||||
|
*/
|
||||||
|
function Create(ctx, messages) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
|
||||||
|
// Send a loading indicator
|
||||||
|
output.Send({
|
||||||
|
type: "loading",
|
||||||
|
props: { message: "Processing your request..." },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send custom messages to the user
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "I'm thinking about your question..." },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return hook response
|
||||||
|
return {
|
||||||
|
messages: messages,
|
||||||
|
temperature: 0.7,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Done Hook Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Done hook - Called after assistant completes response
|
||||||
|
* @param {Context} ctx - Agent context
|
||||||
|
* @param {Array} messages - Conversation messages
|
||||||
|
* @param {Object} response - Assistant response
|
||||||
|
*/
|
||||||
|
function Done(ctx, messages, response) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
|
||||||
|
// Send a completion message
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "Response complete!" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send an action
|
||||||
|
output.Send({
|
||||||
|
type: "action",
|
||||||
|
props: {
|
||||||
|
name: "save_conversation",
|
||||||
|
payload: { chat_id: ctx.chat_id },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Progress Updates Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function ProcessData(ctx, data) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
|
||||||
|
// Show progress
|
||||||
|
const steps = ["Loading", "Processing", "Analyzing", "Complete"];
|
||||||
|
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
output.Send({
|
||||||
|
type: "loading",
|
||||||
|
props: {
|
||||||
|
message: `${steps[i]}... (${i + 1}/${steps.length})`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do some work...
|
||||||
|
processStep(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send final result
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "All done!" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The Output JSAPI throws exceptions for invalid parameters:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
|
||||||
|
// This will throw: message.type is required
|
||||||
|
output.Send({ props: { content: "test" } });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Output error:", e.toString());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common errors:
|
||||||
|
|
||||||
|
- `"Output constructor requires a context argument"` - Missing ctx parameter
|
||||||
|
- `"Send requires a message argument"` - Missing message parameter
|
||||||
|
- `"message.type is required and must be a string"` - Missing or invalid type field
|
||||||
|
- `"SendGroup requires a group argument"` - Missing group parameter
|
||||||
|
- `"group.id is required and must be a string"` - Missing group ID
|
||||||
|
- `"group.messages is required and must be an array"` - Missing or invalid messages array
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **Context Requirement**: The Output object must be created with a valid agent context
|
||||||
|
2. **Writer Required**: The context must have a Writer set (automatically handled in API requests)
|
||||||
|
3. **Message Format**: Messages are automatically adapted based on the context's Accept type (standard, cui-web, cui-native, cui-desktop)
|
||||||
|
4. **Streaming**: For streaming responses, use delta updates with proper message IDs
|
||||||
|
5. **Method Chaining**: All methods return the Output instance for convenient chaining
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Output Package Documentation](../README.md)
|
||||||
|
- [Message Types](../BUILTIN_TYPES.md)
|
||||||
|
- [Agent Context](../../context/README.md)
|
||||||
|
- [Hook System](../../assistant/hook/README.md)
|
||||||
412
agent/output/jsapi/output.go
Normal file
412
agent/output/jsapi/output.go
Normal file
|
|
@ -0,0 +1,412 @@
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
|
"github.com/yaoapp/gou/runtime/v8/bridge"
|
||||||
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/output"
|
||||||
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Auto-register Output JavaScript API when package is imported
|
||||||
|
v8.RegisterFunction("Output", ExportFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage from JavaScript:
|
||||||
|
//
|
||||||
|
// const output = new Output(ctx)
|
||||||
|
// output.Send({ type: "text", props: { content: "Hello" } })
|
||||||
|
// output.Send("Hello") // shorthand for text message
|
||||||
|
// output.SendGroup({ id: "group1", messages: [...] })
|
||||||
|
//
|
||||||
|
// Objects:
|
||||||
|
// - Output: Output manager (constructor)
|
||||||
|
|
||||||
|
// ExportFunction exports the Output constructor function template
|
||||||
|
// This is used by v8.RegisterFunction
|
||||||
|
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, outputConstructor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputConstructor is the JavaScript constructor for Output
|
||||||
|
// Usage: new Output(ctx)
|
||||||
|
func outputConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
v8ctx := info.Context()
|
||||||
|
args := info.Args()
|
||||||
|
|
||||||
|
// Require ctx argument
|
||||||
|
if len(args) < 1 {
|
||||||
|
return bridge.JsException(v8ctx, "Output constructor requires a context argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the context object from JavaScript
|
||||||
|
ctxObj, err := args[0].AsObject()
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(v8ctx, fmt.Sprintf("context must be an object: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the goValueID from internal field (index 0)
|
||||||
|
if ctxObj.InternalFieldCount() < 1 {
|
||||||
|
return bridge.JsException(v8ctx, "context object is missing internal fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
goValueIDValue := ctxObj.GetInternalField(0)
|
||||||
|
if goValueIDValue == nil || !goValueIDValue.IsString() {
|
||||||
|
return bridge.JsException(v8ctx, "context object is missing goValueID")
|
||||||
|
}
|
||||||
|
|
||||||
|
goValueID := goValueIDValue.String()
|
||||||
|
|
||||||
|
// Retrieve the Go context object from bridge registry
|
||||||
|
goObj := bridge.GetGoObject(goValueID)
|
||||||
|
if goObj == nil {
|
||||||
|
return bridge.JsException(v8ctx, "context object not found in registry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assert to *agentContext.Context
|
||||||
|
ctx, ok := goObj.(*agentContext.Context)
|
||||||
|
if !ok {
|
||||||
|
return bridge.JsException(v8ctx, fmt.Sprintf("object is not a Context, got %T", goObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output object
|
||||||
|
outputObj, err := NewOutputObject(v8ctx, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(v8ctx, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputObj
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOutputObject creates a JavaScript Output object
|
||||||
|
func NewOutputObject(v8ctx *v8go.Context, ctx *agentContext.Context) (*v8go.Value, error) {
|
||||||
|
jsObject := v8go.NewObjectTemplate(v8ctx.Isolate())
|
||||||
|
|
||||||
|
// Set internal field count to 1 to store the __go_id
|
||||||
|
// Internal fields are not accessible from JavaScript, providing better security
|
||||||
|
jsObject.SetInternalFieldCount(1)
|
||||||
|
|
||||||
|
// Register context in global bridge registry for efficient Go object retrieval
|
||||||
|
// The goValueID will be stored in internal field (index 0) after instance creation
|
||||||
|
goValueID := bridge.RegisterGoObject(ctx)
|
||||||
|
|
||||||
|
// Set methods
|
||||||
|
jsObject.Set("Send", outputSendMethod(v8ctx.Isolate(), ctx))
|
||||||
|
jsObject.Set("SendGroup", outputSendGroupMethod(v8ctx.Isolate(), ctx))
|
||||||
|
|
||||||
|
// Set release function that will be called when JavaScript object is released
|
||||||
|
jsObject.Set("__release", outputGoRelease(v8ctx.Isolate()))
|
||||||
|
|
||||||
|
// Create instance
|
||||||
|
instance, err := jsObject.NewInstance(v8ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Clean up: release from global registry if instance creation failed
|
||||||
|
bridge.ReleaseGoObject(goValueID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the goValueID in internal field (index 0)
|
||||||
|
// This is not accessible from JavaScript, providing better security
|
||||||
|
obj, err := instance.Value.AsObject()
|
||||||
|
if err != nil {
|
||||||
|
bridge.ReleaseGoObject(goValueID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = obj.SetInternalField(0, goValueID)
|
||||||
|
if err != nil {
|
||||||
|
bridge.ReleaseGoObject(goValueID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputGoRelease releases the Go object from the global bridge registry
|
||||||
|
// It retrieves the goValueID from internal field (index 0) and releases the Go object
|
||||||
|
func outputGoRelease(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
// Get the output object (this)
|
||||||
|
thisObj, err := info.This().AsObject()
|
||||||
|
if err == nil && thisObj.InternalFieldCount() > 0 {
|
||||||
|
// Get goValueID from internal field (index 0)
|
||||||
|
goValueIDValue := thisObj.GetInternalField(0)
|
||||||
|
if goValueIDValue != nil && goValueIDValue.IsString() {
|
||||||
|
goValueID := goValueIDValue.String()
|
||||||
|
// Release from global bridge registry
|
||||||
|
bridge.ReleaseGoObject(goValueID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v8go.Undefined(info.Context().Isolate())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputSendMethod implements the Send method
|
||||||
|
// Usage: output.Send(message)
|
||||||
|
// message can be an object with { type: string, props: object, ... } or a simple string (will be converted to text message)
|
||||||
|
func outputSendMethod(iso *v8go.Isolate, ctx *agentContext.Context) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
v8ctx := info.Context()
|
||||||
|
args := info.Args()
|
||||||
|
|
||||||
|
if len(args) < 1 {
|
||||||
|
return bridge.JsException(v8ctx, "Send requires a message argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse message argument
|
||||||
|
msg, err := parseMessage(v8ctx, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(v8ctx, fmt.Sprintf("invalid message: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call output.Send
|
||||||
|
if err := output.Send(ctx, msg); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, fmt.Sprintf("Send failed: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.This().Value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputSendGroupMethod implements the SendGroup method
|
||||||
|
// Usage: output.SendGroup(group)
|
||||||
|
// group must be an object with { id: string, messages: [], ... }
|
||||||
|
func outputSendGroupMethod(iso *v8go.Isolate, ctx *agentContext.Context) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
v8ctx := info.Context()
|
||||||
|
args := info.Args()
|
||||||
|
|
||||||
|
if len(args) < 1 {
|
||||||
|
return bridge.JsException(v8ctx, "SendGroup requires a group argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse group argument
|
||||||
|
group, err := parseGroup(v8ctx, args[0])
|
||||||
|
if err != nil {
|
||||||
|
return bridge.JsException(v8ctx, fmt.Sprintf("invalid group: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call output.SendGroup
|
||||||
|
if err := output.SendGroup(ctx, group); err != nil {
|
||||||
|
return bridge.JsException(v8ctx, fmt.Sprintf("SendGroup failed: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.This().Value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMessage parses a JavaScript value into a message.Message
|
||||||
|
func parseMessage(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Message, error) {
|
||||||
|
// Handle string shorthand: convert to text message
|
||||||
|
if jsValue.IsString() {
|
||||||
|
return &message.Message{
|
||||||
|
Type: message.TypeText,
|
||||||
|
Props: map[string]interface{}{
|
||||||
|
"content": jsValue.String(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle object
|
||||||
|
if !jsValue.IsObject() {
|
||||||
|
return nil, fmt.Errorf("message must be a string or object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to Go map
|
||||||
|
goValue, err := bridge.GoValue(jsValue, v8ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgMap, ok := goValue.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("message must be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build message
|
||||||
|
msg := &message.Message{}
|
||||||
|
|
||||||
|
// Type field (required)
|
||||||
|
if msgType, ok := msgMap["type"].(string); ok {
|
||||||
|
msg.Type = msgType
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("message.type is required and must be a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props field (optional)
|
||||||
|
if props, ok := msgMap["props"].(map[string]interface{}); ok {
|
||||||
|
msg.Props = props
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional fields
|
||||||
|
if id, ok := msgMap["id"].(string); ok {
|
||||||
|
msg.ID = id
|
||||||
|
}
|
||||||
|
if delta, ok := msgMap["delta"].(bool); ok {
|
||||||
|
msg.Delta = delta
|
||||||
|
}
|
||||||
|
if done, ok := msgMap["done"].(bool); ok {
|
||||||
|
msg.Done = done
|
||||||
|
}
|
||||||
|
if deltaPath, ok := msgMap["delta_path"].(string); ok {
|
||||||
|
msg.DeltaPath = deltaPath
|
||||||
|
}
|
||||||
|
if deltaAction, ok := msgMap["delta_action"].(string); ok {
|
||||||
|
msg.DeltaAction = deltaAction
|
||||||
|
}
|
||||||
|
if typeChange, ok := msgMap["type_change"].(bool); ok {
|
||||||
|
msg.TypeChange = typeChange
|
||||||
|
}
|
||||||
|
if groupID, ok := msgMap["group_id"].(string); ok {
|
||||||
|
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)
|
||||||
|
if metadataMap, ok := msgMap["metadata"].(map[string]interface{}); ok {
|
||||||
|
metadata := &message.Metadata{}
|
||||||
|
if timestamp, ok := metadataMap["timestamp"].(float64); ok {
|
||||||
|
metadata.Timestamp = int64(timestamp)
|
||||||
|
}
|
||||||
|
if sequence, ok := metadataMap["sequence"].(float64); ok {
|
||||||
|
metadata.Sequence = int(sequence)
|
||||||
|
}
|
||||||
|
if traceID, ok := metadataMap["trace_id"].(string); ok {
|
||||||
|
metadata.TraceID = traceID
|
||||||
|
}
|
||||||
|
msg.Metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGroup parses a JavaScript value into a message.Group
|
||||||
|
func parseGroup(v8ctx *v8go.Context, jsValue *v8go.Value) (*message.Group, error) {
|
||||||
|
// Must be an object
|
||||||
|
if !jsValue.IsObject() {
|
||||||
|
return nil, fmt.Errorf("group must be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to Go map
|
||||||
|
goValue, err := bridge.GoValue(jsValue, v8ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert group: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupMap, ok := goValue.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("group must be an object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build group
|
||||||
|
group := &message.Group{}
|
||||||
|
|
||||||
|
// ID field (required)
|
||||||
|
if id, ok := groupMap["id"].(string); ok {
|
||||||
|
group.ID = id
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("group.id is required and must be a string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages field (required)
|
||||||
|
if messagesArray, ok := groupMap["messages"].([]interface{}); ok {
|
||||||
|
group.Messages = make([]*message.Message, 0, len(messagesArray))
|
||||||
|
for i, msgInterface := range messagesArray {
|
||||||
|
// Convert to map
|
||||||
|
msgMap, ok := msgInterface.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("group.messages[%d] must be an object", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to Message
|
||||||
|
msg := &message.Message{}
|
||||||
|
|
||||||
|
// Type field (required)
|
||||||
|
if msgType, ok := msgMap["type"].(string); ok {
|
||||||
|
msg.Type = msgType
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("group.messages[%d].type is required", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props field (optional)
|
||||||
|
if props, ok := msgMap["props"].(map[string]interface{}); ok {
|
||||||
|
msg.Props = props
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional fields
|
||||||
|
if id, ok := msgMap["id"].(string); ok {
|
||||||
|
msg.ID = id
|
||||||
|
}
|
||||||
|
if delta, ok := msgMap["delta"].(bool); ok {
|
||||||
|
msg.Delta = delta
|
||||||
|
}
|
||||||
|
if done, ok := msgMap["done"].(bool); ok {
|
||||||
|
msg.Done = done
|
||||||
|
}
|
||||||
|
if deltaPath, ok := msgMap["delta_path"].(string); ok {
|
||||||
|
msg.DeltaPath = deltaPath
|
||||||
|
}
|
||||||
|
if deltaAction, ok := msgMap["delta_action"].(string); ok {
|
||||||
|
msg.DeltaAction = deltaAction
|
||||||
|
}
|
||||||
|
if typeChange, ok := msgMap["type_change"].(bool); ok {
|
||||||
|
msg.TypeChange = typeChange
|
||||||
|
}
|
||||||
|
if groupID, ok := msgMap["group_id"].(string); ok {
|
||||||
|
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)
|
||||||
|
if metadataMap, ok := msgMap["metadata"].(map[string]interface{}); ok {
|
||||||
|
metadata := &message.Metadata{}
|
||||||
|
if timestamp, ok := metadataMap["timestamp"].(float64); ok {
|
||||||
|
metadata.Timestamp = int64(timestamp)
|
||||||
|
}
|
||||||
|
if sequence, ok := metadataMap["sequence"].(float64); ok {
|
||||||
|
metadata.Sequence = int(sequence)
|
||||||
|
}
|
||||||
|
if traceID, ok := metadataMap["trace_id"].(string); ok {
|
||||||
|
metadata.TraceID = traceID
|
||||||
|
}
|
||||||
|
msg.Metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Messages = append(group.Messages, msg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("group.messages is required and must be an array")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata (optional)
|
||||||
|
if metadataMap, ok := groupMap["metadata"].(map[string]interface{}); ok {
|
||||||
|
metadata := &message.Metadata{}
|
||||||
|
if timestamp, ok := metadataMap["timestamp"].(float64); ok {
|
||||||
|
metadata.Timestamp = int64(timestamp)
|
||||||
|
}
|
||||||
|
if sequence, ok := metadataMap["sequence"].(float64); ok {
|
||||||
|
metadata.Sequence = int(sequence)
|
||||||
|
}
|
||||||
|
if traceID, ok := metadataMap["trace_id"].(string); ok {
|
||||||
|
metadata.TraceID = traceID
|
||||||
|
}
|
||||||
|
group.Metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
361
agent/output/jsapi/output_test.go
Normal file
361
agent/output/jsapi/output_test.go
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
|
agentContext "github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/config"
|
||||||
|
"github.com/yaoapp/yao/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOutputConstructor(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
script string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Create Output with context",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
return output !== undefined && output !== null;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Create Output without context should fail",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
const output = new Output();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString().includes("context argument");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := agentContext.New(context.Background(), nil, "test-chat-123", "")
|
||||||
|
ctx.AssistantID = "test-assistant-456"
|
||||||
|
|
||||||
|
// Execute test script with v8.Call
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, tt.script, &ctx)
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, res.(bool))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputSend(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
script string
|
||||||
|
expectError bool
|
||||||
|
validate func(*testing.T, *agentContext.Context)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Send text message with object",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "Hello World" }
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send text message with string shorthand",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.Send("Hello World");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send message with all fields",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.Send({
|
||||||
|
type: "text",
|
||||||
|
props: { content: "Test" },
|
||||||
|
id: "msg-1",
|
||||||
|
delta: true,
|
||||||
|
done: false,
|
||||||
|
delta_path: "content",
|
||||||
|
delta_action: "append",
|
||||||
|
metadata: {
|
||||||
|
timestamp: 1234567890,
|
||||||
|
sequence: 1,
|
||||||
|
trace_id: "trace-123"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send error message",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.Send({
|
||||||
|
type: "error",
|
||||||
|
props: {
|
||||||
|
message: "Something went wrong",
|
||||||
|
code: "ERR_001"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send without message should fail",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.Send();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString().includes("message argument");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send message without type should fail",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.Send({ props: { content: "test" } });
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString().includes("type is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create context with mock writer
|
||||||
|
ctx := agentContext.New(context.Background(), nil, "test-chat", "")
|
||||||
|
ctx.Writer = &mockWriter{}
|
||||||
|
|
||||||
|
// Execute test script with v8.Call
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, tt.script, &ctx)
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, res.(bool))
|
||||||
|
|
||||||
|
if tt.validate != nil {
|
||||||
|
tt.validate(t, &ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputSendGroup(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
script string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Send message group",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.SendGroup({
|
||||||
|
id: "group-1",
|
||||||
|
messages: [
|
||||||
|
{ type: "text", props: { content: "Message 1" } },
|
||||||
|
{ type: "text", props: { content: "Message 2" } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send group with metadata",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.SendGroup({
|
||||||
|
id: "group-1",
|
||||||
|
messages: [
|
||||||
|
{ type: "text", props: { content: "Test" } }
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
timestamp: 1234567890,
|
||||||
|
sequence: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send empty group",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.SendGroup({
|
||||||
|
id: "group-1",
|
||||||
|
messages: []
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send group without id should fail",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.SendGroup({
|
||||||
|
messages: [
|
||||||
|
{ type: "text", props: { content: "Test" } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString().includes("id is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Send group without messages should fail",
|
||||||
|
script: `
|
||||||
|
function test(ctx) {
|
||||||
|
try {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
output.SendGroup({ id: "group-1" });
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString().includes("messages is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create context with mock writer
|
||||||
|
ctx := agentContext.New(context.Background(), nil, "test-chat", "")
|
||||||
|
ctx.Writer = &mockWriter{}
|
||||||
|
|
||||||
|
// Execute test script with v8.Call
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, tt.script, &ctx)
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, res.(bool))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputChaining(t *testing.T) {
|
||||||
|
test.Prepare(t, config.Conf)
|
||||||
|
defer test.Clean()
|
||||||
|
|
||||||
|
script := `
|
||||||
|
function test(ctx) {
|
||||||
|
const output = new Output(ctx);
|
||||||
|
|
||||||
|
// Send should return the output object for chaining
|
||||||
|
const result = output.Send("Message 1");
|
||||||
|
|
||||||
|
// Should be able to chain sends
|
||||||
|
output.Send("Message 2").Send("Message 3");
|
||||||
|
|
||||||
|
return result !== undefined;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx := agentContext.New(context.Background(), nil, "test-chat", "")
|
||||||
|
ctx.Writer = &mockWriter{}
|
||||||
|
|
||||||
|
// Execute test script with v8.Call
|
||||||
|
res, err := v8.Call(v8.CallOptions{}, script, &ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, res.(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockWriter is a mock implementation of http.ResponseWriter for testing
|
||||||
|
type mockWriter struct {
|
||||||
|
data [][]byte
|
||||||
|
header http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *mockWriter) Header() http.Header {
|
||||||
|
if w.header == nil {
|
||||||
|
w.header = make(http.Header)
|
||||||
|
}
|
||||||
|
return w.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *mockWriter) Write(p []byte) (n int, err error) {
|
||||||
|
w.data = append(w.data, p)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *mockWriter) WriteHeader(statusCode int) {}
|
||||||
|
|
||||||
|
func (w *mockWriter) Flush() {}
|
||||||
|
|
@ -45,6 +45,9 @@ type Group struct {
|
||||||
// Built-in message types that all adapters must support
|
// Built-in message types that all adapters must support
|
||||||
// These types have standardized Props structures
|
// These types have standardized Props structures
|
||||||
const (
|
const (
|
||||||
|
// User interaction types
|
||||||
|
TypeUserInput = "user_input" // User input message (frontend display only)
|
||||||
|
|
||||||
// Content types
|
// Content types
|
||||||
TypeText = "text" // Plain text or Markdown content
|
TypeText = "text" // Plain text or Markdown content
|
||||||
TypeThinking = "thinking" // Reasoning/thinking process (e.g., o1 models)
|
TypeThinking = "thinking" // Reasoning/thinking process (e.g., o1 models)
|
||||||
|
|
@ -72,6 +75,15 @@ const (
|
||||||
|
|
||||||
// Standard Props structures for built-in types
|
// Standard Props structures for built-in types
|
||||||
|
|
||||||
|
// UserInputProps defines the standard structure for user input messages
|
||||||
|
// Type: "user_input"
|
||||||
|
// Props: {"content": string | ContentPart[], "role": string, "name": string}
|
||||||
|
type UserInputProps struct {
|
||||||
|
Content interface{} `json:"content"` // User input (text string or multimodal ContentPart[])
|
||||||
|
Role string `json:"role,omitempty"` // User role: "user", "system", "developer" (default: "user")
|
||||||
|
Name string `json:"name,omitempty"` // Optional participant name
|
||||||
|
}
|
||||||
|
|
||||||
// TextProps defines the standard structure for text messages
|
// TextProps defines the standard structure for text messages
|
||||||
// Type: "text"
|
// Type: "text"
|
||||||
// Props: {"content": string}
|
// Props: {"content": string}
|
||||||
|
|
|
||||||
1
main.go
1
main.go
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
_ "github.com/yaoapp/gou/diff"
|
_ "github.com/yaoapp/gou/diff"
|
||||||
_ "github.com/yaoapp/gou/encoding"
|
_ "github.com/yaoapp/gou/encoding"
|
||||||
|
_ "github.com/yaoapp/yao/agent/output/jsapi"
|
||||||
_ "github.com/yaoapp/yao/aigc"
|
_ "github.com/yaoapp/yao/aigc"
|
||||||
_ "github.com/yaoapp/yao/crypto"
|
_ "github.com/yaoapp/yao/crypto"
|
||||||
_ "github.com/yaoapp/yao/excel"
|
_ "github.com/yaoapp/yao/excel"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue