feat: Enhance AI Agent and Channel Management
- Updated labels in the AI Agents dashboard for clarity, changing "流程状态" to "Playbook 状态" and "未发布流程" to "未发布 Playbook". - Introduced AI Agent rollout percentage management in channel editing, allowing users to set and rollback rollout percentages. - Added new API endpoints for rolling back AI Agent rollout and fetching agent run metrics. - Implemented new UI components for displaying agent run details, including status, duration, and input/output tokens. - Enhanced type definitions for AdminChannel and AIAgent to include rollout percentages and runtime modes. - Updated navigation to include a section for agent runs. - Added new translations for agent run features in both English and Chinese.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package einoexperiment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/bootstrap"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/config"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
|
||||
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
|
||||
einomodel "github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// TestRealOpenAICompatibleEndpoint is intentionally opt-in because it spends
|
||||
// a small amount of configured model quota. It verifies the production-shaped
|
||||
// OpenAI-compatible adapter without exposing credentials in test output.
|
||||
func TestRealOpenAICompatibleEndpoint(t *testing.T) {
|
||||
if os.Getenv("EINO_EXPERIMENT_REAL") != "1" {
|
||||
t.Skip("set EINO_EXPERIMENT_REAL=1 to run against the configured endpoint")
|
||||
}
|
||||
configPath := strings.TrimSpace(os.Getenv("EINO_EXPERIMENT_CONFIG"))
|
||||
var err error
|
||||
if configPath == "" {
|
||||
configPath, err = findExperimentConfigPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("get working directory: %v", err)
|
||||
}
|
||||
repoRoot := filepath.Dir(filepath.Dir(configPath))
|
||||
if err := os.Chdir(repoRoot); err != nil {
|
||||
t.Fatalf("change to config root: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(workingDir) })
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load config: %v", err)
|
||||
}
|
||||
db, err := bootstrap.InitDB(cfg.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("open configured database: %v", err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err == nil {
|
||||
defer sqlDB.Close()
|
||||
}
|
||||
var aiConfig models.AIConfig
|
||||
if err := db.Where("model_type = ? AND status = ?", enums.AIModelTypeLLM, enums.StatusOk).Order("id").First(&aiConfig).Error; err != nil {
|
||||
t.Fatalf("load enabled LLM config: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(maxInt(aiConfig.TimeoutMS, 30000))*time.Millisecond)
|
||||
defer cancel()
|
||||
model, err := NewOpenAICompatibleModel(ctx, aiConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("create Eino model adapter: %v", err)
|
||||
}
|
||||
input := []*schema.Message{schema.SystemMessage("You are a terse service assistant."), schema.UserMessage("Reply with exactly: OK")}
|
||||
startedAt := time.Now()
|
||||
result, err := Run(ctx, ReActConfig{Model: model, MaxSteps: 2}, input)
|
||||
if err != nil {
|
||||
t.Fatalf("Eino ReAct request: %v", err)
|
||||
}
|
||||
if result == nil || strings.TrimSpace(result.Content) == "" {
|
||||
t.Fatal("Eino endpoint returned an empty response")
|
||||
}
|
||||
if result.ResponseMeta == nil || result.ResponseMeta.Usage == nil {
|
||||
t.Fatal("Eino endpoint did not return token usage")
|
||||
}
|
||||
t.Logf("real endpoint verified: latency=%s promptTokens=%d completionTokens=%d", time.Since(startedAt).Round(time.Millisecond), result.ResponseMeta.Usage.PromptTokens, result.ResponseMeta.Usage.CompletionTokens)
|
||||
|
||||
toolModel, err := model.WithTools([]*schema.ToolInfo{{
|
||||
Name: "eino_echo",
|
||||
Desc: "Echoes a short input. Always call this tool when asked to verify tool calling.",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"text": {Type: schema.String, Desc: "Short text to echo", Required: true},
|
||||
}),
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("bind Eino tool: %v", err)
|
||||
}
|
||||
toolResult, err := toolModel.Generate(ctx, []*schema.Message{schema.UserMessage("Verify tool calling by invoking eino_echo with text OK.")},
|
||||
einomodel.WithToolChoice(schema.ToolChoiceForced, "eino_echo"),
|
||||
einoopenai.WithExtraFields(map[string]any{"enable_thinking": false}),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("real endpoint tool call: %v", err)
|
||||
}
|
||||
if toolResult == nil || len(toolResult.ToolCalls) != 1 || toolResult.ToolCalls[0].Function.Name != "eino_echo" {
|
||||
t.Fatalf("expected one eino_echo tool call, got %#v", toolResult)
|
||||
}
|
||||
|
||||
stream, err := model.Stream(ctx, []*schema.Message{schema.UserMessage("Reply with exactly: STREAM_OK")})
|
||||
if err != nil {
|
||||
t.Fatalf("real endpoint stream: %v", err)
|
||||
}
|
||||
// ConcatMessageStream consumes and closes the Eino reader. Do not close it
|
||||
// again here: v0.9.6 treats a second close as a panic.
|
||||
streamResult, err := schema.ConcatMessageStream(stream)
|
||||
if err != nil {
|
||||
t.Fatalf("concat real stream: %v", err)
|
||||
}
|
||||
if streamResult == nil || strings.TrimSpace(streamResult.Content) == "" {
|
||||
t.Fatal("Eino endpoint stream returned an empty response")
|
||||
}
|
||||
t.Logf("real endpoint tool and stream verified: toolCalls=%d streamChars=%d", len(toolResult.ToolCalls), len([]rune(streamResult.Content)))
|
||||
}
|
||||
|
||||
func findExperimentConfigPath() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
candidate := filepath.Join(dir, "config", "config.yaml")
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func maxInt(value, fallback int) int {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user