Enhance Assistant's Stream method and context handling for improved traceability and output management

- Refactored the Stream method to retrieve connector capabilities early, allowing output adapters to utilize them effectively.
- Introduced a new Info method in the Assistant to provide structured assistant information, enhancing context accessibility.
- Updated StreamStartData to include additional fields such as ChatID and Assistant info for better event tracking.
- Improved error handling in the Stream method to ensure robust management of connector retrieval failures.
- Enhanced internationalization support for stream event messages, providing localized output for different clients.
This commit is contained in:
Max 2025-11-22 11:14:49 +08:00
parent a49695e906
commit 21c0ec5034
19 changed files with 381 additions and 73 deletions

View file

@ -40,10 +40,27 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
_ = traceID // traceID is available for trace logging
// Get connector and capabilities early (before sending stream_start)
// so that output adapters can use them when converting stream_start event
if ast.Prompts != nil || ast.MCP != nil {
_, capabilities, err := ast.GetConnector(ctx)
if err != nil {
streamHandler := ast.getStreamHandler(ctx, handler...)
ast.sendStreamEndOnError(ctx, streamHandler, streamStartTime, err)
return nil, err
}
// Set capabilities in context for output adapters to use
if capabilities != nil {
ctx.Capabilities = capabilities
}
}
// Determine stream handler
streamHandler := ast.getStreamHandler(ctx, handler...)
// Send ChunkStreamStart only for root stack (agent-level stream start)
// Now ctx.Capabilities is set, so output adapters can use it
ast.sendAgentStreamStart(ctx, streamHandler, streamStartTime)
// Trace Add
@ -111,7 +128,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
return nil, err
}
// Get connector object and capabilities
// Get connector object (capabilities were already set above, before stream_start)
conn, capabilities, err := ast.GetConnector(ctx)
if err != nil {
if agentNode != nil {
@ -338,6 +355,21 @@ func (ast *Assistant) BuildRequest(ctx *context.Context, messages []context.Mess
return finalMessages, options, nil
}
// Info get the assistant information
func (ast *Assistant) Info(locale ...string) *context.AssistantInfo {
lc := "en"
if len(locale) > 0 {
lc = locale[0]
}
return &context.AssistantInfo{
ID: ast.ID,
Type: ast.Type,
Name: i18n.Tr(ast.ID, lc, ast.Name),
Avatar: ast.Avatar,
Description: i18n.Tr(ast.ID, lc, ast.Description),
}
}
// buildMessages builds the final message list with proper priority
// Priority: createResponse.Messages > input messages
// If createResponse is nil or has no messages, use input messages
@ -642,11 +674,13 @@ func (ast *Assistant) sendAgentStreamStart(ctx *context.Context, handler context
return
}
requestID := fmt.Sprintf("agent_req_%d", startTime.UnixNano())
startData := &context.StreamStartData{
RequestID: requestID,
// Build the start data
startData := context.StreamStartData{
RequestID: ctx.RequestID(),
Timestamp: startTime.UnixMilli(),
Model: ast.ID, // Use assistant ID as the "model" for agent-level stream
Assistant: ast.Info(ctx.Locale),
ChatID: ctx.ChatID,
TraceID: ctx.TraceID(),
}
if startJSON, err := jsoniter.Marshal(startData); err == nil {
@ -667,7 +701,7 @@ func (ast *Assistant) sendAgentStreamEnd(ctx *context.Context, handler context.S
}
endData := &context.StreamEndData{
RequestID: fmt.Sprintf("agent_req_%d", startTime.UnixNano()),
RequestID: ctx.RequestID(),
Timestamp: time.Now().UnixMilli(),
DurationMs: time.Since(startTime).Milliseconds(),
Status: status,

View file

@ -358,5 +358,18 @@ func SendInterrupt(contextID string, signal *InterruptSignal) error {
// generateContextID generates a unique context ID
func generateContextID() string {
return fmt.Sprintf("ctx_%d", time.Now().UnixNano())
return fmt.Sprintf("ctx-%d", time.Now().UnixNano())
}
// RequestID returns the request ID for the context
func (ctx *Context) RequestID() string {
return fmt.Sprintf("%s", ctx.ID)
}
// TraceID returns the trace ID for the context
func (ctx *Context) TraceID() string {
if ctx.Stack != nil {
return ctx.Stack.TraceID
}
return ""
}

View file

@ -177,6 +177,15 @@ type InterruptController struct {
contextID string `json:"-"` // Context ID to retrieve the parent context
}
// AssistantInfo represents the assistant information structure
type AssistantInfo struct {
ID string `json:"assistant_id"` // Assistant ID
Type string `json:"type,omitempty"` // Assistant Type, default is assistant
Name string `json:"name,omitempty"` // Assistant Name
Avatar string `json:"avatar,omitempty"` // Assistant Avatar
Description string `json:"description,omitempty"` // Assistant Description
}
// Context the context
type Context struct {
@ -190,6 +199,9 @@ type Context struct {
Writer Writer `json:"-"` // Writer, it will be used to write response data to the client
trace traceTypes.Manager `json:"-"` // Trace manager, lazy initialized on first access
// Model capabilities (set by assistant, used by output adapters)
Capabilities *ModelCapabilities `json:"-"` // Model capabilities for the current connector
// Interrupt control (all interrupt-related logic is encapsulated in InterruptController)
Interrupt *InterruptController `json:"-"` // Interrupt controller for handling user interrupts during streaming

View file

@ -221,10 +221,11 @@ type JSONSchema struct {
// StreamStartData represents the data for stream_start event
// Sent when a streaming request begins
type StreamStartData struct {
RequestID string `json:"request_id"` // Unique identifier for this request
Timestamp int64 `json:"timestamp"` // Unix timestamp when stream started
Model string `json:"model,omitempty"` // Model being used (e.g., "gpt-4o")
Capabilities map[string]interface{} `json:"capabilities,omitempty"` // Model capabilities for this request
RequestID string `json:"request_id"` // Unique identifier for this request
Timestamp int64 `json:"timestamp"` // Unix timestamp when stream started
ChatID string `json:"chat_id"` // Chat ID being used (e.g., "chat-123")
TraceID string `json:"trace_id"` // Trace ID being used (e.g., "trace-123")
Assistant *AssistantInfo `json:"assistant,omitempty"` // Assistant information
}
// StreamEndData represents the data for stream_end event

View file

@ -55,6 +55,10 @@ func init() {
"output.cui.writer.send_error": "Failed to send data to client",
"output.cui.writer.marshal_error": "Failed to marshal chunk",
// Output: Stream event messages
"output.stream_start": "Assistant is processing",
"output.view_trace": "View process",
// Common status messages
"common.status.processing": "Processing",
"common.status.completed": "Completed",
@ -111,6 +115,10 @@ func init() {
"output.cui.writer.send_error": "发送数据到客户端失败",
"output.cui.writer.marshal_error": "序列化数据块失败",
// Output: Stream event messages
"output.stream_start": "智能体正在处理",
"output.view_trace": "查看处理详情",
// Common status messages
"common.status.processing": "处理中",
"common.status.completed": "已完成",
@ -167,6 +175,10 @@ func init() {
"output.cui.writer.send_error": "发送数据到客户端失败",
"output.cui.writer.marshal_error": "序列化数据块失败",
// Output: Stream event messages
"output.stream_start": "智能体正在处理",
"output.view_trace": "查看处理详情",
// Common status messages
"common.status.processing": "处理中",
"common.status.completed": "已完成",

View file

@ -1,6 +1,8 @@
package handlers
import (
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/i18n"
"github.com/yaoapp/yao/agent/output"
@ -73,9 +75,14 @@ type streamState struct {
func (s *streamState) handleStreamStart(data []byte) int {
// Send event message to indicate stream has started
// This is a lifecycle event, CUI clients can show it, OpenAI clients will ignore it
msg := output.NewEventMessage("stream_start", "Connecting...", nil)
var startData context.StreamStartData
err := jsoniter.Unmarshal(data, &startData)
if err != nil {
log.Error("Failed to unmarshal stream start data: %v", err)
}
msg := output.NewEventMessage("stream_start", "Stream started", startData)
output.Send(s.ctx, msg)
return 0 // Continue
return 0
}
// handleGroupStart handles group start event

View file

@ -343,18 +343,25 @@ msg := output.NewEventMessage("stream_start", "Starting stream...", map[string]i
**Important Notes:**
- **Silent in OpenAI clients**: Event messages are NOT sent to standard chat clients
- **CUI clients only**: Only CUI clients process event messages
- **Lifecycle tracking**: Used for tracking agent/stream lifecycle, not chat content
- **Converted in OpenAI clients**: Event messages are typically NOT sent to OpenAI clients, **except** `stream_start`:
- `stream_start`: Converted to a clickable trace link in either `reasoning_content` (thinking models) or `content` (regular models)
- Other events: Silent (not sent to OpenAI clients)
- **CUI clients**: All event messages are processed and may show status indicators
- **Lifecycle tracking**: Used for tracking agent/stream lifecycle
- **Non-blocking**: Events don't interrupt the main message flow
**Example in Hook:**
```go
// Send stream start event
output.Send(ctx, output.NewEventMessage("stream_start", "Initializing...", map[string]interface{}{
"timestamp": time.Now().Unix(),
}))
// Send stream start event (automatically generated by assistant)
// This is typically handled by the framework, not manually sent
startData := context.StreamStartData{
RequestID: ctx.RequestID(),
Timestamp: time.Now().UnixMilli(),
TraceID: ctx.TraceID(),
ChatID: ctx.ChatID,
}
output.Send(ctx, output.NewEventMessage("stream_start", "Stream started", startData))
// Do processing
processData()
@ -368,7 +375,11 @@ output.Send(ctx, output.NewEventMessage("stream_end", "Stream completed", map[st
**Result:**
- **CUI client**: Tracks lifecycle, may show status indicators
- **OpenAI client**: Events are silent (not sent to client)
- **OpenAI client (stream_start only)**:
- Reasoning models: Shows as 🔍 with trace link in `reasoning_content` field
- Regular models: Shows as 🚀 with trace link in `content` field
- Example: "🔍 智能体正在处理 - [查看处理详情](baseURL/trace/traceID/view)"
- **OpenAI client (other events)**: Silent (not sent)
---
@ -530,18 +541,18 @@ CUI adapter passes built-in types through without transformation:
OpenAI adapter converts built-in types to OpenAI format:
| Type | OpenAI Format | Field | Note |
| ----------- | ------------------------- | ----------------------------- | ----------------------------------------- |
| `text` | `delta.content` | `props.content` | |
| `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) |
| `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients |
| `tool_call` | `delta.tool_calls` | `props.{id, name, arguments}` | |
| `error` | `error` | `props.{message, code}` | |
| `image` | `delta.content` | `props.{url, alt}` | Markdown: `![alt](url)` - displays inline |
| `audio` | `delta.content` | `props.url` | Markdown link (can't display inline) |
| `video` | `delta.content` | `props.url` | Markdown link (can't display inline) |
| `action` | (not sent) | - | Silent - system actions only |
| `event` | (not sent) | - | Silent - lifecycle events only |
| Type | OpenAI Format | Field | Note |
| ----------- | ------------------------- | ----------------------------- | -------------------------------------------------------------------- |
| `text` | `delta.content` | `props.content` | |
| `thinking` | `delta.reasoning_content` | `props.content` | Reasoning content (o1 models) |
| `loading` | `delta.reasoning_content` | `props.message` | Shows as thinking in OpenAI clients |
| `tool_call` | `delta.tool_calls` | `props.{id, name, arguments}` | |
| `error` | `error` | `props.{message, code}` | |
| `image` | `delta.content` | `props.{url, alt}` | Markdown: `![alt](url)` - displays inline |
| `audio` | `delta.content` | `props.url` | Markdown link (can't display inline) |
| `video` | `delta.content` | `props.url` | Markdown link (can't display inline) |
| `action` | (not sent) | - | Silent - system actions only |
| `event` | (conditional) | `props.{event, data}` | Most events silent; `stream_start` converted to trace link with i18n |
---

View file

@ -7,7 +7,7 @@ The output module provides a unified API for sending messages to different clien
```
agent/output/
├── message/ # Core types and interfaces (no dependencies)
│ ├── types.go # Message, MessageGroup, Props structures
│ ├── types.go # Message, Group, Props structures
│ └── interfaces.go # Writer, Adapter, Factory interfaces
├── adapters/ # Client-specific adapters
│ ├── cui/ # CUI adapter (native DSL)
@ -325,18 +325,18 @@ Adapters handle the transformation automatically based on `ctx.Accept`.
10 standardized message types with defined Props structures:
| Type | Purpose | CUI | OpenAI |
| ----------- | ------------------ | ------- | ------------------------- |
| `text` | Text content | Direct | `delta.content` |
| `thinking` | LLM reasoning | Direct | `delta.reasoning_content` |
| `loading` | Progress indicator | Direct | `delta.reasoning_content` |
| `tool_call` | Function calls | Direct | `delta.tool_calls` |
| `error` | Error messages | Direct | `error` |
| `image` | Images | Render | `![](url)` markdown |
| `audio` | Audio | Player | Link |
| `video` | Video | Player | Link |
| `action` | System commands | Execute | Silent |
| `event` | Lifecycle events | Track | Silent |
| Type | Purpose | CUI | OpenAI |
| ----------- | ------------------ | ------- | -------------------------------------------- |
| `text` | Text content | Direct | `delta.content` |
| `thinking` | LLM reasoning | Direct | `delta.reasoning_content` |
| `loading` | Progress indicator | Direct | `delta.reasoning_content` |
| `tool_call` | Function calls | Direct | `delta.tool_calls` |
| `error` | Error messages | Direct | `error` |
| `image` | Images | Render | `![](url)` markdown |
| `audio` | Audio | Player | Link |
| `video` | Video | Player | Link |
| `action` | System commands | Execute | Silent |
| `event` | Lifecycle events | Track | Conditional (stream_start converted to link) |
## Usage

View file

@ -50,7 +50,7 @@ func (w *Writer) Write(msg *message.Message) error {
}
// WriteGroup writes a message group to the output stream
func (w *Writer) WriteGroup(group *message.MessageGroup) error {
func (w *Writer) WriteGroup(group *message.Group) error {
// For CUI, we send a group start message, all messages, then a group end message
// The group structure itself is also sent for clients that want it

View file

@ -8,14 +8,79 @@ OpenAI adapter converts universal DSL messages to OpenAI-compatible format.
These types are defined in `output.types.go` and have standardized Props structures that all adapters must support:
| Message Type | Constant | Props Structure | OpenAI Format | Description |
| ------------ | --------------------- | --------------- | ------------------------- | ------------------------------------- |
| `text` | `output.TypeText` | `TextProps` | `delta.content` | Plain text or Markdown |
| `thinking` | `output.TypeThinking` | `ThinkingProps` | `delta.reasoning_content` | Reasoning process (o1 models) |
| `loading` | `output.TypeLoading` | `LoadingProps` | `delta.reasoning_content` | Loading indicator (shows as thinking) |
| `tool_call` | `output.TypeToolCall` | `ToolCallProps` | `delta.tool_calls` | Tool/function calls |
| `error` | `output.TypeError` | `ErrorProps` | `error` | Error messages |
| `action` | `output.TypeAction` | `ActionProps` | (not sent) | System actions (silent) |
| Message Type | Constant | Props Structure | OpenAI Format | Description |
| ------------ | --------------------- | --------------- | ------------------------- | ----------------------------------------- |
| `text` | `output.TypeText` | `TextProps` | `delta.content` | Plain text or Markdown |
| `thinking` | `output.TypeThinking` | `ThinkingProps` | `delta.reasoning_content` | Reasoning process (o1 models) |
| `loading` | `output.TypeLoading` | `LoadingProps` | `delta.reasoning_content` | Loading indicator (shows as thinking) |
| `tool_call` | `output.TypeToolCall` | `ToolCallProps` | `delta.tool_calls` | Tool/function calls |
| `error` | `output.TypeError` | `ErrorProps` | `error` | Error messages |
| `action` | `output.TypeAction` | `ActionProps` | (not sent) | System actions (silent) |
| `event` | `output.TypeEvent` | `EventProps` | (conditional) | Lifecycle events (stream_start converted) |
### Event Type (Lifecycle Events)
The `event` type has special handling in the OpenAI adapter:
| Event Name | Conversion | Example Output |
| -------------- | ------------------------------------------- | --------------------------------------------------- |
| `stream_start` | Converted to trace link (with i18n support) | 🔍 智能体正在处理 - [查看处理详情](/trace/xxx/view) |
| Other events | Silent (not sent) | - |
**Conversion Logic for `stream_start`:**
1. **Extract trace data**: Gets `TraceID` from event data
2. **Check model capabilities**: Determines if model supports reasoning
3. **Format based on capabilities**:
- **Reasoning models** (o1, DeepSeek R1): Uses `reasoning_content` field with 🔍 icon
- **Regular models**: Uses `content` field with 🚀 icon
4. **Apply i18n**: Uses locale from context for localized text
5. **Generate trace link**: Creates clickable link to `/trace/{traceID}/view` for standalone viewing
**Example Conversion:**
```go
// Input (event message)
{
"type": "event",
"props": {
"event": "stream_start",
"message": "Stream started",
"data": {
"trace_id": "20251122779905354593",
"request_id": "ctx-1763779905679380000",
"chat_id": "uP4CWZCMHy84nCw7"
}
}
}
// Output (reasoning model - Chinese locale)
{
"choices": [{
"delta": {
"reasoning_content": "🔍 智能体正在处理 - [查看处理详情](http://localhost:8000/__yao_admin_root/trace/20251122779905354593/view)\n"
}
}]
}
// Output (regular model - English locale)
{
"choices": [{
"delta": {
"content": "🚀 Assistant is processing - [View process](http://localhost:8000/__yao_admin_root/trace/20251122779905354593/view)\n"
}
}]
}
```
**Internationalization:**
The adapter uses `i18n.T()` to provide localized text:
| Key | English (en-us) | Chinese (zh-cn) |
| --------------------- | ----------------------- | --------------- |
| `output.stream_start` | Assistant is processing | 智能体正在处理 |
| `output.view_trace` | View process | 查看处理详情 |
### Custom Types

View file

@ -54,6 +54,20 @@ func WithModel(model string) Option {
}
}
// WithCapabilities sets the model capabilities
func WithCapabilities(capabilities *ModelCapabilities) Option {
return func(a *Adapter) {
a.config.Capabilities = capabilities
}
}
// WithLocale sets the locale for internationalization
func WithLocale(locale string) Option {
return func(a *Adapter) {
a.config.Locale = locale
}
}
// WithConverter registers a custom converter for a message type
func WithConverter(msgType string, converter ConverterFunc) Option {
return func(a *Adapter) {
@ -63,8 +77,16 @@ func WithConverter(msgType string, converter ConverterFunc) Option {
// Adapt converts a universal Message to OpenAI-compatible format
func (a *Adapter) Adapt(msg *message.Message) ([]interface{}, error) {
// Skip event messages - they are CUI-only lifecycle events
// Handle event messages specially
if msg.Type == message.TypeEvent {
// Check if this is a stream_start event
if event, ok := msg.Props["event"].(string); ok && event == message.EventStreamStart {
// Use the stream_start converter
if converter, exists := a.registry.GetConverter(message.EventStreamStart); exists {
return converter(msg, a.config)
}
}
// Other event messages are CUI-only, skip them
return []interface{}{}, nil // Return empty array, nothing to send
}

View file

@ -4,6 +4,8 @@ import (
"fmt"
"time"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/i18n"
"github.com/yaoapp/yao/agent/output/message"
)
@ -16,15 +18,16 @@ type ConverterRegistry struct {
func NewConverterRegistry() *ConverterRegistry {
return &ConverterRegistry{
converters: map[string]ConverterFunc{
message.TypeText: convertText,
message.TypeThinking: convertThinking,
message.TypeLoading: convertLoading,
message.TypeToolCall: convertToolCall,
message.TypeError: convertError,
message.TypeImage: convertImage,
message.TypeAudio: convertToLink,
message.TypeVideo: convertToLink,
message.TypeAction: convertAction,
message.TypeText: convertText,
message.TypeThinking: convertThinking,
message.TypeLoading: convertLoading,
message.TypeToolCall: convertToolCall,
message.TypeError: convertError,
message.TypeImage: convertImage,
message.TypeAudio: convertToLink,
message.TypeVideo: convertToLink,
message.TypeAction: convertAction,
message.EventStreamStart: convertStreamStart, // Handle stream_start events
},
}
}
@ -137,6 +140,84 @@ func convertAction(msg *message.Message, config *AdapterConfig) ([]interface{},
return []interface{}{}, nil
}
// convertStreamStart converts stream_start event to OpenAI format
// If model supports reasoning: converts to reasoning_content (thinking)
// Otherwise: converts to regular Markdown text with trace link
func convertStreamStart(msg *message.Message, config *AdapterConfig) ([]interface{}, error) {
// Extract stream_start data from props
data, ok := msg.Props["data"]
if !ok {
// No data, skip this message
return []interface{}{}, nil
}
// Try to convert to StreamStartData
var startData context.StreamStartData
switch v := data.(type) {
case context.StreamStartData:
startData = v
case map[string]interface{}:
// If it's a map, try to extract traceID
if traceID, ok := v["trace_id"].(string); ok {
startData.TraceID = traceID
}
if requestID, ok := v["request_id"].(string); ok {
startData.RequestID = requestID
}
default:
// Unknown data type, skip
return []interface{}{}, nil
}
// Check if we have a trace ID to link to
if startData.TraceID == "" {
// No trace ID, skip this message
return []interface{}{}, nil
}
// Generate trace link
traceLink := generateTraceLink(startData.TraceID, config)
// Check if model supports reasoning
supportsReasoning := false
if config.Capabilities != nil && config.Capabilities.Reasoning != nil {
supportsReasoning = *config.Capabilities.Reasoning
}
// Get localized text using i18n
streamStartText := i18n.T(config.Locale, "output.stream_start")
viewTraceText := i18n.T(config.Locale, "output.view_trace")
// Convert based on reasoning support
if supportsReasoning {
// Convert to thinking format (reasoning_content)
// Reasoning models display this as part of the thinking process
content := fmt.Sprintf("🔍 %s - [%s](%s)\n", streamStartText, viewTraceText, traceLink)
chunk := createOpenAIChunk(msg.ID, config.Model, map[string]interface{}{
"reasoning_content": content,
})
return []interface{}{chunk}, nil
}
// Convert to regular Markdown text
content := fmt.Sprintf("🚀 %s - [%s](%s)\n", streamStartText, viewTraceText, traceLink)
chunk := createOpenAIChunk(msg.ID, config.Model, map[string]interface{}{
"content": content,
})
return []interface{}{chunk}, nil
}
// generateTraceLink generates a trace link URL
// Uses 'view' mode (clean page without sidebar) for better viewing experience in chat
func generateTraceLink(traceID string, config *AdapterConfig) string {
baseURL := config.BaseURL
if baseURL == "" {
// If no base URL, return a relative link
return fmt.Sprintf("/trace/%s/view", traceID)
}
return fmt.Sprintf("%s/trace/%s/view", baseURL, traceID)
}
// convertImage converts image messages to Markdown image format
// Uses ![alt](url) which displays inline in Markdown-supporting clients
func convertImage(msg *message.Message, config *AdapterConfig) ([]interface{}, error) {

View file

@ -26,6 +26,19 @@ type AdapterConfig struct {
// Model name to include in OpenAI responses
Model string
// Capabilities holds the model capabilities
// Used to determine how to convert certain message types (e.g., stream_start)
Capabilities *ModelCapabilities
// Locale for internationalization (e.g., "en-US", "zh-CN")
Locale string
}
// ModelCapabilities is a simplified version of context.ModelCapabilities
// We use a local type to avoid circular dependencies
type ModelCapabilities struct {
Reasoning *bool // Supports reasoning/thinking mode (o1, DeepSeek R1)
}
// DefaultLinkTemplates provides default Markdown templates for non-text message types

View file

@ -17,8 +17,20 @@ type Writer struct {
// NewWriter creates a new OpenAI writer
func NewWriter(ctx *context.Context) (*Writer, error) {
// Create adapter with default config
adapter := NewAdapter()
// Get model capabilities from context (set by assistant)
var capabilities *ModelCapabilities
if ctx.Capabilities != nil && ctx.Capabilities.Reasoning != nil {
capabilities = &ModelCapabilities{
Reasoning: ctx.Capabilities.Reasoning,
}
}
// Create adapter with capabilities, base URL, and locale
adapter := NewAdapter(
WithCapabilities(capabilities),
WithBaseURL(getBaseURL(ctx)),
WithLocale(ctx.Locale),
)
return &Writer{
ctx: ctx,
@ -27,6 +39,22 @@ func NewWriter(ctx *context.Context) (*Writer, error) {
}, nil
}
// getBaseURL gets the base URL from context or environment
func getBaseURL(ctx *context.Context) string {
// @todo: get from context metadata
return "http://localhost:8000/__yao_admin_root"
// // Try to get from context metadata
// if ctx.Metadata != nil {
// if baseURL, ok := ctx.Metadata["base_url"].(string); ok && baseURL != "" {
// return baseURL
// }
// }
// // TODO: Get from environment variable or config
// return ""
}
// Write writes a single message to the output stream
func (w *Writer) Write(msg *message.Message) error {
// Convert message to OpenAI format using adapter
@ -67,7 +95,7 @@ func (w *Writer) Write(msg *message.Message) error {
}
// WriteGroup writes a message group to the output stream
func (w *Writer) WriteGroup(group *message.MessageGroup) error {
func (w *Writer) WriteGroup(group *message.Group) error {
// For OpenAI, we don't send group markers
// Just send each message individually
for _, msg := range group.Messages {

View file

@ -79,7 +79,7 @@ func NewActionMessage(name string, payload map[string]interface{}) *message.Mess
}
// NewEventMessage creates an event message
func NewEventMessage(event string, msg string, data map[string]interface{}) *message.Message {
func NewEventMessage(event string, msg string, data interface{}) *message.Message {
return &message.Message{
Type: message.TypeEvent,
Props: map[string]interface{}{

View file

@ -9,7 +9,7 @@ type Writer interface {
Write(msg *Message) error
// WriteGroup writes a group of messages
WriteGroup(group *MessageGroup) error
WriteGroup(group *Group) error
// Flush flushes any buffered data
Flush() error

View file

@ -35,8 +35,8 @@ type Metadata struct {
TraceID string `json:"trace_id,omitempty"` // Trace ID (for debugging)
}
// MessageGroup represents a semantically complete group of messages
type MessageGroup struct {
// Group represents a semantically complete group of messages
type Group struct {
ID string `json:"id"` // Message group ID
Messages []*Message `json:"messages"` // List of messages
Metadata *Metadata `json:"metadata,omitempty"` // Metadata
@ -62,6 +62,14 @@ const (
TypeEvent = "event" // Lifecycle event (stream_start, stream_end, etc.) - CUI only, silent in OpenAI clients
)
// Event types for TypeEvent messages
const (
EventStreamStart = "stream_start" // Stream started event
EventStreamEnd = "stream_end" // Stream ended event
EventGroupStart = "group_start" // Message group started event
EventGroupEnd = "group_end" // Message group ended event
)
// Standard Props structures for built-in types
// TextProps defines the standard structure for text messages

View file

@ -26,7 +26,7 @@ func Send(ctx *context.Context, msg *message.Message) error {
}
// SendGroup sends a message group using the appropriate writer for the context
func SendGroup(ctx *context.Context, group *message.MessageGroup) error {
func SendGroup(ctx *context.Context, group *message.Group) error {
writer, err := GetWriter(ctx)
if err != nil {
return err

View file

@ -48,6 +48,7 @@ func GinCreateCompletions(c *gin.Context) {
fmt.Println("Chat ID: ", ctx.ChatID)
fmt.Println("Assistant ID: ", ctx.AssistantID)
fmt.Println("Model: ", completionReq.Model)
fmt.Println("Locale: ", ctx.Locale)
fmt.Println("Messages count: ", len(completionReq.Messages))
if completionReq.Temperature != nil {
fmt.Println("Temperature: ", *completionReq.Temperature)