diff --git a/.gitignore b/.gitignore index 398f12b..616dcd6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ output/* # Vscode files .vscode - +.DS_Store diff --git a/go.mod b/go.mod index dfa2253..4d15bad 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/cloudwego/eino-examples go 1.22.0 -toolchain go1.22.1 +toolchain go1.22.10 require ( github.com/cloudwego/eino v0.3.7 diff --git a/quickstart/eino_assistant/.env b/quickstart/eino_assistant/.env new file mode 100644 index 0000000..0cbb6f2 --- /dev/null +++ b/quickstart/eino_assistant/.env @@ -0,0 +1,18 @@ +# ark model: https://console.volcengine.com/ark +# 必填, +# 火山云方舟 ChatModel 的 Endpoint ID +export ARK_CHAT_MODEL="" +# 火山云方舟 向量化模型的 Endpoint ID +export ARK_EMBEDDING_MODEL="" +# 火山云方舟的 API Key +export ARK_API_KEY="" + +# langfuse: https://cloud.langfuse.com/ +# 下面两个环境变量如果为空,则不开启 langfuse callback +# Langfuse Project 的 Public Key +export LANGFUSE_PUBLIC_KEY="" +# Langfuse Project 的 Secret Key。 注意,Secret Key 仅可在被创建时查看一次 +export LANGFUSE_SECRET_KEY="" + +# Redis Server 的地址,不填写时,默认是 localhost:6379 +export REDIS_ADDR= diff --git a/quickstart/eino_assistant/.gitignore b/quickstart/eino_assistant/.gitignore new file mode 100644 index 0000000..d27265c --- /dev/null +++ b/quickstart/eino_assistant/.gitignore @@ -0,0 +1,166 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea/ +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +.vscode/ +log/ +cmd/einoagent/data/ +__debug_bin* +/einoagent +.DS_Store +/data/ +/einoagent \ No newline at end of file diff --git a/quickstart/eino_assistant/README.md b/quickstart/eino_assistant/README.md new file mode 100644 index 0000000..9361bad --- /dev/null +++ b/quickstart/eino_assistant/README.md @@ -0,0 +1,42 @@ +## 说明 + +### docker 启动 redis 作为向量数据库 + +```bash +docker-compose up -d +# 可以在 http://127.0.0.1:8001 看到 redis 的 web 界面 +# redis 监听在 127.0.0.1:6379, 使用 redis-cli ping 可测试 +``` + +### 环境变量 + +所需的大模型和 API Key. +豆包大模型地址: https://console.volcengine.com/ark/region:ark+cn-beijing/model +> ChatModel 推荐: [Doubao-pro-4k (functioncall)](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-4k) +> EmbeddingModel 推荐: [Doubao-embedding-large](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-embedding-large) +> 进入页面后点击 `推理` 按钮,即可创建按量计费的模型接入点,对应的 `ep-xxx` 就是所需的 model 名称 + +```bash +export ARK_API_KEY=xxx +export ARK_CHAT_MODEL=xxx +export ARK_EMBEDDING_MODEL=xxx +``` + +### 启动 eino agent server + +```bash +# 为了使用 data 目录,需要在 eino_assistant 目录下执行指令 +go run cmd/einoagent/*.go +``` + +### 访问 + +访问 http://127.0.0.1:8080/ 即可看到效果 + +### 命令行运行 index (可选) + +```bash +# 因示例的Markdown文件存放在 cmd/knowledgeindexing/eino-docs 目录,代码中指定了相对路径 ./eino-docs,所以需在 cmd/knowledgeindexing 运行指令 +cd cmd/knowledgeindexing +go run main.go +``` diff --git a/quickstart/eino_assistant/cmd/einoagent/agent/agent.go b/quickstart/eino_assistant/cmd/einoagent/agent/agent.go new file mode 100644 index 0000000..a1aed8e --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/agent/agent.go @@ -0,0 +1,181 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agent + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "sync" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/eino/einoagent" + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/mem" +) + +var memory = mem.GetDefaultMemory() + +var cbHandler callbacks.Handler + +var once sync.Once + +func Init() error { + var err error + once.Do(func() { + os.MkdirAll("log", 0755) + var f *os.File + f, err = os.OpenFile("log/eino.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return + } + + cbConfig := &LogCallbackConfig{ + Detail: true, + Writer: f, + } + if os.Getenv("DEBUG") == "true" { + cbConfig.Debug = true + } + // this is for invoke option of WithCallback + cbHandler = LogCallback(cbConfig) + + // init global callback, for trace and metrics + if os.Getenv("LANGFUSE_PUBLIC_KEY") != "" && os.Getenv("LANGFUSE_SECRET_KEY") != "" { + fmt.Println("[eino agent] INFO: use langfuse as callback, watch at: https://cloud.langfuse.com") + cbh, _ := langfuse.NewLangfuseHandler(&langfuse.Config{ + Host: "https://cloud.langfuse.com", + PublicKey: os.Getenv("LANGFUSE_PUBLIC_KEY"), + SecretKey: os.Getenv("LANGFUSE_SECRET_KEY"), + Name: "Eino Assistant", + Public: true, + Release: "release/v0.0.1", + UserID: "eino_god", + Tags: []string{"eino", "assistant"}, + }) + callbacks.InitCallbackHandlers([]callbacks.Handler{cbh}) + } + }) + return err +} + +func RunAgent(ctx context.Context, id string, msg string) (*schema.StreamReader[*schema.Message], error) { + + runner, err := einoagent.BuildEinoAgent(ctx, &einoagent.BuildConfig{ + EinoAgent: &einoagent.EinoAgentBuildConfig{}, + }) + if err != nil { + return nil, fmt.Errorf("failed to build agent graph: %w", err) + } + + conversation := memory.GetConversation(id, true) + + userMessage := &einoagent.UserMessage{ + ID: id, + Query: msg, + History: conversation.GetMessages(), + } + + sr, err := runner.Stream(ctx, userMessage, compose.WithCallbacks(cbHandler)) + if err != nil { + return nil, fmt.Errorf("failed to stream: %w", err) + } + + srs := sr.Copy(2) + + go func() { + // for save to memory + fullMsgs := make([]*schema.Message, 0) + + defer func() { + // close stream if you used it + srs[1].Close() + + // add user input to history + conversation.Append(schema.UserMessage(msg)) + + fullMsg, err := schema.ConcatMessages(fullMsgs) + if err != nil { + fmt.Println("error concatenating messages: ", err.Error()) + } + // add agent response to history + conversation.Append(fullMsg) + }() + + outer: + for { + select { + case <-ctx.Done(): + fmt.Println("context done", ctx.Err()) + return + default: + chunk, err := srs[1].Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break outer + } + } + + fullMsgs = append(fullMsgs, chunk) + } + } + }() + + return srs[0], nil +} + +type LogCallbackConfig struct { + Detail bool + Debug bool + Writer io.Writer +} + +func LogCallback(config *LogCallbackConfig) callbacks.Handler { + if config == nil { + config = &LogCallbackConfig{ + Detail: true, + Writer: os.Stdout, + } + } + if config.Writer == nil { + config.Writer = os.Stdout + } + builder := callbacks.NewHandlerBuilder() + builder.OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + fmt.Fprintf(config.Writer, "[view]: start [%s:%s:%s]\n", info.Component, info.Type, info.Name) + if config.Detail { + var b []byte + if config.Debug { + b, _ = json.MarshalIndent(input, "", " ") + } else { + b, _ = json.Marshal(input) + } + fmt.Fprintf(config.Writer, "%s\n", string(b)) + } + return ctx + }) + builder.OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + fmt.Fprintf(config.Writer, "[view]: end [%s:%s:%s]\n", info.Component, info.Type, info.Name) + return ctx + }) + return builder.Build() +} diff --git a/quickstart/eino_assistant/cmd/einoagent/agent/server.go b/quickstart/eino_assistant/cmd/einoagent/agent/server.go new file mode 100644 index 0000000..93b4af6 --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/agent/server.go @@ -0,0 +1,242 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package agent + +import ( + "bufio" + "context" + "embed" + "errors" + "io" + "log" + "mime" + "os" + "path/filepath" + "time" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/cloudwego/hertz/pkg/route" + "github.com/hertz-contrib/sse" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/mem" +) + +//go:embed web +var webContent embed.FS + +type ChatRequest struct { + ID string `json:"id"` + Message string `json:"message"` +} + +func BindRoutes(r *route.RouterGroup) error { + if err := Init(); err != nil { + return err + } + + // API 路由 + r.GET("/api/chat", HandleChat) + r.GET("/api/log", HandleLog) + r.GET("/api/history", HandleHistory) + r.DELETE("/api/history", HandleDeleteHistory) + + // 静态文件服务 + r.GET("/", func(ctx context.Context, c *app.RequestContext) { + content, err := webContent.ReadFile("web/index.html") + if err != nil { + c.String(consts.StatusNotFound, "File not found") + return + } + c.Header("Content-Type", "text/html") + c.Write(content) + }) + + r.GET("/:file", func(ctx context.Context, c *app.RequestContext) { + file := c.Param("file") + content, err := webContent.ReadFile("web/" + file) + if err != nil { + c.String(consts.StatusNotFound, "File not found") + return + } + + contentType := mime.TypeByExtension(filepath.Ext(file)) + if contentType == "" { + contentType = "application/octet-stream" + } + c.Header("Content-Type", contentType) + c.Write(content) + }) + + return nil +} + +func HandleChat(ctx context.Context, c *app.RequestContext) { + id := c.Query("id") + message := c.Query("message") + if id == "" || message == "" { + c.JSON(consts.StatusBadRequest, map[string]string{ + "status": "error", + "error": "missing id or message parameter", + }) + return + } + + log.Printf("[Chat] Starting chat with ID: %s, Message: %s\n", id, message) + + sr, err := RunAgent(ctx, id, message) + if err != nil { + log.Printf("[Chat] Error running agent: %v\n", err) + c.JSON(consts.StatusInternalServerError, map[string]string{ + "status": "error", + "error": err.Error(), + }) + return + } + + s := sse.NewStream(c) + defer func() { + sr.Close() + c.Flush() + + log.Printf("[Chat] Finished chat with ID: %s\n", id) + }() + +outer: + for { + select { + case <-ctx.Done(): + log.Printf("[Chat] Context done for chat ID: %s\n", id) + return + default: + msg, err := sr.Recv() + if errors.Is(err, io.EOF) { + log.Printf("[Chat] EOF received for chat ID: %s\n", id) + break outer + } + if err != nil { + log.Printf("[Chat] Error receiving message: %v\n", err) + break outer + } + + err = s.Publish(&sse.Event{ + Data: []byte(msg.Content), + }) + if err != nil { + log.Printf("[Chat] Error publishing message: %v\n", err) + break outer + } + } + } +} + +func HandleHistory(ctx context.Context, c *app.RequestContext) { + // query: id => get history, none => list all + id := c.Query("id") + + if id == "" { + ids := mem.GetDefaultMemory().ListConversations() + + c.JSON(consts.StatusOK, map[string]interface{}{ + "ids": ids, + }) + return + } + + conversation := mem.GetDefaultMemory().GetConversation(id, false) + if conversation == nil { + c.JSON(consts.StatusNotFound, map[string]string{ + "error": "conversation not found", + }) + return + } + + c.JSON(consts.StatusOK, map[string]interface{}{ + "conversation": conversation, + }) + +} + +func HandleDeleteHistory(ctx context.Context, c *app.RequestContext) { + id := c.Query("id") + if id == "" { + c.JSON(consts.StatusBadRequest, map[string]string{ + "error": "missing id parameter", + }) + return + } + + mem.GetDefaultMemory().DeleteConversation(id) + c.JSON(consts.StatusOK, map[string]string{ + "status": "success", + }) +} + +func HandleLog(ctx context.Context, c *app.RequestContext) { + file, err := os.Open("log/eino.log") + if err != nil { + c.JSON(consts.StatusInternalServerError, map[string]string{ + "status": "error", + "error": err.Error(), + }) + return + } + defer file.Close() + + // Create a new SSE stream + s := sse.NewStream(c) + defer c.Flush() + + // Seek to the end of the file + _, err = file.Seek(0, io.SeekEnd) + if err != nil { + log.Println("error seeking file:", err) + return + } + + // Use a goroutine to continuously read new lines + go func() { + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + log.Println("error reading log:", err) + break + } + + // If we got a line, publish it + if line != "" { + err = s.Publish(&sse.Event{ + Data: []byte(line), + }) + if err != nil { + log.Println("error publishing log:", err) + break + } + } + + // If we hit EOF, wait a bit and try again + if err == io.EOF { + time.Sleep(100 * time.Millisecond) + continue + } + } + }() + + // Keep the connection open + <-ctx.Done() +} diff --git a/quickstart/eino_assistant/cmd/einoagent/agent/web/index.html b/quickstart/eino_assistant/cmd/einoagent/agent/web/index.html new file mode 100644 index 0000000..62c2e4d --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/agent/web/index.html @@ -0,0 +1,96 @@ + + + + + + Eino Agent Chat + + + + + + + + + + +
+ +
+
+

Chat History

+
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+ +
+
+

Task List

+ +
+
+ +
+
+ + +
+
+

Log Output

+ +
+
+
+
+
+ + + +
+
+ + + \ No newline at end of file diff --git a/quickstart/eino_assistant/cmd/einoagent/agent/web/script.js b/quickstart/eino_assistant/cmd/einoagent/agent/web/script.js new file mode 100644 index 0000000..a53eb1e --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/agent/web/script.js @@ -0,0 +1,524 @@ +document.addEventListener('DOMContentLoaded', () => { + const messageInput = document.getElementById('message-input'); + const sendButton = document.getElementById('send-button'); + const chatMessages = document.getElementById('chat-messages'); + const logMessages = document.getElementById('log-messages'); + const chatHistory = document.getElementById('chat-history'); + const rightPanel = document.getElementById('right-panel'); + const togglePanel = document.getElementById('toggle-panel'); + const newChatButton = document.getElementById('new-chat'); + + let chatId = uuidv4(); + let currentConversation = null; + let abortController = null; // 用于取消请求 + + // 创建取消按钮 + const cancelButton = document.createElement('button'); + cancelButton.id = 'cancel-button'; + cancelButton.className = 'w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 hidden absolute left-1/2 -translate-x-1/2 -top-10'; + cancelButton.innerHTML = ''; + messageInput.parentElement.style.position = 'relative'; + messageInput.parentElement.appendChild(cancelButton); + + // 取消按钮点击事件 + cancelButton.addEventListener('click', () => { + if (abortController) { + abortController.abort(); + abortController = null; + } + + // 隐藏取消按钮,显示发送按钮 + cancelButton.classList.add('hidden'); + sendButton.classList.remove('hidden'); + // 重新启用输入框 + messageInput.disabled = false; + sendButton.disabled = false; + sendButton.classList.remove('opacity-50'); + }); + + // 配置 marked + marked.setOptions({ + highlight: function(code, language) { + if (Prism.languages[language]) { + return Prism.highlight(code, Prism.languages[language], language); + } + return code; + }, + breaks: true, + gfm: true + }); + + // 处理消息内容的函数 + function processMessageContent(content) { + return content; // 直接返回原始内容 + } + + // 添加复制按钮到代码块 + function addCopyButtons() { + // 只选择包含 code 标签的 pre 元素 + document.querySelectorAll('pre code').forEach(code => { + const pre = code.parentElement; + if (pre.classList.contains('copy-button-added')) { + return; + } + pre.classList.add('copy-button-added'); + + const button = document.createElement('button'); + button.className = 'copy-button'; + button.textContent = 'Copy'; + + button.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(code.textContent); + button.textContent = 'Copied!'; + button.classList.add('copied'); + setTimeout(() => { + button.textContent = 'Copy'; + button.classList.remove('copied'); + }, 2000); + } catch (err) { + console.error('Failed to copy:', err); + button.textContent = 'Failed'; + setTimeout(() => { + button.textContent = 'Copy'; + }, 2000); + } + }); + + pre.insertBefore(button, pre.firstChild); + }); + } + + // 面板控制 + document.querySelectorAll('.panel-toggle').forEach(button => { + button.addEventListener('click', () => { + const target = document.getElementById(button.dataset.target); + const panel = button.closest('.panel'); + const icon = button.querySelector('svg'); + + if (target.id === 'task-content') { + // Task panel 使用高度控制 + if (panel.style.flex === '0 1 48px') { + panel.style.flex = '1 1 60%'; + icon.style.transform = 'rotate(180deg)'; + } else { + panel.style.flex = '0 1 48px'; + icon.style.transform = 'rotate(0deg)'; + } + } else { + // Log panel 高度收缩 + if (panel.style.flex === '0 1 48px') { + panel.style.flex = '1 1 300px'; + icon.style.transform = 'rotate(180deg)'; + } else { + panel.style.flex = '0 1 48px'; + icon.style.transform = 'rotate(0deg)'; + } + } + }); + }); + + // 右侧面板切换 + togglePanel.addEventListener('click', () => { + rightPanel.classList.toggle('w-0'); + rightPanel.classList.toggle('w-[500px]'); + rightPanel.classList.toggle('opacity-0'); + rightPanel.classList.toggle('opacity-100'); + }); + + // 新建对话 + newChatButton.addEventListener('click', () => { + chatId = uuidv4(); + currentConversation = null; + chatMessages.innerHTML = ''; + messageInput.value = ''; + + // 创建新的历史记录项 + const historyItem = document.createElement('div'); + historyItem.className = 'chat-item p-3 hover:bg-gray-100 cursor-pointer rounded-lg mb-2 transition-colors flex justify-between items-start'; + historyItem.dataset.chatId = chatId; + historyItem.innerHTML = ` +
+
Empty
+
ID: ${chatId.substring(0, 8)}...
+
+ + `; + + // 添加删除按钮事件 + const deleteButton = historyItem.querySelector('.delete-chat'); + deleteButton.addEventListener('click', (e) => { + e.stopPropagation(); + deleteConversation(chatId, historyItem); + }); + + // 添加点击事件 + historyItem.querySelector('.flex-1').addEventListener('click', () => loadConversation(chatId)); + + // 将新对话添加到列表顶部 + if (chatHistory.firstChild) { + chatHistory.insertBefore(historyItem, chatHistory.firstChild); + } else { + chatHistory.appendChild(historyItem); + } + + highlightCurrentChat(); + }); + + // 设置 SSE 日志监听 + let isAutoScrollLog = true; + + function connectLogStream() { + console.log('Connecting to log stream...'); + const logSource = new EventSource('/agent/api/log'); + + logSource.onmessage = (event) => { + const logMessage = event.data; + const wasAtBottom = isAutoScrollLog; + + // 创建新的日志行 + const logLine = document.createElement('div'); + logLine.className = 'log-line'; + logLine.textContent = logMessage; + logMessages.appendChild(logLine); + + // 保持最新的1000行日志 + const maxLogLines = 1000; + while (logMessages.children.length > maxLogLines) { + logMessages.removeChild(logMessages.firstChild); + } + + // 如果之前在底部,则自动滚动到新消息 + if (wasAtBottom) { + logMessages.scrollTop = logMessages.scrollHeight; + } + }; + + logSource.onerror = (error) => { + console.error('Log SSE Error:', error); + logSource.close(); + + // 3秒后尝试重连 + console.log('Reconnecting in 3 seconds...'); + setTimeout(connectLogStream, 3000); + }; + + logSource.onopen = () => { + console.log('Log stream connected'); + }; + } + + // 监听日志面板的滚动事件 + logMessages.addEventListener('scroll', () => { + const scrollBottom = logMessages.scrollHeight - logMessages.clientHeight; + isAutoScrollLog = Math.abs(scrollBottom - logMessages.scrollTop) < 2; + }); + + // 初始连接 + connectLogStream(); + + function highlightCurrentChat() { + document.querySelectorAll('.chat-item').forEach(item => { + item.classList.remove('bg-blue-50'); + }); + const currentItem = document.querySelector(`[data-chat-id="${chatId}"]`); + if (currentItem) { + currentItem.classList.add('bg-blue-50'); + } + } + + // 删除对话 + async function deleteConversation(id, element) { + try { + const response = await fetch(`/agent/api/history?id=${id}`, { + method: 'DELETE' + }); + + if (response.ok) { + element.remove(); + if (id === chatId) { + chatId = uuidv4(); + currentConversation = null; + chatMessages.innerHTML = ''; + } + } else { + console.error('Failed to delete conversation'); + } + } catch (error) { + console.error('Error deleting conversation:', error); + } + } + + // 加载对话 + async function loadConversation(id) { + try { + const response = await fetch(`/agent/api/history?id=${id}`); + const data = await response.json(); + + if (data.conversation) { + chatId = id; + currentConversation = data.conversation; + chatMessages.innerHTML = ''; + + data.conversation.messages.forEach(msg => { + appendMessage(msg.content, msg.role === 'user', false); + }); + + highlightCurrentChat(); + } + } catch (error) { + console.error('Error loading conversation:', error); + } + } + + // 添加消息到聊天区域 + function appendMessage(content, isUser, animate = true) { + const processedContent = processMessageContent(content); + const messageDiv = document.createElement('div'); + messageDiv.className = 'flex items-start gap-3 mb-4'; + + // 添加头像 + const avatar = document.createElement('div'); + avatar.className = 'w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 flex-shrink-0'; + avatar.textContent = isUser ? '🧑‍💻' : '🤖'; + messageDiv.appendChild(avatar); + + // 消息内容 + const contentDiv = document.createElement('div'); + contentDiv.className = `message markdown-body rounded-lg p-4 ${isUser ? 'bg-gray-100' : 'bg-gray-50'}`; + + if (!animate || isUser) { + contentDiv.innerHTML = marked.parse(processedContent); + addCopyButtons(); + } else { + const typingDiv = document.createElement('div'); + contentDiv.appendChild(typingDiv); + + new Typed(typingDiv, { + strings: [marked.parse(processedContent)], + typeSpeed: 20, + showCursor: false, + onComplete: () => addCopyButtons() + }); + } + + messageDiv.appendChild(contentDiv); + chatMessages.appendChild(messageDiv); + chatMessages.scrollTop = chatMessages.scrollHeight; + } + + async function sendMessage() { + const message = messageInput.value.trim(); + if (!message) return; + + // 如果是新对话的第一条消息,更新历史记录标题 + const historyItem = document.querySelector(`[data-chat-id="${chatId}"]`); + if (historyItem && historyItem.querySelector('.font-medium').textContent === 'Empty') { + historyItem.querySelector('.font-medium').textContent = message; + } + + appendMessage(message, true); + messageInput.value = ''; + + // 禁用输入框和发送按钮,显示取消按钮 + messageInput.disabled = true; + sendButton.disabled = true; + sendButton.classList.add('opacity-50'); + sendButton.classList.add('hidden'); + cancelButton.classList.remove('hidden'); + + try { + console.log('Starting chat with ID:', chatId); + + let hasReceivedMessage = false; + let currentMessageDiv = null; + let contentDiv = null; + let accumulatedContent = ''; + let isFirstChunk = true; + let lastRenderTime = 0; + + // 创建新的 AbortController + abortController = new AbortController(); + + // 使用 fetch 替代 EventSource,添加 signal + const response = await fetch(`/agent/api/chat?id=${chatId}&message=${encodeURIComponent(message)}`, { + signal: abortController.signal + }); + + // 检查响应状态 + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; // 用于存储不完整的 SSE 消息 + + try { + while (true) { + const {value, done} = await reader.read(); + if (done) break; + + // 解码新的数据块并添加到缓冲区 + buffer += decoder.decode(value, {stream: true}); + + // 按行分割并处理每一行 + const lines = buffer.split(/\r\n|\r|\n/); + // 保留最后一个可能不完整的行 + buffer = lines.pop() || ''; + + for (const line of lines) { + // 解析 SSE 格式的行 + if (line.startsWith('data:')) { + // 保留 data: 后的所有内容,包括前导空格 + const rawData = line.slice(5); // 直接截取 'data:' 后的内容 + // console.log(`Raw SSE data: |${rawData}|`); + hasReceivedMessage = true; + + // 如果是第一个 chunk,创建新的消息框 + if (isFirstChunk) { + const messageDiv = document.createElement('div'); + messageDiv.className = 'flex items-start gap-3 mb-4'; + + // 添加头像 + const avatar = document.createElement('div'); + avatar.className = 'w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 flex-shrink-0'; + avatar.textContent = '🤖'; + messageDiv.appendChild(avatar); + + // 消息内容 + contentDiv = document.createElement('div'); + contentDiv.className = 'message markdown-body rounded-lg p-4 bg-gray-50'; + messageDiv.appendChild(contentDiv); + chatMessages.appendChild(messageDiv); + + currentMessageDiv = contentDiv; + isFirstChunk = false; + accumulatedContent = rawData; + } else { + if (rawData === '') { + // 如果是空数据,添加换行符 + accumulatedContent += '\n'; + } else { + // 否则直接拼接数据 + accumulatedContent += rawData; + } + } + + // 限制渲染频率 + const now = Date.now(); + if (now - lastRenderTime >= 100) { + renderContent(); + } else { + clearTimeout(window.renderTimeout); + window.renderTimeout = setTimeout(renderContent, 100 - (now - lastRenderTime)); + } + } + } + } + + function renderContent() { + currentMessageDiv.innerHTML = marked.parse(accumulatedContent); + addCopyButtons(); + chatMessages.scrollTop = chatMessages.scrollHeight; + lastRenderTime = Date.now(); + } + + // 请求完成后,隐藏取消按钮,显示发送按钮 + cancelButton.classList.add('hidden'); + sendButton.classList.remove('hidden'); + abortController = null; + + } finally { + // 确保读取器被正确关闭 + reader.cancel(); + } + + } catch (error) { + console.error('Error sending message:', error); + if (error.name === 'AbortError') { + } else { + appendMessage('Error: Failed to send message. Please try again.', false); + } + + abortController = null; + } finally { + messageInput.disabled = false; + sendButton.disabled = false; + sendButton.classList.remove('opacity-50'); + sendButton.classList.remove('hidden'); + cancelButton.classList.add('hidden'); + } + } + + sendButton.addEventListener('click', sendMessage); + messageInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }); + + // 加载历史对话列表 + fetch('/agent/api/history') + .then(response => response.json()) + .then(data => { + if (data.ids && data.ids.length > 0) { + chatHistory.innerHTML = ''; // 清空现有历史 + let firstConversationId = null; + + const loadPromises = data.ids.map(id => + fetch(`/agent/api/history?id=${id}`) + .then(response => response.json()) + .then(convData => { + if (convData.conversation) { + const firstMessage = convData.conversation.messages.length > 0 + ? convData.conversation.messages[0].content + : 'Empty'; + + const historyItem = document.createElement('div'); + historyItem.className = 'chat-item p-3 hover:bg-gray-100 cursor-pointer rounded-lg mb-2 transition-colors flex justify-between items-start'; + historyItem.dataset.chatId = id; + historyItem.innerHTML = ` +
+
${firstMessage}
+
ID: ${id.substring(0, 8)}...
+
+ + `; + + const deleteButton = historyItem.querySelector('.delete-chat'); + deleteButton.addEventListener('click', (e) => { + e.stopPropagation(); + deleteConversation(id, historyItem); + }); + + historyItem.querySelector('.flex-1').addEventListener('click', () => loadConversation(id)); + chatHistory.appendChild(historyItem); + + // 记录第一个对话的ID + if (firstConversationId === null) { + firstConversationId = id; + } + } + }) + ); + + // 等待所有历史记录加载完成后,加载第一个对话 + Promise.all(loadPromises).then(() => { + if (firstConversationId) { + loadConversation(firstConversationId); + } + }); + } + }) + .catch(error => console.error('Error loading history:', error)); +}); \ No newline at end of file diff --git a/quickstart/eino_assistant/cmd/einoagent/agent/web/style.css b/quickstart/eino_assistant/cmd/einoagent/agent/web/style.css new file mode 100644 index 0000000..10f11c1 --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/agent/web/style.css @@ -0,0 +1,235 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + background-color: #f5f5f5; +} + +.container { + display: flex; + gap: 20px; + max-width: 1200px; + margin: 0 auto; +} + +.chat-container { + flex: 2; + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 20px; + display: flex; + flex-direction: column; + height: 80vh; +} + +.log-container { + flex: 1; + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 20px; + height: 80vh; + overflow-y: auto; +} + +#chat-messages { + flex-grow: 1; + overflow-y: auto; + margin-bottom: 20px; + padding: 10px; +} + +.message { + margin-bottom: 10px; + padding: 10px; + border-radius: 8px; + max-width: 80%; +} + +.user-message { + background-color: #007bff; + color: white; + margin-left: auto; +} + +.agent-message { + background-color: #e9ecef; + color: #212529; +} + +.input-container { + display: flex; + gap: 10px; +} + +#message-input { + flex-grow: 1; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 16px; +} + +#send-button { + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} + +#send-button:hover { + background-color: #0056b3; +} + +#log-messages { + font-family: monospace; + white-space: pre-wrap; + font-size: 14px; + color: #666; +} + +.markdown-body { + color: inherit; + background-color: transparent; +} + +.markdown-body pre { + background-color: #2d3748; + border-radius: 6px; + padding: 16px; + overflow: auto; + color: #f7fafc; + position: relative; +} + +.copy-button { + position: absolute; + top: 8px; + right: 8px; + padding: 4px 8px; + background-color: #4a5568; + color: #e2e8f0; + border: none; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s ease-in-out; +} + +.copy-button:hover { + background-color: #718096; +} + +.markdown-body pre:hover .copy-button { + opacity: 1; +} + +.copy-button.copied { + background-color: #48bb78; +} + +.markdown-body code { + background-color: rgba(175,184,193,0.2); + padding: 0.2em 0.4em; + border-radius: 6px; + font-size: 85%; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + -webkit-font-smoothing: antialiased; + font-weight: normal; +} + +.markdown-body pre code { + background-color: transparent; + padding: 0; + font-size: inherit; + color: inherit; + font-family: inherit; + line-height: 1.5; + text-shadow: none; +} + +.markdown-body h1 { font-size: 2em; margin-bottom: 0.5em; font-weight: 600; } +.markdown-body h2 { font-size: 1.5em; margin-bottom: 0.5em; font-weight: 600; } +.markdown-body h3 { font-size: 1.25em; margin-bottom: 0.5em; font-weight: 600; } +.markdown-body h4 { font-size: 1em; margin-bottom: 0.5em; font-weight: 600; } +.markdown-body h5 { font-size: 0.875em; margin-bottom: 0.5em; font-weight: 600; } +.markdown-body h6 { font-size: 0.85em; margin-bottom: 0.5em; font-weight: 600; } + +.markdown-body ul, .markdown-body ol { + padding-left: 2em; + margin-bottom: 1em; + list-style-position: outside; +} + +.markdown-body ul { + list-style-type: disc; +} + +.markdown-body ul ul { + list-style-type: circle; +} + +.markdown-body ul ul ul { + list-style-type: square; +} + +.markdown-body ol { + list-style-type: decimal; +} + +.markdown-body li { + margin-bottom: 0.25em; +} + +.markdown-body li > p { + margin-top: 1em; +} + +.markdown-body blockquote { + padding: 0 1em; + color: #656d76; + border-left: 0.25em solid #d0d7de; + margin-bottom: 1em; +} + +.markdown-body p { + margin-bottom: 1em; + line-height: 1.6; +} + +.markdown-body table { + border-collapse: collapse; + margin-bottom: 1em; + width: 100%; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #d0d7de; +} + +.markdown-body table tr { + background-color: #ffffff; + border-top: 1px solid #d0d7de; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #d0d7de; + border: 0; +} + +.panel { + transition: all 0.3s ease-in-out; +} \ No newline at end of file diff --git a/quickstart/eino_assistant/cmd/einoagent/data/memory/f1d40587-b890-4a77-b915-39125e2d7b3e.jsonl b/quickstart/eino_assistant/cmd/einoagent/data/memory/f1d40587-b890-4a77-b915-39125e2d7b3e.jsonl new file mode 100644 index 0000000..63a76b7 --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/data/memory/f1d40587-b890-4a77-b915-39125e2d7b3e.jsonl @@ -0,0 +1,2 @@ +{"role":"user","content":"Eino 好不好"} +{"role":"assistant","content":"Eino是一个有其自身优势的框架。\n\n**一、优点**\n\n1. **项目搭建便捷性**\n - Eino提供了项目脚手架功能,例如通过`eino_tool`中的`init_template`动作可以初始化项目模板。这有助于快速搭建项目结构,节省开发时间。例如,如果要创建一个基于`http_agent`模板的项目,就可以利用这个功能轻松创建相关文件,避免从零开始构建项目的繁琐过程。\n2. **生态资源丰富**\n - 它有相关的示例项目(`eino - examples`)。可以通过`eino_tool`的`get_example_project`动作获取示例项目的路径或网址。这些示例项目能够为开发者提供实际的参考范例,无论是对于新手学习框架的使用,还是对于有经验的开发者查找特定功能的实现方式都非常有帮助。\n3. **文档支持**\n - 可以使用`eino_tool`的`get_doc_url`获取Eino网站的文档网址。有详细的文档有助于开发者深入理解框架的各种功能、使用方法、最佳实践等内容。\n\n4. **社区和开源协作**\n - 其在GitHub上有相关的代码仓库(如`eino`、`eino - ext`等),可以通过`eino_tool`的`get_github_repo`获取仓库网址。开源的性质意味着开发者可以参与到项目的改进、扩展中,也可以从社区中获取支持和分享自己的经验。\n\n5. **任务管理功能集成**\n - 借助`task_manager`工具,可以方便地管理与Eino项目相关的任务。可以添加、获取、更新、删除和列出任务,这有助于在开发过程中对任务进行有效的组织和跟踪。例如,对于一个复杂的Eino项目,开发者可以将各个功能模块的开发任务进行分类管理,明确任务的完成状态、截止日期等信息。\n\n6. **灵活的工具集成**\n - 它集成了像`open`(打开文件/网址)、`gitclone`(克隆或拉取仓库)、`duckduckgo_search`(进行网络搜索)等工具,这些工具可以方便地融入到开发流程中。比如在开发过程中需要参考网络上的资料,可以直接使用`duckduckgo_search`进行查询;如果需要获取外部代码仓库,可以使用`gitclone`;如果要查看项目中的文档或者相关网页,可以使用`open`功能。\n\n**二、是否适合取决于具体需求**\n\n1. **对于小型项目**\n - 如果是小型项目,Eino的便捷性和丰富的示例资源可以让开发者快速上手并完成项目开发,减少不必要的开发成本。\n2. **对于大型项目**\n - 在大型项目中,Eino的框架结构和任务管理功能可以帮助团队更好地组织开发工作。同时,其开源的特性和社区支持也有利于应对项目中可能遇到的复杂问题。但是,如果项目有非常特殊的需求或者与现有架构有很强的依赖关系,可能需要对Eino进行定制或者评估其与现有系统的兼容性。\n\n3. **对于不同技术背景的开发者**\n - 对于熟悉类似框架或者有良好编程基础的开发者来说,Eino的学习曲线相对较平缓,可以快速掌握并利用其优势。而对于初学者来说,虽然有文档和示例项目,但可能需要花费一些时间来理解框架的概念和工作流程。","response_meta":{"finish_reason":"stop","usage":{"prompt_tokens":923,"completion_tokens":745,"total_tokens":1668}}} diff --git a/quickstart/eino_assistant/cmd/einoagent/data/task/tasks.jsonl b/quickstart/eino_assistant/cmd/einoagent/data/task/tasks.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/quickstart/eino_assistant/cmd/einoagent/main.go b/quickstart/eino_assistant/cmd/einoagent/main.go new file mode 100644 index 0000000..890742e --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/main.go @@ -0,0 +1,91 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/cloudwego/eino-ext/devops" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/cmd/einoagent/agent" + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/cmd/einoagent/task" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" +) + +func init() { + if os.Getenv("EINO_DEBUG") != "false" { + err := devops.Init(context.Background()) + if err != nil { + log.Printf("[eino dev] init failed, err=%v", err) + } + } +} + +func main() { + // 获取端口 + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + // 创建 Hertz 服务器 + h := server.Default(server.WithHostPorts(":" + port)) + + h.Use(LogMiddleware()) + + // 注册 task 路由组 + taskGroup := h.Group("/task") + if err := task.BindRoutes(taskGroup); err != nil { + log.Fatal("failed to bind task routes:", err) + } + + // 注册 agent 路由组 + agentGroup := h.Group("/agent") + if err := agent.BindRoutes(agentGroup); err != nil { + log.Fatal("failed to bind agent routes:", err) + } + + // Redirect root path to /agent + h.GET("/", func(ctx context.Context, c *app.RequestContext) { + c.Redirect(302, []byte("/agent")) + }) + + // 启动服务器 + h.Spin() +} + +// LogMiddleware 记录 HTTP 请求日志 +func LogMiddleware() app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + start := time.Now() + path := string(c.Request.URI().Path()) + method := string(c.Request.Method()) + + // 处理请求 + c.Next(ctx) + + // 记录请求信息 + latency := time.Since(start) + statusCode := c.Response.StatusCode() + log.Printf("[HTTP] %s %s %d %v\n", method, path, statusCode, latency) + } +} diff --git a/quickstart/eino_assistant/cmd/einoagent/task/server.go b/quickstart/eino_assistant/cmd/einoagent/task/server.go new file mode 100644 index 0000000..97295cb --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/task/server.go @@ -0,0 +1,97 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package task + +import ( + "context" + "embed" + "mime" + "path/filepath" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/cloudwego/hertz/pkg/route" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/tool/task" +) + +//go:embed web +var webContent embed.FS + +// BindRoutes 注册路由 +func BindRoutes(r *route.RouterGroup) error { + ctx := context.Background() + + taskTool, err := task.NewTaskToolImpl(ctx, &task.TaskToolConfig{ + Storage: task.GetDefaultStorage(), + }) + if err != nil { + return err + } + + // API 处理 + r.POST("/api", func(ctx context.Context, c *app.RequestContext) { + var req task.TaskRequest + if err := c.Bind(&req); err != nil { + c.JSON(consts.StatusBadRequest, map[string]string{ + "status": "error", + "error": err.Error(), + }) + return + } + + resp, err := taskTool.Invoke(ctx, &req) + if err != nil { + c.JSON(consts.StatusInternalServerError, map[string]string{ + "status": "error", + "error": err.Error(), + }) + return + } + + c.JSON(consts.StatusOK, resp) + }) + + // 静态文件服务 + r.GET("/", func(ctx context.Context, c *app.RequestContext) { + content, err := webContent.ReadFile("web/index.html") + if err != nil { + c.String(consts.StatusNotFound, "File not found") + return + } + c.Header("Content-Type", "text/html") + c.Write(content) + }) + + r.GET("/:file", func(ctx context.Context, c *app.RequestContext) { + file := c.Param("file") + content, err := webContent.ReadFile("web/" + file) + if err != nil { + c.String(consts.StatusNotFound, "File not found") + return + } + + contentType := mime.TypeByExtension(filepath.Ext(file)) + if contentType == "" { + contentType = "application/octet-stream" + } + c.Header("Content-Type", contentType) + c.Write(content) + }) + + return nil +} diff --git a/quickstart/eino_assistant/cmd/einoagent/task/web/index.html b/quickstart/eino_assistant/cmd/einoagent/task/web/index.html new file mode 100644 index 0000000..0388ba2 --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/task/web/index.html @@ -0,0 +1,179 @@ + + + + + + Task Manager + + + +
+
+

Task Manager

+ +
+ + + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/quickstart/eino_assistant/cmd/einoagent/task/web/script.js b/quickstart/eino_assistant/cmd/einoagent/task/web/script.js new file mode 100644 index 0000000..4d3a4fd --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/task/web/script.js @@ -0,0 +1,401 @@ +// URL 参数处理 +function getQueryParams() { + const params = new URLSearchParams(window.location.search); + return { + query: params.get('q') || '', + is_done: params.get('done') === null ? null : params.get('done') === 'true', + limit: parseInt(params.get('limit')) || 10, + sort: params.get('sort') || 'urgency' + }; +} + +function updateQueryParams(params) { + const url = new URL(window.location); + if (params.query) url.searchParams.set('q', params.query); + else url.searchParams.delete('q'); + + if (params.is_done !== null) url.searchParams.set('done', params.is_done); + else url.searchParams.delete('done'); + + if (params.limit !== 10) url.searchParams.set('limit', params.limit); + else url.searchParams.delete('limit'); + + if (params.sort !== 'urgency') url.searchParams.set('sort', params.sort); + else url.searchParams.delete('sort'); + + window.history.pushState({}, '', url); +} + +// 表单初始化 +function initializeFormValues() { + const params = getQueryParams(); + document.getElementById('searchInput').value = params.query; + document.getElementById('statusFilter').value = params.is_done === null ? '' : params.is_done; + document.getElementById('limitFilter').value = params.limit; + document.getElementById('sortFilter').value = params.sort; +} + +// 时间处理函数 +function formatDate(dateStr) { + if (!dateStr) return ''; + const date = new Date(dateStr); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); +} + +function calculateUrgency(task) { + if (!task.deadline || task.completed) return Infinity; + const now = new Date(); + const deadline = new Date(task.deadline); + const hoursLeft = (deadline - now) / (1000 * 60 * 60); + return hoursLeft; +} + +function formatTimeDiff(diffMs) { + const hours = Math.floor(Math.abs(diffMs) / (1000 * 60 * 60)); + const minutes = Math.floor((Math.abs(diffMs) % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((Math.abs(diffMs) % (1000 * 60)) / 1000); + + if (hours > 2) { + return `${hours} 小时`; + } else if (hours > 0) { + return `${hours} 小时 ${minutes} 分钟`; + } else if (minutes > 0) { + return `${minutes} 分钟 ${seconds} 秒`; + } else { + return `${seconds} 秒`; + } +} + +function getDeadlineStatus(deadline, completed) { + if (!deadline) return null; + if (completed) return { status: 'completed', class: 'text-gray-500' }; + + const now = new Date(); + const deadlineDate = new Date(deadline); + const hoursLeft = (deadlineDate - now) / (1000 * 60 * 60); + + if (hoursLeft < 0) return { status: 'error', class: 'text-red-600 font-bold' }; + if (hoursLeft <= 12) return { status: 'warning', class: 'text-yellow-600 font-bold' }; + return { status: 'normal', class: 'text-gray-500' }; +} + +// 倒计时处理 +function updateCountdown(element, deadline) { + const now = new Date(); + const diffMs = deadline - now; + + if (diffMs <= 0) { + loadTasks(); + return; + } + + let text = `截止: ${formatDate(deadline)}`; + if (diffMs > 0) { + text += ` (剩余 ${formatTimeDiff(diffMs)})`; + } else { + text += ` (已超出 ${formatTimeDiff(Math.abs(diffMs))})`; + } + + element.textContent = text; +} + +// Task 列表处理 +async function loadTasks() { + const params = getQueryParams(); + try { + const response = await fetch('/task/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'list', + list: params + }) + }); + const data = await response.json(); + if (data.status === 'success') { + renderTasks(data.task_list); + } + } catch (error) { + console.error('Failed to load tasks:', error); + } +} + +function renderTasks(tasks) { + const list = document.getElementById('taskList'); + const template = document.getElementById('taskTemplate'); + list.innerHTML = ''; + + // 清除所有现有的倒计时定时器 + if (window.countdownTimers) { + window.countdownTimers.forEach(timer => clearInterval(timer)); + } + window.countdownTimers = []; + + // 排序 + const sortType = document.getElementById('sortFilter').value; + tasks.sort((a, b) => { + if (sortType === 'urgency') { + return calculateUrgency(a) - calculateUrgency(b); + } else if (sortType === 'deadline') { + if (!a.deadline) return 1; + if (!b.deadline) return -1; + return new Date(a.deadline) - new Date(b.deadline); + } else { // created + return new Date(b.created_at) - new Date(a.created_at); + } + }); + + tasks.forEach(task => { + const clone = template.content.cloneNode(true); + const item = clone.querySelector('.task-item'); + + item.dataset.id = task.id; + item.dataset.task = JSON.stringify(task); + item.querySelector('.task-checkbox').checked = task.completed; + item.querySelector('.task-title').textContent = task.title; + item.querySelector('.task-content').textContent = task.content; + item.querySelector('.task-created').textContent = `创建: ${formatDate(task.created_at)}`; + + if (task.deadline) { + const deadlineEl = item.querySelector('.task-deadline'); + const status = getDeadlineStatus(task.deadline, task.completed); + deadlineEl.className = `task-deadline ${status.class}`; + + const now = new Date(); + const deadline = new Date(task.deadline); + const hoursLeft = (deadline - now) / (1000 * 60 * 60); + const needsCountdown = !task.completed && hoursLeft > 0 && hoursLeft <= 2; + + if (needsCountdown) { + updateCountdown(deadlineEl, deadline); + const timer = setInterval(() => { + updateCountdown(deadlineEl, deadline); + }, 1000); + window.countdownTimers.push(timer); + } else { + const diffMs = deadline - now; + let text = `截止: ${formatDate(task.deadline)}`; + if (!task.completed) { + text += ` (${diffMs > 0 ? '剩余 ' : '已超出 '}${formatTimeDiff(diffMs)})`; + } + deadlineEl.textContent = text; + } + } + + if (task.completed) { + item.querySelector('.task-title').classList.add('line-through', 'text-gray-500'); + item.querySelector('.task-content').classList.add('line-through', 'text-gray-500'); + } + + list.appendChild(clone); + }); +} + +// 对话框处理 +function openAddDialog() { + document.getElementById('addDialog').classList.remove('hidden'); + document.getElementById('addForm').reset(); +} + +function closeAddDialog() { + document.getElementById('addDialog').classList.add('hidden'); +} + +function openEditDialog(task) { + const dialog = document.getElementById('editDialog'); + const form = document.getElementById('editForm'); + + form.id.value = task.id; + form.title.value = task.title; + form.content.value = task.content; + form.deadline.value = task.deadline ? task.deadline.slice(0, 16) : ''; + + dialog.classList.remove('hidden'); +} + +function closeEditDialog() { + document.getElementById('editDialog').classList.add('hidden'); +} + +// 事件处理 +function debounce(func, delay) { + let timer; + return function() { + clearTimeout(timer); + timer = setTimeout(() => func.apply(this, arguments), delay); + } +} + +const handleFilterChange = debounce(() => { + const params = { + query: document.getElementById('searchInput').value, + is_done: document.getElementById('statusFilter').value === '' ? null : document.getElementById('statusFilter').value === 'true', + limit: parseInt(document.getElementById('limitFilter').value), + sort: document.getElementById('sortFilter').value + }; + updateQueryParams(params); + loadTasks(); +}, 300); + +// 在文件顶部添加定时器变量 +let autoRefreshTimer; + +// 初始化事件监听 +document.addEventListener('DOMContentLoaded', () => { + // 添加任务 + document.getElementById('addTaskBtn').addEventListener('click', openAddDialog); + document.getElementById('addForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const form = e.target; + const task = { + title: form.title.value, + content: form.content.value, + deadline: form.deadline.value + }; + + try { + const response = await fetch('/task/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'add', + task: task + }) + }); + const data = await response.json(); + if (data.status === 'success') { + closeAddDialog(); + form.reset(); + loadTasks(); + } + } catch (error) { + console.error('Failed to add task:', error); + } + }); + + // 更新任务状态 + document.getElementById('taskList').addEventListener('change', async (e) => { + if (e.target.matches('.task-checkbox')) { + const item = e.target.closest('.task-item'); + const id = item.dataset.id; + const completed = e.target.checked; + + try { + const response = await fetch('/task/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'update', + task: { id, completed } + }) + }); + const data = await response.json(); + if (data.status === 'success') { + loadTasks(); + } + } catch (error) { + console.error('Failed to update task:', error); + } + } + }); + + // 删除任务 + document.getElementById('taskList').addEventListener('click', async (e) => { + if (e.target.closest('.delete-btn')) { + const item = e.target.closest('.task-item'); + const id = item.dataset.id; + + if (!confirm('确定要删除这个任务吗?')) return; + + try { + const response = await fetch('/task/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'delete', + task: { id } + }) + }); + const data = await response.json(); + if (data.status === 'success') { + loadTasks(); + } + } catch (error) { + console.error('Failed to delete task:', error); + } + } + }); + + // 编辑任务 + document.getElementById('taskList').addEventListener('click', async (e) => { + if (e.target.closest('.edit-btn')) { + const item = e.target.closest('.task-item'); + const task = JSON.parse(item.dataset.task); + openEditDialog(task); + } + }); + + document.getElementById('editForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const form = e.target; + const task = { + id: form.id.value, + title: form.title.value, + content: form.content.value, + deadline: form.deadline.value + }; + + try { + const response = await fetch('/task/api', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'update', + task: task + }) + }); + const data = await response.json(); + if (data.status === 'success') { + closeEditDialog(); + loadTasks(); + } + } catch (error) { + console.error('Failed to update task:', error); + } + }); + + // 筛选事件 + document.getElementById('searchInput').addEventListener('input', handleFilterChange); + document.getElementById('statusFilter').addEventListener('change', handleFilterChange); + document.getElementById('limitFilter').addEventListener('change', handleFilterChange); + document.getElementById('sortFilter').addEventListener('change', handleFilterChange); + + // 浏览器前进/后退 + window.addEventListener('popstate', () => { + initializeFormValues(); + loadTasks(); + }); + + // 设置自动刷新定时器 + autoRefreshTimer = setInterval(loadTasks, 10000); // 每10秒刷新一次 + + // 修改页面卸载事件处理,清理所有定时器 + window.addEventListener('beforeunload', () => { + if (window.countdownTimers) { + window.countdownTimers.forEach(timer => clearInterval(timer)); + } + if (autoRefreshTimer) { + clearInterval(autoRefreshTimer); + } + }); + + // 初始化 + initializeFormValues(); + loadTasks(); +}); \ No newline at end of file diff --git a/quickstart/eino_assistant/cmd/einoagent/task/web/style.css b/quickstart/eino_assistant/cmd/einoagent/task/web/style.css new file mode 100644 index 0000000..2e8ba5b --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagent/task/web/style.css @@ -0,0 +1,78 @@ +/* 自定义样式补充 */ +.task-item.completed .task-title { + text-decoration: line-through; + color: #9CA3AF; +} + +.task-item.completed .task-content { + color: #9CA3AF; +} + +.task-checkbox { + width: 1.2rem; + height: 1.2rem; + cursor: pointer; +} + +.delete-task { + opacity: 0; + transition: opacity 0.2s; +} + +.task-item:hover .delete-task { + opacity: 1; +} + +/* 输入框样式优化 */ +input[type="text"], +input[type="datetime-local"], +textarea, +select { + border: 1px solid #D1D5DB; + padding: 0.5rem; + border-radius: 0.375rem; + width: 100%; + outline: none; + background-color: white; + color: #1F2937; +} + +input[type="text"]:focus, +input[type="datetime-local"]:focus, +textarea:focus, +select:focus { + border-color: #3B82F6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +textarea { + min-height: 100px; + resize: vertical; +} + +/* 动画效果 */ +.task-item { + transition: transform 0.2s, box-shadow 0.2s; +} + +.task-item:hover { + transform: translateY(-1px); +} + +/* 响应式调整 */ +@media (max-width: 640px) { + .container { + padding-left: 1rem; + padding-right: 1rem; + } + + .flex.justify-between { + flex-direction: column; + gap: 1rem; + } + + .flex.space-x-4 { + flex-direction: column; + gap: 0.5rem; + } +} \ No newline at end of file diff --git a/quickstart/eino_assistant/cmd/einoagentcli/main.go b/quickstart/eino_assistant/cmd/einoagentcli/main.go new file mode 100644 index 0000000..892ac41 --- /dev/null +++ b/quickstart/eino_assistant/cmd/einoagentcli/main.go @@ -0,0 +1,247 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "log" + "math/rand" + "os" + "strconv" + "strings" + + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/eino/einoagent" + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/mem" +) + +var id = flag.String("id", "", "conversation id") + +var memory = mem.GetDefaultMemory() + +var cbHandler callbacks.Handler + +func main() { + flag.Parse() + + // 开启 Eino 的可视化调试能力 + err := devops.Init(context.Background()) + if err != nil { + log.Printf("[eino dev] init failed, err=%v", err) + return + } + + if *id == "" { + *id = strconv.Itoa(rand.Intn(1000000)) + } + + ctx := context.Background() + + err = Init() + if err != nil { + log.Printf("[eino agent] init failed, err=%v", err) + return + } + + // Start interactive dialogue + reader := bufio.NewReader(os.Stdin) + for { + fmt.Printf("🧑‍ : ") + input, err := reader.ReadString('\n') + if err != nil { + fmt.Printf("Error reading input: %v\n", err) + return + } + + input = strings.TrimSpace(input) + if input == "" || input == "exit" || input == "quit" { + return + } + + // Call RunAgent with the input + sr, err := RunAgent(ctx, *id, input) + if err != nil { + fmt.Printf("Error from RunAgent: %v\n", err) + continue + } + + // Print the response + fmt.Print("🤖 : ") + for { + msg, err := sr.Recv() + if err != nil { + if err == io.EOF { + break + } + fmt.Printf("Error receiving message: %v\n", err) + break + } + fmt.Print(msg.Content) + } + fmt.Println() + fmt.Println() + } +} + +func Init() error { + + os.MkdirAll("log", 0755) + var f *os.File + f, err := os.OpenFile("log/eino.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return err + } + + cbConfig := &LogCallbackConfig{ + Detail: true, + Writer: f, + } + if os.Getenv("DEBUG") == "true" { + cbConfig.Debug = true + } + // this is for invoke option of WithCallback + cbHandler = LogCallback(cbConfig) + + // init global callback, for trace and metrics + if os.Getenv("LANGFUSE_PUBLIC_KEY") != "" && os.Getenv("LANGFUSE_SECRET_KEY") != "" { + fmt.Println("[eino agent] INFO: use langfuse as callback, watch at: https://cloud.langfuse.com") + cbh, _ := langfuse.NewLangfuseHandler(&langfuse.Config{ + Host: "https://cloud.langfuse.com", + PublicKey: os.Getenv("LANGFUSE_PUBLIC_KEY"), + SecretKey: os.Getenv("LANGFUSE_SECRET_KEY"), + Name: "Eino Assistant", + Public: true, + Release: "release/v0.0.1", + UserID: "eino_god", + Tags: []string{"eino", "assistant"}, + }) + callbacks.InitCallbackHandlers([]callbacks.Handler{cbh}) + } + + return nil +} + +func RunAgent(ctx context.Context, id string, msg string) (*schema.StreamReader[*schema.Message], error) { + + runner, err := einoagent.BuildEinoAgent(ctx, &einoagent.BuildConfig{ + EinoAgent: &einoagent.EinoAgentBuildConfig{}, + }) + if err != nil { + return nil, fmt.Errorf("failed to build agent graph: %w", err) + } + + conversation := memory.GetConversation(id, true) + + userMessage := &einoagent.UserMessage{ + ID: id, + Query: msg, + History: conversation.GetMessages(), + } + + sr, err := runner.Stream(ctx, userMessage, compose.WithCallbacks(cbHandler)) + if err != nil { + return nil, fmt.Errorf("failed to stream: %w", err) + } + + srs := sr.Copy(2) + + go func() { + // for save to memory + fullMsgs := make([]*schema.Message, 0) + + defer func() { + // close stream if you used it + srs[1].Close() + + // add user input to history + conversation.Append(schema.UserMessage(msg)) + + fullMsg, err := schema.ConcatMessages(fullMsgs) + if err != nil { + fmt.Println("error concatenating messages: ", err.Error()) + } + // add agent response to history + conversation.Append(fullMsg) + }() + + outer: + for { + select { + case <-ctx.Done(): + fmt.Println("context done", ctx.Err()) + return + default: + chunk, err := srs[1].Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break outer + } + } + + fullMsgs = append(fullMsgs, chunk) + } + } + }() + + return srs[0], nil +} + +type LogCallbackConfig struct { + Detail bool + Debug bool + Writer io.Writer +} + +func LogCallback(config *LogCallbackConfig) callbacks.Handler { + if config == nil { + config = &LogCallbackConfig{ + Detail: true, + Writer: os.Stdout, + } + } + if config.Writer == nil { + config.Writer = os.Stdout + } + builder := callbacks.NewHandlerBuilder() + builder.OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + fmt.Fprintf(config.Writer, "[view]: start [%s:%s:%s]\n", info.Component, info.Type, info.Name) + if config.Detail { + var b []byte + if config.Debug { + b, _ = json.MarshalIndent(input, "", " ") + } else { + b, _ = json.Marshal(input) + } + fmt.Fprintf(config.Writer, "%s\n", string(b)) + } + return ctx + }) + builder.OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + fmt.Fprintf(config.Writer, "[view]: end [%s:%s:%s]\n", info.Component, info.Type, info.Name) + return ctx + }) + return builder.Build() +} diff --git a/quickstart/eino_assistant/cmd/knowledgeindexing/eino-docs/_index.md b/quickstart/eino_assistant/cmd/knowledgeindexing/eino-docs/_index.md new file mode 100644 index 0000000..4bd3fba --- /dev/null +++ b/quickstart/eino_assistant/cmd/knowledgeindexing/eino-docs/_index.md @@ -0,0 +1,55 @@ + +> Eino 发音:美 / 'aino /,近似音: i know,有希望应用程序达到 "i know" 的愿景 + +# Eino 是什么 + +> 💡 +> Go AI 集成组件的研发框架。 + +Eino 旨在提供 Golang 语言的 AI 应用开发框架。 Eino 参考了开源社区中诸多优秀的 AI 应用开发框架,例如 LangChain、LangGraph、LlamaIndex 等,提供了更符合 Golang 编程习惯的 AI 应用开发框架。 + +Eino 提供了丰富的辅助 AI 应用开发的**原子组件**、**集成组件**、**组件编排**、**切面扩展**等能力,可以帮助开发者更加简单便捷地开发出架构清晰、易维护、高可用的 AI 应用。 + +以 React Agent 为例: + +- 提供了 ChatModel、ToolNode、PromptTemplate 等常用组件,并且业务可定制、可扩展。 +- 可基于现有组件进行灵活编排,产生集成组件,在业务服务中使用。 + +![](/img/eino/react_agent_graph.png) + +# Eino 组件 + +> Eino 的其中一个目标是:搜集和完善 AI 应用场景下的组件体系,让业务可轻松找到一些通用的 AI 组件,方便业务的迭代。 + +Eino 会围绕 AI 应用的场景,提供具有比较好的抽象的组件,并围绕该抽象提供一些常用的实现。 + +- Eino 组件的抽象定义在:[eino/components](https://github.com/cloudwego/eino/tree/main/components) +- Eino 组件的实现在:[eino-ext/components](https://github.com/cloudwego/eino-ext/tree/main/components) + +# Eino 应用场景 + +得益于 Eino 轻量化和内场亲和属性,用户只需短短数行代码就能给你的存量微服务引入强力的大模型能力,让传统微服务进化出 AI 基因。 + +可能大家听到【Graph 编排】这个词时,第一反应就是将整个应用接口的实现逻辑进行分段、分层的逻辑拆分,并将其转换成可编排的 Node。 这个过程中遇到的最大问题就是**长距离的上下文传递(跨 Node 节点的变量传递)**问题,无论是使用 Graph/Chain 的 State,还是使用 Options 透传,整个编排过程都极其复杂,远没有直接进行函数调用简单。 + +基于当前的 Graph 编排能力,适合编排的场景具有如下几个特点: + +- 整体是围绕模型的语义处理相关能力。这里的语义不限模态 +- 编排产物中有极少数节点是 Session 相关的。整体来看,绝大部分节点没有类似用户/设备等不可枚举地业务实体粒度的处理逻辑 + + - 无论是通过 Graph/Chain 的 State、还是通过 CallOptions,对于读写或透传用户/设备粒度的信息的方式,均不简便 +- 需要公共的切面能力,基于此建设观测、限流、评测等横向治理能力 + +编排的意义是什么: 把长距离的编排元素上下文以固定的范式进行聚合控制和呈现。 + +**整体来说,“Graph 编排”适用的场景是: 业务定制的 AI 集成组件。 ****即把 AI 相关的原子能力,进行灵活编排****,提供简单易用的场景化的 AI 组件。 并且该 AI 组件中,具有统一且完整的横向治理能力。** + +- 推荐的使用方式 + +![](/img/eino/recommend_way_of_handler.png) + +- 挑战较大的方式 -- 【业务全流程的节点编排】 + - Biz Handler 一般重业务逻辑,轻数据流,比较适合函数栈调用的方式进行开发 + - 如果采用图编排的方式进行逻辑划分与组合,会增大业务逻辑开发的难度 + +![](/img/eino/big_challenge_graph.png) diff --git a/quickstart/eino_assistant/cmd/knowledgeindexing/eino-docs/agent_llm_with_tools.md b/quickstart/eino_assistant/cmd/knowledgeindexing/eino-docs/agent_llm_with_tools.md new file mode 100644 index 0000000..36f7f13 --- /dev/null +++ b/quickstart/eino_assistant/cmd/knowledgeindexing/eino-docs/agent_llm_with_tools.md @@ -0,0 +1,247 @@ +## **Agent 是什么** + +Agent(智能代理)是一个能够感知环境并采取行动以实现特定目标的系统。在 AI 应用中,Agent 通过结合大语言模型的理解能力和预定义工具的执行能力,可以自主地完成复杂的任务。是未来 AI 应用到生活生产中主要的形态。 + +> 💡 +> 本文中示例的代码片段详见:[eino-examples/quickstart/taskagent](https://github.com/cloudwego/eino-examples/blob/master/quickstart/taskagent/main.go) + +## **Agent 的核心组成** + +在 Eino 中,要实现 Agent 主要需要两个核心部分:ChatModel 和 Tool。 + +### **ChatModel** + +ChatModel 是 Agent 的大脑,它通过强大的语言理解能力来处理用户的自然语言输入。当用户提出请求时,ChatModel 会深入理解用户的意图,分析任务需求,并决定是否需要调用特定的工具来完成任务。在需要使用工具时,它能够准确地选择合适的工具并生成正确的参数。不仅如此,ChatModel 还能将工具执行的结果转化为用户易于理解的自然语言回应,实现流畅的人机对话。 + +> 更详细的 ChatModel 的信息,可以参考: [Eino: ChatModel 使用说明](/zh/docs/eino/core_modules/components/chat_model_guide) + +### **Tool** + +Tool 是 Agent 的执行器,提供了具体的功能实现。每个 Tool 都有明确的功能定义和参数规范,使 ChatModel 能够准确地调用它们。Tool 可以实现各种功能,从简单的数据操作到复杂的外部服务调用都可以封装成 Tool。 + +> 更详细关于 Tool 和 ToolsNode 的信息,可参考: [Eino: ToolsNode 使用说明](/zh/docs/eino/core_modules/components/tools_node_guide) + +## **Tool 的实现方式** + +在 Eino 中,我们提供了多种方式来实现 Tool。下面通过一个待办事项(Task)管理系统的例子来说明。 + +### **方式一:使用 NewTool 构建** + +这种方式适合简单的工具实现,通过定义工具信息和处理函数来创建 Tool: + +```go +func getAddTaskTool() tool.InvokableTool { + info := &schema.ToolInfo{ + Name: "add_task", + Desc: "Add a task item", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "content": { + Desc: "The content of the task item", + Type: schema.String, + Required: true, + }, + "started_at": { + Desc: "The started time of the task item, in unix timestamp", + Type: schema.Integer, + }, + "deadline": { + Desc: "The deadline of the task item, in unix timestamp", + Type: schema.Integer, + }, + }), + } + + return utils.NewTool(info, AddTaskFunc) +} +``` + +这种方式虽然直观,但存在一个明显的缺点:需要在 ToolInfo 中手动定义参数信息(ParamsOneOf),和实际的参数结构(TaskAddParams)是分开定义的。这样不仅造成了代码的冗余,而且在参数发生变化时需要同时修改两处地方,容易导致不一致,维护起来也比较麻烦。 + +### **方式二:使用 InferTool 构建** + +这种方式更加简洁,通过结构体的 tag 来定义参数信息,就能实现参数结构体和描述信息同源,无需维护两份信息: + +```go +type TaskUpdateParams struct { + ID string `json:"id" jsonschema:"description=id of the task"` + Content *string `json:"content,omitempty" jsonschema:"description=content of the task"` + StartedAt *int64 `json:"started_at,omitempty" jsonschema:"description=start time in unix timestamp"` + Deadline *int64 `json:"deadline,omitempty" jsonschema:"description=deadline of the task in unix timestamp"` + Done *bool `json:"done,omitempty" jsonschema:"description=done status"` +} + +// 使用 InferTool 创建工具 +updateTool, err := utils.InferTool("update_task", "Update a task item, eg: content,deadline...", UpdateTaskFunc) +``` + +### **方式三:实现 Tool 接口** + +对于需要更多自定义逻辑的场景,可以通过实现 Tool 接口来创建: + +```go +type ListTaskTool struct {} + +func (lt *ListTaskTool) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: "list_task", + Desc: "List all task items", + ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{ + "finished": { + Desc: "filter task items if finished", + Type: schema.Boolean, + Required: false, + }, + }), + }, nil +} + +func (lt *ListTaskTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) { + // 具体的调用逻辑 +} +``` + +### **方式四:使用官方封装的工具** + +除了自己实现工具,我们还提供了许多开箱即用的工具。这些工具经过充分测试和优化,可以直接集成到你的 Agent 中。以 Google Search 工具为例: + +```go +import ( + "github.com/bytedance/eino-ext/components/tool/googlesearch" +) + +func main() { + // 创建 Google Search 工具 + searchTool, err := googlesearch.NewGoogleSearchTool(ctx, &googlesearch.Config{ + APIKey: os.Getenv("GOOGLE_API_KEY"), // Google API Key + SearchEngineID: os.Getenv("GOOGLE_SEARCH_ENGINE_ID"), // 自定义搜索引擎 ID + Num: 5, // 每次返回的结果数量 + Lang: "zh-CN", // 搜索结果的语言 + }) + if err != nil { + log.Fatal(err) + } +} +``` + +使用 eino-ext 提供的工具不仅能避免重复开发的工作量,还能确保工具的稳定性和可靠性。这些工具都经过充分测试和持续维护,可以直接集成到项目中使用。 + +## **用 Chain 构建 Agent** + +在构建 Agent 时,ToolsNode 是一个核心组件,它负责管理和执行工具调用。ToolsNode 可以集成多个工具,并提供统一的调用接口。它支持同步调用(Invoke)和流式调用(Stream)两种方式,能够灵活地处理不同类型的工具执行需求。 + +要创建一个 ToolsNode,你需要提供一个工具列表配置: + +```go +func main() { + conf := &compose.ToolsNodeConfig{ + Tools: []tool.BaseTool{tool1, tool2}, // 工具可以是 InvokableTool 或 StreamableTool + } + toolsNode, err := compose.NewToolNode(ctx, conf) +} +``` + +下面是一个完整的 Agent 示例,它使用 OpenAI 的 ChatModel 并结合了上述的 Task 工具: + +```go +func main() { + // 初始化 tools + taskTools := []tool.BaseTool{ + getAddTaskTool(), // 使用 NewTool 方式 + updateTool, // 使用 InferTool 方式 + &ListTaskTool{}, + searchTool, // 使用结构体实现方式, 此处未实现底层逻辑 + } + + // 创建并配置 ChatModel + temp := float32(0.7) + chatModel, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{ + Model: "gpt-4", + APIKey: os.Getenv("OPENAI_API_KEY"), + Temperature: &temp, + }) + if err != nil { + log.Fatal(err) + } + + // 获取工具信息, 用于绑定到 ChatModel + toolInfos := make([]*schema.ToolInfo, 0, len(taskTools)) + for _, tool := range taskTools { + info, err := tool.Info(ctx) + if err != nil { + log.Fatal(err) + } + toolInfos = append(toolInfos, info) + } + + // 将 tools 绑定到 ChatModel + err = chatModel.BindTools(toolInfos) + if err != nil { + log.Fatal(err) + } + + + // 创建 tools 节点 + taskToolsNode, err := compose.NewToolNode(context.Background(), &compose.ToolsNodeConfig{ + Tools: taskTools, + }) + if err != nil { + log.Fatal(err) + } + + // 构建完整的处理链 + chain := compose.NewChain[*schema.Message, []*schema.Message]() + chain. + AppendChatModel(chatModel, compose.WithNodeName("chat_model")). + AppendToolsNode(taskToolsNode, compose.WithNodeName("tools")) + + // 编译并运行 chain + agent, err := chain.Compile(ctx) + if err != nil { + log.Fatal(err) + } + + // 运行示例 + resp, err := agent.Invoke(context.Background(), &schema.Message{ + Content: "帮我创建一个明天下午3点截止的待办事项:准备Eino项目演示文稿", + }) + if err != nil { + log.Fatal(err) + } + + // 输出结果 + for _, msg := range resp { + fmt.Println(msg.Content) + } +} +``` + +这个示例有一个假设,也就是 ChatModel 一定会做出 tool 调用的决策。实际上这个例子是 tool calling agent 的一个简化版本。更完整的 toolcalling agent 可以参考: [Tool Calling Agent](/zh/docs/eino/usage_guide/examples_collection/task_manager_implementation) + +## **使用其他方式构建 Agent** + +除了上述使用 Chain/Graph 构建的 agent 之外,Eino 还提供了常用的 Agent 模式的封装。 + +### **ReAct Agent** + +ReAct(Reasoning + Acting)Agent 结合了推理和行动能力,通过思考-行动-观察的循环来解决复杂问题。它能够在执行任务时进行深入的推理,并根据观察结果调整策略,特别适合需要多步推理的复杂场景。 + +> 更详细的 react agent 可以参考: [Eino: React Agent 使用手册](/zh/docs/eino/core_modules/flow_integration_components/react_agent_manual) + +### **Multi Agent** + +Multi Agent 系统由多个协同工作的 Agent 组成,每个 Agent 都有其特定的职责和专长。通过 Agent 间的交互与协作,可以处理更复杂的任务,实现分工协作。这种方式特别适合需要多个专业领域知识结合的场景。 + +> 更详细的 multi agent 可以参考: [Eino Tutorial: Host Multi-Agent ](/zh/docs/eino/core_modules/flow_integration_components/multi_agent_hosting) + +## **总结** + +介绍了使用 Eino 框架构建 Agent 的基本方法。通过 Chain、Tool Calling 和 ReAct 等不同方式,我们可以根据实际需求灵活地构建 AI Agent。 + +Agent 是 AI 技术发展的重要方向。它不仅能够理解用户意图,还能主动采取行动,通过调用各种工具来完成复杂任务。随着大语言模型能力的不断提升,Agent 将在未来扮演越来越重要的角色,成为连接 AI 与现实世界的重要桥梁。我们期待 Eino 能为用户带来更强大、易用的 agent 构建方案,推动更多基于 Agent 的应用创新。 + +## **关联阅读** + +- 快速开始 + - [实现一个最简 LLM 应用-ChatModel](/zh/docs/eino/quick_start/simple_llm_application) + - [和幻觉说再见-RAG 召回再回答](/zh/docs/eino/quick_start/rag_retrieval_qa) + - [复杂业务逻辑的利器-编排](/zh/docs/eino/quick_start/complex_business_logic_orchestration) diff --git a/quickstart/eino_assistant/cmd/knowledgeindexing/main.go b/quickstart/eino_assistant/cmd/knowledgeindexing/main.go new file mode 100644 index 0000000..fb144d5 --- /dev/null +++ b/quickstart/eino_assistant/cmd/knowledgeindexing/main.go @@ -0,0 +1,158 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "fmt" + "io/fs" + "path/filepath" + "strings" + + "github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/components/embedding" + "github.com/redis/go-redis/v9" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/eino/knowledgeindexing" +) + +func main() { + ctx := context.Background() + + err := indexMarkdownFiles(ctx, "./eino-docs") + if err != nil { + panic(err) + } + + fmt.Println("index success") +} + +func indexMarkdownFiles(ctx context.Context, dir string) error { + runner, err := knowledgeindexing.BuildKnowledgeIndexing(ctx, &knowledgeindexing.BuildConfig{ + KnowledgeIndexing: &knowledgeindexing.KnowledgeIndexingBuildConfig{ + MarkdownSplitterKeyOfDocumentTransformer: &markdown.HeaderConfig{ + Headers: map[string]string{ + "#": "title", + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("build index graph failed: %w", err) + } + + // 遍历 dir 下的所有 markdown 文件 + err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walk dir failed: %w", err) + } + if d.IsDir() { + return nil + } + + if !strings.HasSuffix(path, ".md") { + fmt.Printf("[skip] not a markdown file: %s\n", path) + return nil + } + + fmt.Printf("[start] indexing file: %s\n", path) + + ids, err := runner.Invoke(ctx, document.Source{URI: path}) + if err != nil { + return fmt.Errorf("invoke index graph failed: %w", err) + } + + fmt.Printf("[done] indexing file: %s, len of parts: %d\n", path, len(ids)) + + return nil + }) + + return err +} + +type RedisVectorStoreConfig struct { + RedisKeyPrefix string + IndexName string + Embedding embedding.Embedder + Dimension int + MinScore float64 + RedisAddr string +} + +func initVectorIndex(ctx context.Context, config *RedisVectorStoreConfig) (err error) { + if config.Embedding == nil { + return fmt.Errorf("embedding cannot be nil") + } + if config.Dimension <= 0 { + return fmt.Errorf("dimension must be positive") + } + + client := redis.NewClient(&redis.Options{ + Addr: config.RedisAddr, + }) + + // 确保在错误时关闭连接 + defer func() { + if err != nil { + client.Close() + } + }() + + if err = client.Ping(ctx).Err(); err != nil { + return fmt.Errorf("failed to connect to Redis: %w", err) + } + + indexName := fmt.Sprintf("%s%s", config.RedisKeyPrefix, config.IndexName) + + // 检查是否存在索引 + exists, err := client.Do(ctx, "FT.INFO", indexName).Result() + if err != nil { + if !strings.Contains(err.Error(), "Unknown index name") { + return fmt.Errorf("failed to check if index exists: %w", err) + } + err = nil + } else if exists != nil { + return nil + } + + // Create new index + createIndexArgs := []interface{}{ + "FT.CREATE", indexName, + "ON", "HASH", + "PREFIX", "1", config.RedisKeyPrefix, + "SCHEMA", + "content", "TEXT", + "metadata", "TEXT", + "vector", "VECTOR", "FLAT", + "6", + "TYPE", "FLOAT32", + "DIM", config.Dimension, + "DISTANCE_METRIC", "COSINE", + } + + if err = client.Do(ctx, createIndexArgs...).Err(); err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + + // 验证索引是否创建成功 + if _, err = client.Do(ctx, "FT.INFO", indexName).Result(); err != nil { + return fmt.Errorf("failed to verify index creation: %w", err) + } + + return nil +} diff --git a/quickstart/eino_assistant/data/memory/ce10d8a8-a866-474f-919a-c1ef094c8236.jsonl b/quickstart/eino_assistant/data/memory/ce10d8a8-a866-474f-919a-c1ef094c8236.jsonl new file mode 100644 index 0000000..ba1735b --- /dev/null +++ b/quickstart/eino_assistant/data/memory/ce10d8a8-a866-474f-919a-c1ef094c8236.jsonl @@ -0,0 +1,8 @@ +{"role":"user","content":"你好,可以帮我介绍一下 Eino 的优势吗? Thanks♪(・ω・)ノ"} +{"role":"assistant","content":"Eino具有以下优势:\n- **借鉴优秀框架且符合Go语言习惯**:Eino参考了开源社区中诸多优秀的AI应用开发框架,例如LangChain、LangGraph、LlamaIndex 等,提供了更符合Golang编程习惯的AI应用开发框架。 \n- **编排优势明显**:以组件为第一编排对象,同时提供具有极强扩展能力的Lambda节点作为编排对象,能够实现快速上手和定制扩展的双优势。并且编排过程中强化了 “数据流” ,callbacks 提供了观测和调试的基础能力。 \n- **助力项目精准定义与成本控制**:其AI处理功能可帮助用户准确地定义项目,进而降低部署成本。\n- **响应灵活**:能快速响应不断变化的客户或项目需求,让用户对所做更改充满信心。 \n- **支持团队协作**:支持团队成员在单个项目上进行协作,并可通过编辑历史和项目状态进行管理。 \n\n你可以点击[此处](https://www.cloudwego.io/zh/docs/eino/overview/)查看更多相关信息。 ","response_meta":{"finish_reason":"stop","usage":{"prompt_tokens":1973,"completion_tokens":234,"total_tokens":2207}}} +{"role":"user","content":"Eino 的项目地址是什么?官方文档在哪里?可以帮我打开下吗"} +{"role":"assistant","content":"Eino 的官方文档地址为:https://www.cloudwego.io/zh/docs/eino/ ,我已经帮你打开了该文档链接。如果你需要了解 Eino 项目地址等更多信息,可以在文档中进一步查找。如果你还有其他需求,请随时告诉我。 ","response_meta":{"finish_reason":"stop","usage":{"prompt_tokens":1102,"completion_tokens":59,"total_tokens":1161}}} +{"role":"user","content":"在网上查一下 cloudwego 有什么新的项目发布吗?"} +{"role":"assistant","content":"为你查询到CloudWeGo的相关信息如下:\n- CloudWeGo是一套由字节跳动开源的、可快速构建企业级云原生微服务架构的中间件集合。该项目共同的特点是高性能、高扩展性、高可靠,专注于微服务通信与治理。\n- 第一批开源了四个项目:Kitex、Netpoll、Thriftgo和netpoll-http2, 以RPC框架Kitex和网络库Netpoll为主。最近又发布了面向 AI 应用场景的开发框架 Eino,致力于让开发者轻松构建高质量的 AI 应用。\n\n更多信息可查看[CloudWeGo官网](https://www.cloudwego.io/)。","response_meta":{"finish_reason":"stop","usage":{"prompt_tokens":2377,"completion_tokens":186,"total_tokens":2563}}} +{"role":"user","content":"帮我定一个大后天晚上 19:00 的 todo,要把 Eino 的 example 通读一遍"} +{"role":"assistant","content":"已成功为你添加一个待办事项:在2025-1-17 19:00,把 Eino 的 example 通读一遍。","response_meta":{"finish_reason":"stop","usage":{"prompt_tokens":1083,"completion_tokens":36,"total_tokens":1119}}} diff --git a/quickstart/eino_assistant/data/redis/dump.rdb b/quickstart/eino_assistant/data/redis/dump.rdb new file mode 100644 index 0000000..5103d42 Binary files /dev/null and b/quickstart/eino_assistant/data/redis/dump.rdb differ diff --git a/quickstart/eino_assistant/data/task/tasks.jsonl b/quickstart/eino_assistant/data/task/tasks.jsonl new file mode 100644 index 0000000..9cb7fbe --- /dev/null +++ b/quickstart/eino_assistant/data/task/tasks.jsonl @@ -0,0 +1 @@ +{"id":"a13f1db5-0a00-4a9f-8151-52fdc097b11a","title":"阅读 Eino example","content":"把 Eino 的 example 通读一遍","completed":false,"deadline":"2025-1-17 19:00:00","is_deleted":false,"created_at":"2025-01-14T14:45:13+08:00"} diff --git a/quickstart/eino_assistant/docker-compose.yml b/quickstart/eino_assistant/docker-compose.yml new file mode 100644 index 0000000..c67b099 --- /dev/null +++ b/quickstart/eino_assistant/docker-compose.yml @@ -0,0 +1,17 @@ +services: + redis-stack: + image: redis/redis-stack:latest + container_name: redis-stack + ports: + - "6379:6379" # Redis port + - "8001:8001" # RedisInsight port + volumes: + - ./data/redis:/data + environment: + - REDIS_ARGS=--dir /data --appendonly no --save 1800 1 + restart: unless-stopped + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 10s + timeout: 5s + retries: 3 diff --git a/quickstart/eino_assistant/eino/eino_agent.json b/quickstart/eino_assistant/eino/eino_agent.json new file mode 100644 index 0000000..2bf50a2 --- /dev/null +++ b/quickstart/eino_assistant/eino/eino_agent.json @@ -0,0 +1,1573 @@ +{ + "name": "EinoAgent", + "node_trigger_mode": "AllPredecessor", + "input_type": { + "title": "*UserMessage" + }, + "output_type": { + "title": "*schema.Message", + "description": "github.com/cloudwego/eino/schema" + }, + "gen_local_state": { + "output_type": {} + }, + "id": "f_oI8e", + "component": "Graph", + "nodes": [ + { + "id": "start", + "key": "start", + "name": "Start", + "type": "start", + "layoutData": { + "position": { + "x": 80, + "y": 682 + } + } + }, + { + "id": "end", + "key": "end", + "name": "End", + "type": "end", + "layoutData": { + "position": { + "x": 1686.38, + "y": 355.35 + } + } + }, + { + "id": "8AgKo6", + "key": "InputToQuery", + "name": "UserMessageToQuery", + "type": "Lambda", + "component_schema": { + "name": "Lambda", + "component": "Lambda", + "component_source": "custom", + "input_type": { + "title": "*UserMessage", + "description": "" + }, + "output_type": { + "title": "string", + "description": "" + }, + "extra_property": { + "schema": { + "type": "object", + "description": "", + "properties": { + "has_option": { + "type": "boolean", + "description": "" + }, + "interaction_type": { + "type": "string", + "description": "", + "enum": [ + "invoke", + "stream", + "collect", + "transform" + ] + }, + "option_package_path": { + "type": "string", + "description": "" + }, + "option_type_name": { + "type": "string", + "description": "" + } + }, + "required": [ + "interaction_type", + "has_option" + ] + }, + "extra_property_input": "{\"has_option\":true,\"interaction_type\":\"invoke\"}" + }, + "is_io_type_mutable": true, + "version": "1.0.0", + "method": "NewInputToQuery" + }, + "layoutData": { + "position": { + "x": 375, + "y": 735 + } + }, + "node_option": {} + }, + { + "id": "A7z9_b", + "key": "ChatTemplate", + "name": "", + "type": "ChatTemplate", + "component_schema": { + "name": "chatTemplate", + "component": "ChatTemplate", + "component_source": "official", + "identifier": "github.com/cloudwego/eino/components/prompt", + "input_type": { + "title": "map[string]any", + "description": "" + }, + "output_type": { + "title": "[]*schema.Message", + "description": "" + }, + "config": { + "description": "github.com/cloudwego/eino/blob/main/components/prompt/chat_template.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "FormatType": { + "type": "number", + "description": "", + "enum": [ + "0", + "1", + "2" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "schema.FormatType", + "kind": "uint8", + "isPtr": false + } + } + }, + "propertyOrder": [ + "FormatType" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "Config", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"FormatType\":1}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewChatTemplate" + }, + "layoutData": { + "position": { + "x": 1035, + "y": 498 + } + }, + "node_option": {} + }, + { + "id": "CDNTqO", + "key": "ReactAgent", + "name": "ReAct Agent", + "type": "Lambda", + "component_schema": { + "name": "react", + "component": "Lambda", + "component_source": "official", + "identifier": "github.com/cloudwego/eino/flow/agent/react", + "input_type": { + "title": "[]*schema.Message", + "description": "" + }, + "output_type": { + "title": "*schema.Message", + "description": "" + }, + "slots": [ + { + "component": "ChatModel", + "field_loc_path": "Model", + "multiple": false, + "required": false, + "component_items": [ + { + "name": "ark", + "component": "ChatModel", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/model/ark", + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/model/ark/chatmodel.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "APIKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "AccessKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "BaseURL": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "FrequencyPenalty": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "float32", + "kind": "float32", + "isPtr": true + } + }, + "LogProbs": { + "type": "boolean", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "bool", + "kind": "bool", + "isPtr": true + } + }, + "LogitBias": { + "type": "object", + "description": "", + "additionalProperties": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "map[string]int", + "kind": "map", + "isPtr": false + } + }, + "MaxTokens": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "Model": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "N": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "PresencePenalty": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "float32", + "kind": "float32", + "isPtr": true + } + }, + "Region": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "RepetitionPenalty": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "float32", + "kind": "float32", + "isPtr": true + } + }, + "RetryTimes": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "SecretKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Stop": { + "type": "array", + "description": "", + "items": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "[]string", + "kind": "slice", + "isPtr": false + } + }, + "Stream": { + "type": "boolean", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "bool", + "kind": "bool", + "isPtr": true + } + }, + "Temperature": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "float32", + "kind": "float32", + "isPtr": true + } + }, + "Timeout": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "time", + "pkgPath": "time" + }, + "typeName": "time.Duration", + "kind": "int64", + "isPtr": true + } + }, + "TopLogProbs": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "TopP": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "float32", + "kind": "float32", + "isPtr": true + } + }, + "User": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": true + } + } + }, + "propertyOrder": [ + "BaseURL", + "Region", + "Timeout", + "RetryTimes", + "APIKey", + "AccessKey", + "SecretKey", + "Model", + "MaxTokens", + "Temperature", + "TopP", + "Stream", + "Stop", + "FrequencyPenalty", + "LogitBias", + "LogProbs", + "TopLogProbs", + "User", + "PresencePenalty", + "RepetitionPenalty", + "N" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "ark.ChatModelConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"Stop\":[],\"LogitBias\":{\"einoAdditionalPropertyInput\":[]}}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewArkChatModel", + "input_type": {}, + "output_type": {}, + "id": "WMHlcP", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1684.65, + "y": 506.65 + } + } + } + ], + "go_definition": { + "libraryRef": { + "version": "v0.3.4", + "module": "github.com/cloudwego/eino", + "pkgPath": "github.com/cloudwego/eino/components/model" + }, + "typeName": "model.ChatModel", + "kind": "interface", + "isPtr": false + } + }, + { + "component": "Tool", + "field_loc_path": "ToolsConfig.Tools", + "multiple": true, + "required": true, + "component_items": [ + { + "name": "duckduckgo", + "component": "Tool", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/tool/duckduckgo/search.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "DDGConfig": { + "type": "object", + "description": "", + "properties": { + "Cache": { + "type": "boolean", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "bool", + "kind": "bool", + "isPtr": false + } + }, + "Headers": { + "type": "object", + "description": "", + "additionalProperties": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "map[string]string", + "kind": "map", + "isPtr": false + } + }, + "MaxRetries": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "Proxy": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Timeout": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "time", + "pkgPath": "time" + }, + "typeName": "time.Duration", + "kind": "int64", + "isPtr": false + } + } + }, + "propertyOrder": [ + "Headers", + "Proxy", + "Timeout", + "Cache", + "MaxRetries" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "ddgsearch.Config", + "kind": "struct", + "isPtr": true + } + }, + "MaxResults": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "Region": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "ddgsearch.Region", + "kind": "string", + "isPtr": false + } + }, + "SafeSearch": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "ddgsearch.SafeSearch", + "kind": "string", + "isPtr": false + } + }, + "TimeRange": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "github.com/cloudwego/eino-ext/components/tool/duckduckgo", + "pkgPath": "github.com/cloudwego/eino-ext/components/tool/duckduckgo/ddgsearch" + }, + "typeName": "ddgsearch.TimeRange", + "kind": "string", + "isPtr": false + } + }, + "ToolDesc": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "ToolName": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + } + }, + "propertyOrder": [ + "ToolName", + "ToolDesc", + "Region", + "MaxResults", + "SafeSearch", + "TimeRange", + "DDGConfig" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "duckduckgo.Config", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"DDGConfig\":{\"Headers\":{\"einoAdditionalPropertyInput\":[]}}}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "id": "Z7Yokw", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1691.55, + "y": 602.75 + } + }, + "method": "NewDuckDuckGoTool", + "input_type": {}, + "output_type": {} + }, + { + "name": "Tool", + "component": "Tool", + "component_source": "custom", + "extra_property": { + "schema": { + "type": "object", + "description": "", + "properties": { + "interaction_type": { + "type": "string", + "description": "", + "enum": [ + "invoke", + "stream" + ] + } + }, + "required": [ + "interaction_type" + ] + }, + "extra_property_input": "{\"interaction_type\":\"invoke\"}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewTaskTool", + "input_type": {}, + "output_type": {}, + "id": "pEMFBs", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1693.28, + "y": 709.2 + } + } + }, + { + "name": "Tool", + "component": "Tool", + "component_source": "custom", + "extra_property": { + "schema": { + "type": "object", + "description": "", + "properties": { + "interaction_type": { + "type": "string", + "description": "", + "enum": [ + "invoke", + "stream" + ] + } + }, + "required": [ + "interaction_type" + ] + }, + "extra_property_input": "{\"interaction_type\":\"invoke\"}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewEinoTool", + "input_type": {}, + "output_type": {}, + "id": "CMur3X", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1696.72, + "y": 869.65 + } + } + }, + { + "name": "Tool", + "component": "Tool", + "component_source": "custom", + "extra_property": { + "schema": { + "type": "object", + "description": "", + "properties": { + "interaction_type": { + "type": "string", + "description": "", + "enum": [ + "invoke", + "stream" + ] + } + }, + "required": [ + "interaction_type" + ] + }, + "extra_property_input": "{\"interaction_type\":\"invoke\"}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewOpenURITool", + "input_type": {}, + "output_type": {}, + "id": "fKVHkP", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1693.28, + "y": 1026.65 + } + } + }, + { + "name": "Tool", + "component": "Tool", + "component_source": "custom", + "extra_property": { + "schema": { + "type": "object", + "description": "", + "properties": { + "interaction_type": { + "type": "string", + "description": "", + "enum": [ + "invoke", + "stream" + ] + } + }, + "required": [ + "interaction_type" + ] + }, + "extra_property_input": "{\"interaction_type\":\"invoke\"}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewGitCloneTool", + "input_type": {}, + "output_type": {}, + "id": "cfymTR", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1695, + "y": 1194 + } + } + } + ], + "go_definition": { + "libraryRef": { + "version": "v0.3.4", + "module": "github.com/cloudwego/eino", + "pkgPath": "github.com/cloudwego/eino/components/tool" + }, + "typeName": "tool.BaseTool", + "kind": "interface", + "isPtr": false + } + } + ], + "config": { + "description": "github.com/cloudwego/eino/blob/main/flow/agent/react/react.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "MaxStep": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "ToolReturnDirectly": { + "type": "object", + "description": "", + "additionalProperties": { + "type": "object", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "struct{}", + "kind": "struct", + "isPtr": false + } + }, + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "map[string]struct{}", + "kind": "map", + "isPtr": false + } + } + }, + "propertyOrder": [ + "MaxStep", + "ToolReturnDirectly" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "react.AgentConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"ToolReturnDirectly\":{\"einoAdditionalPropertyInput\":[]},\"MaxStep\":25}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewReactAgent" + }, + "layoutData": { + "position": { + "x": 1365, + "y": 465 + } + }, + "node_option": {} + }, + { + "id": "iunICK", + "key": "RedisRetriever", + "name": "", + "type": "Retriever", + "component_schema": { + "name": "redis", + "component": "Retriever", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/retriever/redis", + "slots": [ + { + "component": "Embedding", + "field_loc_path": "Embedding", + "multiple": false, + "required": false, + "component_items": [ + { + "name": "ark", + "component": "Embedding", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/embedding/ark", + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/embedding/ark/embedding.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "APIKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "AccessKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "BaseURL": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Dimensions": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "Model": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Region": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "RetryTimes": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "SecretKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Timeout": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "time", + "pkgPath": "time" + }, + "typeName": "time.Duration", + "kind": "int64", + "isPtr": true + } + }, + "User": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": true + } + } + }, + "propertyOrder": [ + "BaseURL", + "Region", + "Timeout", + "RetryTimes", + "APIKey", + "AccessKey", + "SecretKey", + "Model", + "User", + "Dimensions" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "ark.EmbeddingConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewArkEmbedding", + "input_type": {}, + "output_type": {}, + "id": "6tbVVn", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1035, + "y": 841 + } + } + } + ], + "go_definition": { + "libraryRef": { + "version": "v0.3.6", + "module": "github.com/cloudwego/eino", + "pkgPath": "github.com/cloudwego/eino/components/embedding" + }, + "typeName": "embedding.Embedder", + "kind": "interface", + "isPtr": false + } + } + ], + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/retriever/redis/retriever.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "Dialect": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "DistanceThreshold": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "float64", + "kind": "float64", + "isPtr": true + } + }, + "Index": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "ReturnFields": { + "type": "array", + "description": "", + "items": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "[]string", + "kind": "slice", + "isPtr": false + } + }, + "TopK": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "VectorField": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + } + }, + "propertyOrder": [ + "Index", + "VectorField", + "DistanceThreshold", + "Dialect", + "ReturnFields", + "TopK" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "redis.RetrieverConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"ReturnFields\":[]}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewRedisRetriever", + "input_type": {}, + "output_type": {} + }, + "layoutData": { + "position": { + "x": 705, + "y": 801 + } + }, + "node_option": { + "output_key": "documents" + } + }, + { + "id": "lJ0-pN", + "key": "InputToHistory", + "name": "UserMessageToVariables", + "type": "Lambda", + "component_schema": { + "name": "Lambda", + "component": "Lambda", + "component_source": "custom", + "input_type": { + "title": "*UserMessage", + "description": "" + }, + "output_type": { + "title": "map[string]any", + "description": "" + }, + "extra_property": { + "schema": { + "type": "object", + "description": "", + "properties": { + "has_option": { + "type": "boolean", + "description": "" + }, + "interaction_type": { + "type": "string", + "description": "", + "enum": [ + "invoke", + "stream", + "collect", + "transform" + ] + }, + "option_package_path": { + "type": "string", + "description": "" + }, + "option_type_name": { + "type": "string", + "description": "" + } + }, + "required": [ + "interaction_type", + "has_option" + ] + }, + "extra_property_input": "{\"has_option\":true,\"interaction_type\":\"invoke\"}" + }, + "is_io_type_mutable": true, + "version": "1.0.0", + "method": "NewInputToHistory" + }, + "layoutData": { + "position": { + "x": 705, + "y": 437 + } + }, + "node_option": {} + } + ], + "edges": [ + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "start", + "targetWorkflowNodeId": "8AgKo6", + "source_node_key": "start", + "target_node_key": "InputToQuery" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "start", + "targetWorkflowNodeId": "lJ0-pN", + "source_node_key": "start", + "target_node_key": "InputToHistory" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "CDNTqO", + "targetWorkflowNodeId": "end", + "source_node_key": "ReactAgent", + "target_node_key": "end" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "8AgKo6", + "targetWorkflowNodeId": "iunICK", + "source_node_key": "InputToQuery", + "target_node_key": "RedisRetriever" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "iunICK", + "targetWorkflowNodeId": "A7z9_b", + "source_node_key": "RedisRetriever", + "target_node_key": "ChatTemplate" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "lJ0-pN", + "targetWorkflowNodeId": "A7z9_b", + "source_node_key": "InputToHistory", + "target_node_key": "ChatTemplate" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "A7z9_b", + "targetWorkflowNodeId": "CDNTqO", + "source_node_key": "ChatTemplate", + "target_node_key": "ReactAgent" + } + ], + "branches": [] +} \ No newline at end of file diff --git a/quickstart/eino_assistant/eino/einoagent/embedding.go b/quickstart/eino_assistant/eino/einoagent/embedding.go new file mode 100644 index 0000000..d197cd5 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/embedding.go @@ -0,0 +1,47 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + "os" + + "github.com/cloudwego/eino-ext/components/embedding/ark" + "github.com/cloudwego/eino/components/embedding" +) + +func defaultArkEmbeddingConfig(ctx context.Context) (*ark.EmbeddingConfig, error) { + config := &ark.EmbeddingConfig{ + Model: os.Getenv("ARK_EMBEDDING_MODEL"), + APIKey: os.Getenv("ARK_API_KEY"), + } + return config, nil +} + +func NewArkEmbedding(ctx context.Context, config *ark.EmbeddingConfig) (eb embedding.Embedder, err error) { + if config == nil { + config, err = defaultArkEmbeddingConfig(ctx) + if err != nil { + return nil, err + } + } + eb, err = ark.NewEmbedder(ctx, config) + if err != nil { + return nil, err + } + return eb, nil +} diff --git a/quickstart/eino_assistant/eino/einoagent/flow.go b/quickstart/eino_assistant/eino/einoagent/flow.go new file mode 100644 index 0000000..7151352 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/flow.go @@ -0,0 +1,65 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent/react" +) + +func defaultReactAgentConfig(ctx context.Context) (*react.AgentConfig, error) { + config := &react.AgentConfig{ + MaxStep: 25, + ToolReturnDirectly: map[string]struct{}{}} + chatModelCfg11, err := defaultArkChatModelConfig(ctx) + if err != nil { + return nil, err + } + chatModelIns11, err := NewArkChatModel(ctx, chatModelCfg11) + if err != nil { + return nil, err + } + config.Model = chatModelIns11 + + tools, err := GetTools(ctx) + if err != nil { + return nil, err + } + + config.ToolsConfig.Tools = tools + return config, nil +} + +func NewReactAgent(ctx context.Context, config *react.AgentConfig) (lba *compose.Lambda, err error) { + if config == nil { + config, err = defaultReactAgentConfig(ctx) + if err != nil { + return nil, err + } + } + ins, err := react.NewAgent(ctx, config) + if err != nil { + return nil, err + } + lba, err = compose.AnyLambda(ins.Generate, ins.Stream, nil, nil) + if err != nil { + return nil, err + } + return lba, nil +} diff --git a/quickstart/eino_assistant/eino/einoagent/lambda_func.go b/quickstart/eino_assistant/eino/einoagent/lambda_func.go new file mode 100644 index 0000000..bb930e1 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/lambda_func.go @@ -0,0 +1,34 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + "time" +) + +func NewInputToQuery(ctx context.Context, input *UserMessage, opts ...any) (output string, err error) { + return input.Query, nil +} + +func NewInputToHistory(ctx context.Context, input *UserMessage, opts ...any) (output map[string]any, err error) { + return map[string]any{ + "content": input.Query, + "history": input.History, + "date": time.Now().Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/quickstart/eino_assistant/eino/einoagent/model.go b/quickstart/eino_assistant/eino/einoagent/model.go new file mode 100644 index 0000000..f2ab0f9 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/model.go @@ -0,0 +1,47 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + "os" + + "github.com/cloudwego/eino-ext/components/model/ark" + "github.com/cloudwego/eino/components/model" +) + +func defaultArkChatModelConfig(ctx context.Context) (*ark.ChatModelConfig, error) { + config := &ark.ChatModelConfig{ + Model: os.Getenv("ARK_CHAT_MODEL"), + APIKey: os.Getenv("ARK_API_KEY"), + } + return config, nil +} + +func NewArkChatModel(ctx context.Context, config *ark.ChatModelConfig) (cm model.ChatModel, err error) { + if config == nil { + config, err = defaultArkChatModelConfig(ctx) + if err != nil { + return nil, err + } + } + cm, err = ark.NewChatModel(ctx, config) + if err != nil { + return nil, err + } + return cm, nil +} diff --git a/quickstart/eino_assistant/eino/einoagent/orchestration.go b/quickstart/eino_assistant/eino/einoagent/orchestration.go new file mode 100644 index 0000000..9c5cd41 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/orchestration.go @@ -0,0 +1,78 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + + "github.com/cloudwego/eino-ext/components/retriever/redis" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent/react" + "github.com/cloudwego/eino/schema" +) + +type EinoAgentBuildConfig struct { + ChatTemplateKeyOfChatTemplate *ChatTemplateConfig + ReactAgentKeyOfLambda *react.AgentConfig + RedisRetrieverKeyOfRetriever *redis.RetrieverConfig +} + +type BuildConfig struct { + EinoAgent *EinoAgentBuildConfig +} + +func BuildEinoAgent(ctx context.Context, config *BuildConfig) (r compose.Runnable[*UserMessage, *schema.Message], err error) { + const ( + InputToQuery = "InputToQuery" + ChatTemplate = "ChatTemplate" + ReactAgent = "ReactAgent" + RedisRetriever = "RedisRetriever" + InputToHistory = "InputToHistory" + ) + g := compose.NewGraph[*UserMessage, *schema.Message]() + _ = g.AddLambdaNode(InputToQuery, compose.InvokableLambdaWithOption(NewInputToQuery), + compose.WithNodeName("UserMessageToQuery")) + chatTemplateKeyOfChatTemplate, err := NewChatTemplate(ctx, config.EinoAgent.ChatTemplateKeyOfChatTemplate) + if err != nil { + return nil, err + } + _ = g.AddChatTemplateNode(ChatTemplate, chatTemplateKeyOfChatTemplate) + reactAgentKeyOfLambda, err := NewReactAgent(ctx, config.EinoAgent.ReactAgentKeyOfLambda) + if err != nil { + return nil, err + } + _ = g.AddLambdaNode(ReactAgent, reactAgentKeyOfLambda, compose.WithNodeName("ReAct Agent")) + redisRetrieverKeyOfRetriever, err := NewRedisRetriever(ctx, config.EinoAgent.RedisRetrieverKeyOfRetriever) + if err != nil { + return nil, err + } + _ = g.AddRetrieverNode(RedisRetriever, redisRetrieverKeyOfRetriever, compose.WithOutputKey("documents")) + _ = g.AddLambdaNode(InputToHistory, compose.InvokableLambdaWithOption(NewInputToHistory), + compose.WithNodeName("UserMessageToVariables")) + _ = g.AddEdge(compose.START, InputToQuery) + _ = g.AddEdge(compose.START, InputToHistory) + _ = g.AddEdge(ReactAgent, compose.END) + _ = g.AddEdge(InputToQuery, RedisRetriever) + _ = g.AddEdge(RedisRetriever, ChatTemplate) + _ = g.AddEdge(InputToHistory, ChatTemplate) + _ = g.AddEdge(ChatTemplate, ReactAgent) + r, err = g.Compile(ctx, compose.WithGraphName("EinoAgent"), compose.WithNodeTriggerMode(compose.AllPredecessor)) + if err != nil { + return nil, err + } + return r, err +} diff --git a/quickstart/eino_assistant/eino/einoagent/prompt.go b/quickstart/eino_assistant/eino/einoagent/prompt.go new file mode 100644 index 0000000..2c6b017 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/prompt.go @@ -0,0 +1,85 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/schema" +) + +type ChatTemplateConfig struct { + FormatType schema.FormatType + Templates []schema.MessagesTemplate +} + +var systemPrompt = ` +# Role: Eino Expert Assistant + +## Core Competencies +- knowledge of Eino framework and ecosystem +- Project scaffolding and best practices consultation +- Documentation navigation and implementation guidance +- Search web, clone github repo, open file/url, task management + +## Interaction Guidelines +- Before responding, ensure you: + • Fully understand the user's request and requirements, if there are any ambiguities, clarify with the user + • Consider the most appropriate solution approach + +- When providing assistance: + • Be clear and concise + • Include practical examples when relevant + • Reference documentation when helpful + • Suggest improvements or next steps if applicable + +- If a request exceeds your capabilities: + • Clearly communicate your limitations, suggest alternative approaches if possible + +- If the question is compound or complex, you need to think step by step, avoiding giving low-quality answers directly. + +## Context Information +- Current Date: {date} +- Related Documents: |- +==== doc start ==== + {documents} +==== doc end ==== +` + +func defaultPromptTemplateConfig(ctx context.Context) (*ChatTemplateConfig, error) { + config := &ChatTemplateConfig{ + FormatType: schema.FString, + Templates: []schema.MessagesTemplate{ + schema.SystemMessage(systemPrompt), + schema.MessagesPlaceholder("history", true), + schema.UserMessage("{content}"), + }, + } + return config, nil +} + +func NewChatTemplate(ctx context.Context, config *ChatTemplateConfig) (ct prompt.ChatTemplate, err error) { + if config == nil { + config, err = defaultPromptTemplateConfig(ctx) + if err != nil { + return nil, err + } + } + ct = prompt.FromMessages(config.FormatType, config.Templates...) + return ct, nil +} diff --git a/quickstart/eino_assistant/eino/einoagent/retriever.go b/quickstart/eino_assistant/eino/einoagent/retriever.go new file mode 100644 index 0000000..46a00c1 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/retriever.go @@ -0,0 +1,94 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + "fmt" + "os" + "strconv" + + "github.com/cloudwego/eino-ext/components/retriever/redis" + "github.com/cloudwego/eino/components/retriever" + "github.com/cloudwego/eino/schema" + redisCli "github.com/redis/go-redis/v9" + + redispkg "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/redis" +) + +func defaultRedisRetrieverConfig(ctx context.Context) (*redis.RetrieverConfig, error) { + redisAddr := os.Getenv("REDIS_ADDR") + redisClient := redisCli.NewClient(&redisCli.Options{ + Addr: redisAddr, + Protocol: 2, + }) + + config := &redis.RetrieverConfig{ + Client: redisClient, + Index: fmt.Sprintf("%s%s", redispkg.RedisPrefix, redispkg.IndexName), + Dialect: 2, + ReturnFields: []string{redispkg.ContentField, redispkg.MetadataField, redispkg.DistanceField}, + TopK: 8, + VectorField: redispkg.VectorField, + DocumentConverter: func(ctx context.Context, doc redisCli.Document) (*schema.Document, error) { + resp := &schema.Document{ + ID: doc.ID, + Content: "", + MetaData: map[string]any{}, + } + for field, val := range doc.Fields { + if field == redispkg.ContentField { + resp.Content = val + } else if field == redispkg.MetadataField { + resp.MetaData[field] = val + } else if field == redispkg.DistanceField { + distance, err := strconv.ParseFloat(val, 64) + if err != nil { + continue + } + resp.WithScore(1 - distance) + } + } + + return resp, nil + }, + } + embeddingCfg11, err := defaultArkEmbeddingConfig(ctx) + if err != nil { + return nil, err + } + embeddingIns11, err := NewArkEmbedding(ctx, embeddingCfg11) + if err != nil { + return nil, err + } + config.Embedding = embeddingIns11 + return config, nil +} + +func NewRedisRetriever(ctx context.Context, config *redis.RetrieverConfig) (rtr retriever.Retriever, err error) { + if config == nil { + config, err = defaultRedisRetrieverConfig(ctx) + if err != nil { + return nil, err + } + } + rtr, err = redis.NewRetriever(ctx, config) + if err != nil { + return nil, err + } + return rtr, nil +} diff --git a/quickstart/eino_assistant/eino/einoagent/tool.go b/quickstart/eino_assistant/eino/einoagent/tool.go new file mode 100644 index 0000000..3d04c95 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/tool.go @@ -0,0 +1,98 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import ( + "context" + + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/tool/einotool" + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/tool/gitclone" + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/tool/open" + "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/tool/task" + "github.com/cloudwego/eino-ext/components/tool/duckduckgo" + "github.com/cloudwego/eino/components/tool" +) + +func GetTools(ctx context.Context) ([]tool.BaseTool, error) { + einoAssistantTool, err := NewEinoAssistantTool(ctx) + if err != nil { + return nil, err + } + + toolTask, err := NewTaskTool(ctx) + if err != nil { + return nil, err + } + + toolOpen, err := NewOpenFileTool(ctx) + if err != nil { + return nil, err + } + + toolGitClone, err := NewGitCloneFile(ctx) + if err != nil { + return nil, err + } + + toolDDGSearch, err := NewDDGSearch(ctx, nil) + if err != nil { + return nil, err + } + + return []tool.BaseTool{ + einoAssistantTool, + toolTask, + toolOpen, + toolGitClone, + toolDDGSearch, + }, nil +} + +func defaultDDGSearchConfig(ctx context.Context) (*duckduckgo.Config, error) { + config := &duckduckgo.Config{} + return config, nil +} + +func NewDDGSearch(ctx context.Context, config *duckduckgo.Config) (tn tool.BaseTool, err error) { + if config == nil { + config, err = defaultDDGSearchConfig(ctx) + if err != nil { + return nil, err + } + } + tn, err = duckduckgo.NewTool(ctx, config) + if err != nil { + return nil, err + } + return tn, nil +} + +func NewOpenFileTool(ctx context.Context) (tn tool.BaseTool, err error) { + return open.NewOpenFileTool(ctx, nil) +} + +func NewGitCloneFile(ctx context.Context) (tn tool.BaseTool, err error) { + return gitclone.NewGitCloneFile(ctx, nil) +} + +func NewEinoAssistantTool(ctx context.Context) (tn tool.BaseTool, err error) { + return einotool.NewEinoAssistantTool(ctx, nil) +} + +func NewTaskTool(ctx context.Context) (tn tool.BaseTool, err error) { + return task.NewTaskTool(ctx, nil) +} diff --git a/quickstart/eino_assistant/eino/einoagent/types.go b/quickstart/eino_assistant/eino/einoagent/types.go new file mode 100644 index 0000000..484e695 --- /dev/null +++ b/quickstart/eino_assistant/eino/einoagent/types.go @@ -0,0 +1,25 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einoagent + +import "github.com/cloudwego/eino/schema" + +type UserMessage struct { + ID string `json:"id"` + Query string `json:"query"` + History []*schema.Message `json:"history"` +} diff --git a/quickstart/eino_assistant/eino/knowledge_indexing.json b/quickstart/eino_assistant/eino/knowledge_indexing.json new file mode 100644 index 0000000..91f2217 --- /dev/null +++ b/quickstart/eino_assistant/eino/knowledge_indexing.json @@ -0,0 +1,547 @@ +{ + "name": "KnowledgeIndexing", + "node_trigger_mode": "AnyPredecessor", + "input_type": { + "title": "document.Source", + "description": "github.com/cloudwego/eino/components/document" + }, + "output_type": { + "title": "[]string" + }, + "gen_local_state": { + "output_type": {} + }, + "id": "BZe_z8", + "component": "Graph", + "nodes": [ + { + "id": "start", + "key": "start", + "name": "Start", + "type": "start", + "layoutData": { + "position": { + "x": 80, + "y": 86 + } + } + }, + { + "id": "end", + "key": "end", + "name": "End", + "type": "end", + "layoutData": { + "position": { + "x": 1365, + "y": 0 + } + } + }, + { + "id": "NE2Tk4", + "key": "FileLoader", + "name": "", + "type": "Loader", + "component_schema": { + "name": "file", + "component": "Loader", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/document/loader/file", + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/document/loader/file/file_loader.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "UseNameAsID": { + "type": "boolean", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "bool", + "kind": "bool", + "isPtr": false + } + } + }, + "propertyOrder": [ + "UseNameAsID" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "file.FileLoaderConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{}" + }, + "is_io_type_mutable": false, + "input_type": {}, + "output_type": {}, + "method": "NewFileLoader" + }, + "layoutData": { + "position": { + "x": 375, + "y": 71 + } + }, + "node_option": {} + }, + { + "id": "Jih7Gv", + "key": "MarkdownSplitter", + "name": "", + "type": "DocumentTransformer", + "component_schema": { + "name": "markdown", + "component": "DocumentTransformer", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown", + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/document/transformer/splitter/markdown/header.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "Headers": { + "type": "object", + "description": "", + "additionalProperties": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "map[string]string", + "kind": "map", + "isPtr": false + } + }, + "TrimHeaders": { + "type": "boolean", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "bool", + "kind": "bool", + "isPtr": false + } + } + }, + "propertyOrder": [ + "Headers", + "TrimHeaders" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "markdown.HeaderConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"Headers\":{},\"TrimHeaders\":true}" + }, + "is_io_type_mutable": false, + "method": "NewMarkdownSplitter", + "input_type": {}, + "output_type": {} + }, + "layoutData": { + "position": { + "x": 705, + "y": 71 + } + }, + "node_option": {} + }, + { + "id": "Ju7Igu", + "key": "RedisIndexer", + "name": "", + "type": "Indexer", + "component_schema": { + "name": "redis", + "component": "Indexer", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/indexer/redis", + "slots": [ + { + "component": "Embedding", + "field_loc_path": "Embedding", + "multiple": false, + "required": false, + "component_items": [ + { + "name": "ark", + "component": "Embedding", + "component_source": "official", + "identifier": "github.com/cloudwego/eino-ext/components/embedding/ark", + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/embedding/ark/embedding.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "APIKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "AccessKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "BaseURL": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Dimensions": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "HTTPClient": { + "type": "object", + "description": "", + "properties": { + "Timeout": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "time", + "pkgPath": "time" + }, + "typeName": "time.Duration", + "kind": "int64", + "isPtr": false + } + } + }, + "propertyOrder": [ + "Timeout" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "net/http", + "pkgPath": "net/http" + }, + "typeName": "http.Client", + "kind": "struct", + "isPtr": true + } + }, + "Model": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Region": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "RetryTimes": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": true + } + }, + "SecretKey": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + }, + "Timeout": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "time", + "pkgPath": "time" + }, + "typeName": "time.Duration", + "kind": "int64", + "isPtr": true + } + }, + "User": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": true + } + } + }, + "propertyOrder": [ + "BaseURL", + "Region", + "HTTPClient", + "Timeout", + "RetryTimes", + "APIKey", + "AccessKey", + "SecretKey", + "Model", + "User", + "Dimensions" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "ark.EmbeddingConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"HTTPClient\":{\"Timeout\":0},\"BaseURL\":\"https://ark.cn-beijing.volces.com/api/v3\"}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewArkEmbedding", + "input_type": {}, + "output_type": {}, + "id": "MbIZvg", + "layoutData": { + "isSlotNode": true, + "position": { + "x": 1365, + "y": 172 + } + } + } + ], + "go_definition": { + "libraryRef": { + "version": "v0.3.6", + "module": "github.com/cloudwego/eino", + "pkgPath": "github.com/cloudwego/eino/components/embedding" + }, + "typeName": "embedding.Embedder", + "kind": "interface", + "isPtr": false + } + } + ], + "config": { + "description": "github.com/cloudwego/eino-ext/blob/main/components/indexer/redis/indexer.go", + "schema": { + "type": "object", + "description": "", + "properties": { + "BatchSize": { + "type": "number", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "int", + "kind": "int", + "isPtr": false + } + }, + "KeyPrefix": { + "type": "string", + "description": "", + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "string", + "kind": "string", + "isPtr": false + } + } + }, + "propertyOrder": [ + "KeyPrefix", + "BatchSize" + ], + "goDefinition": { + "libraryRef": { + "version": "", + "module": "", + "pkgPath": "" + }, + "typeName": "redis.IndexerConfig", + "kind": "struct", + "isPtr": false + } + }, + "config_input": "{\"BatchSize\":1,\"KeyPrefix\":\"eino_assistant\"}" + }, + "is_io_type_mutable": false, + "version": "1.0.0", + "method": "NewRedisIndexer", + "input_type": {}, + "output_type": {} + }, + "layoutData": { + "position": { + "x": 1035, + "y": 56 + } + }, + "node_option": {} + } + ], + "edges": [ + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "start", + "targetWorkflowNodeId": "NE2Tk4", + "source_node_key": "start", + "target_node_key": "FileLoader" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "Ju7Igu", + "targetWorkflowNodeId": "end", + "source_node_key": "RedisIndexer", + "target_node_key": "end" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "NE2Tk4", + "targetWorkflowNodeId": "Jih7Gv", + "source_node_key": "FileLoader", + "target_node_key": "MarkdownSplitter" + }, + { + "id": "", + "name": "", + "sourceWorkflowNodeId": "Jih7Gv", + "targetWorkflowNodeId": "Ju7Igu", + "source_node_key": "MarkdownSplitter", + "target_node_key": "RedisIndexer" + } + ], + "branches": [] +} \ No newline at end of file diff --git a/quickstart/eino_assistant/eino/knowledgeindexing/embedding.go b/quickstart/eino_assistant/eino/knowledgeindexing/embedding.go new file mode 100644 index 0000000..8cec46c --- /dev/null +++ b/quickstart/eino_assistant/eino/knowledgeindexing/embedding.go @@ -0,0 +1,51 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package knowledgeindexing + +import ( + "context" + "log" + "os" + + "github.com/cloudwego/eino-ext/components/embedding/ark" + "github.com/cloudwego/eino/components/embedding" +) + +func defaultArkEmbeddingConfig(ctx context.Context) (*ark.EmbeddingConfig, error) { + config := &ark.EmbeddingConfig{ + BaseURL: "https://ark.cn-beijing.volces.com/api/v3", + APIKey: os.Getenv("ARK_API_KEY"), + Model: os.Getenv("ARK_EMBEDDING_MODEL"), + } + + log.Printf("apiKey: %v, model: %v", config.APIKey, config.Model) + return config, nil +} + +func NewArkEmbedding(ctx context.Context, config *ark.EmbeddingConfig) (eb embedding.Embedder, err error) { + if config == nil { + config, err = defaultArkEmbeddingConfig(ctx) + if err != nil { + return nil, err + } + } + eb, err = ark.NewEmbedder(ctx, config) + if err != nil { + return nil, err + } + return eb, nil +} diff --git a/quickstart/eino_assistant/eino/knowledgeindexing/indexer.go b/quickstart/eino_assistant/eino/knowledgeindexing/indexer.go new file mode 100644 index 0000000..f4f0aa4 --- /dev/null +++ b/quickstart/eino_assistant/eino/knowledgeindexing/indexer.go @@ -0,0 +1,98 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package knowledgeindexing + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino-ext/components/indexer/redis" + "github.com/cloudwego/eino/components/indexer" + "github.com/cloudwego/eino/schema" + "github.com/google/uuid" + redisCli "github.com/redis/go-redis/v9" + + redispkg "github.com/cloudwego/eino-examples/quickstart/eino_assistant/pkg/redis" +) + +func init() { + err := redispkg.Init() + if err != nil { + log.Fatalf("failed to init redis index: %v", err) + } +} + +func defaultRedisIndexerConfig(ctx context.Context) (*redis.IndexerConfig, error) { + redisAddr := os.Getenv("REDIS_ADDR") + redisClient := redisCli.NewClient(&redisCli.Options{ + Addr: redisAddr, + Protocol: 2, + }) + + config := &redis.IndexerConfig{ + Client: redisClient, + KeyPrefix: redispkg.RedisPrefix, + BatchSize: 1, + DocumentToHashes: func(ctx context.Context, doc *schema.Document) (*redis.Hashes, error) { + if doc.ID == "" { + doc.ID = uuid.New().String() + } + key := doc.ID + + metadataBytes, err := json.Marshal(doc.MetaData) + if err != nil { + return nil, fmt.Errorf("failed to marshal metadata: %w", err) + } + + return &redis.Hashes{ + Key: key, + Field2Value: map[string]redis.FieldValue{ + redispkg.ContentField: {Value: doc.Content, EmbedKey: redispkg.VectorField}, + redispkg.MetadataField: {Value: metadataBytes}, + }, + }, nil + }, + } + + embeddingCfg11, err := defaultArkEmbeddingConfig(ctx) + if err != nil { + return nil, err + } + embeddingIns11, err := NewArkEmbedding(ctx, embeddingCfg11) + if err != nil { + return nil, err + } + config.Embedding = embeddingIns11 + return config, nil +} + +func NewRedisIndexer(ctx context.Context, config *redis.IndexerConfig) (idr indexer.Indexer, err error) { + if config == nil { + config, err = defaultRedisIndexerConfig(ctx) + if err != nil { + return nil, err + } + } + idr, err = redis.NewIndexer(ctx, config) + if err != nil { + return nil, err + } + return idr, nil +} diff --git a/quickstart/eino_assistant/eino/knowledgeindexing/loader.go b/quickstart/eino_assistant/eino/knowledgeindexing/loader.go new file mode 100644 index 0000000..e0510c3 --- /dev/null +++ b/quickstart/eino_assistant/eino/knowledgeindexing/loader.go @@ -0,0 +1,43 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package knowledgeindexing + +import ( + "context" + + "github.com/cloudwego/eino-ext/components/document/loader/file" + "github.com/cloudwego/eino/components/document" +) + +func defaultFileLoaderConfig(ctx context.Context) (*file.FileLoaderConfig, error) { + config := &file.FileLoaderConfig{} + return config, nil +} + +func NewFileLoader(ctx context.Context, config *file.FileLoaderConfig) (ldr document.Loader, err error) { + if config == nil { + config, err = defaultFileLoaderConfig(ctx) + if err != nil { + return nil, err + } + } + ldr, err = file.NewFileLoader(ctx, config) + if err != nil { + return nil, err + } + return ldr, nil +} diff --git a/quickstart/eino_assistant/eino/knowledgeindexing/orchestration.go b/quickstart/eino_assistant/eino/knowledgeindexing/orchestration.go new file mode 100644 index 0000000..206355a --- /dev/null +++ b/quickstart/eino_assistant/eino/knowledgeindexing/orchestration.go @@ -0,0 +1,70 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package knowledgeindexing + +import ( + "context" + + "github.com/cloudwego/eino-ext/components/document/loader/file" + "github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown" + "github.com/cloudwego/eino-ext/components/indexer/redis" + "github.com/cloudwego/eino/components/document" + "github.com/cloudwego/eino/compose" +) + +type KnowledgeIndexingBuildConfig struct { + FileLoaderKeyOfLoader *file.FileLoaderConfig + MarkdownSplitterKeyOfDocumentTransformer *markdown.HeaderConfig + RedisIndexerKeyOfIndexer *redis.IndexerConfig +} + +type BuildConfig struct { + KnowledgeIndexing *KnowledgeIndexingBuildConfig +} + +func BuildKnowledgeIndexing(ctx context.Context, config *BuildConfig) (r compose.Runnable[document.Source, []string], err error) { + const ( + FileLoader = "FileLoader" + MarkdownSplitter = "MarkdownSplitter" + RedisIndexer = "RedisIndexer" + ) + g := compose.NewGraph[document.Source, []string]() + fileLoaderKeyOfLoader, err := NewFileLoader(ctx, config.KnowledgeIndexing.FileLoaderKeyOfLoader) + if err != nil { + return nil, err + } + _ = g.AddLoaderNode(FileLoader, fileLoaderKeyOfLoader) + markdownSplitterKeyOfDocumentTransformer, err := NewMarkdownSplitter(ctx, config.KnowledgeIndexing.MarkdownSplitterKeyOfDocumentTransformer) + if err != nil { + return nil, err + } + _ = g.AddDocumentTransformerNode(MarkdownSplitter, markdownSplitterKeyOfDocumentTransformer) + redisIndexerKeyOfIndexer, err := NewRedisIndexer(ctx, config.KnowledgeIndexing.RedisIndexerKeyOfIndexer) + if err != nil { + return nil, err + } + _ = g.AddIndexerNode(RedisIndexer, redisIndexerKeyOfIndexer) + _ = g.AddEdge(compose.START, FileLoader) + _ = g.AddEdge(RedisIndexer, compose.END) + _ = g.AddEdge(FileLoader, MarkdownSplitter) + _ = g.AddEdge(MarkdownSplitter, RedisIndexer) + r, err = g.Compile(ctx, compose.WithGraphName("KnowledgeIndexing"), compose.WithNodeTriggerMode(compose.AnyPredecessor)) + if err != nil { + return nil, err + } + return r, err +} diff --git a/quickstart/eino_assistant/eino/knowledgeindexing/transformer.go b/quickstart/eino_assistant/eino/knowledgeindexing/transformer.go new file mode 100644 index 0000000..62c40c8 --- /dev/null +++ b/quickstart/eino_assistant/eino/knowledgeindexing/transformer.go @@ -0,0 +1,44 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package knowledgeindexing + +import ( + "context" + + "github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown" + "github.com/cloudwego/eino/components/document" +) + +func defaultMarkdownSplitterConfig(ctx context.Context) (*markdown.HeaderConfig, error) { + config := &markdown.HeaderConfig{ + TrimHeaders: true} + return config, nil +} + +func NewMarkdownSplitter(ctx context.Context, config *markdown.HeaderConfig) (tfr document.Transformer, err error) { + if config == nil { + config, err = defaultMarkdownSplitterConfig(ctx) + if err != nil { + return nil, err + } + } + tfr, err = markdown.NewHeaderSplitter(ctx, config) + if err != nil { + return nil, err + } + return tfr, nil +} diff --git a/quickstart/eino_assistant/go.mod b/quickstart/eino_assistant/go.mod new file mode 100644 index 0000000..c5fd8dc --- /dev/null +++ b/quickstart/eino_assistant/go.mod @@ -0,0 +1,70 @@ +module github.com/cloudwego/eino-examples/quickstart/eino_assistant + +go 1.21 + +require ( + github.com/cloudwego/eino v0.3.7 + github.com/cloudwego/eino-ext/callbacks/langfuse v0.0.0-20250117061805-cd80d1780d76 + github.com/cloudwego/eino-ext/components/model/ark v0.0.0-20250117061805-cd80d1780d76 + github.com/cloudwego/eino-ext/components/retriever/redis v0.0.0-20250117061805-cd80d1780d76 + github.com/cloudwego/eino-ext/components/tool/duckduckgo v0.0.0-20250117061805-cd80d1780d76 + github.com/cloudwego/eino-ext/devops v0.0.0-20250117061805-cd80d1780d76 + github.com/cloudwego/hertz v0.9.5 + github.com/google/uuid v1.6.0 + github.com/hertz-contrib/sse v0.0.6-0.20240617114443-10a844794bf3 + github.com/redis/go-redis/v9 v9.7.0 +) + +require ( + github.com/bytedance/gopkg v0.1.0 // indirect + github.com/bytedance/sonic v1.12.7 // indirect + github.com/bytedance/sonic/loader v0.2.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/eino-ext/components/document/loader/file v0.0.0-20250116071241-3f1eaaafd49c + github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown v0.0.0-20250116071241-3f1eaaafd49c + github.com/cloudwego/eino-ext/components/embedding/ark v0.0.0-20250116071241-3f1eaaafd49c + github.com/cloudwego/eino-ext/components/indexer/redis v0.0.0-20250116071241-3f1eaaafd49c + github.com/cloudwego/eino-ext/libs/acl/langfuse v0.0.0-20250113033825-eb19b2b6b386 // indirect + github.com/cloudwego/netpoll v0.6.4 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/getkin/kin-openapi v0.118.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.19.5 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/invopop/yaml v0.3.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/nikolalohinski/gonja v1.5.3 // indirect + github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/volcengine/volc-sdk-golang v1.0.23 // indirect + github.com/volcengine/volcengine-go-sdk v1.0.160 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + golang.org/x/arch v0.12.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/sys v0.28.0 // indirect + google.golang.org/protobuf v1.36.3 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/quickstart/eino_assistant/go.sum b/quickstart/eino_assistant/go.sum new file mode 100644 index 0000000..d0717e4 --- /dev/null +++ b/quickstart/eino_assistant/go.sum @@ -0,0 +1,319 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.0 h1:aAxB7mm1qms4Wz4sp8e1AtKDOeFLtdqvGiUe7aonRJs= +github.com/bytedance/gopkg v0.1.0/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ= +github.com/bytedance/mockey v1.2.13 h1:jokWZAm/pUEbD939Rhznz615MKUCZNuvCFQlJ2+ntoo= +github.com/bytedance/mockey v1.2.13/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY= +github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= +github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o= +github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/eino v0.3.7 h1:PE1yFaAPVenRhDl0x6N1U2rKrfZkSr1hKlcacO6P+VA= +github.com/cloudwego/eino v0.3.7/go.mod h1:+kmJimGEcKuSI6OKhet7kBedkm1WUZS3H1QRazxgWUo= +github.com/cloudwego/eino-ext/callbacks/langfuse v0.0.0-20250117061805-cd80d1780d76 h1:ItCp3l6FEb2UAGp8S5n7+zIVF3HRnipn5AOjtCvawkU= +github.com/cloudwego/eino-ext/callbacks/langfuse v0.0.0-20250117061805-cd80d1780d76/go.mod h1:5StXiP9SugyHuqTZ1cAX5wOGnQq4hKGK+R81C74uHHM= +github.com/cloudwego/eino-ext/components/document/loader/file v0.0.0-20250116071241-3f1eaaafd49c h1:FCsu5ctlFx8Frxu/LcswWk3vB/26qbYIKEKKIQOKubQ= +github.com/cloudwego/eino-ext/components/document/loader/file v0.0.0-20250116071241-3f1eaaafd49c/go.mod h1:x4Xh/L1IrRaou18Y1Rbh87hfM/mv+8PysInEUvauZwA= +github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown v0.0.0-20250116071241-3f1eaaafd49c h1:l0epKk0YL24uPRFMWH9MU1Co9GM+OLRujdcJo5yJ2ps= +github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown v0.0.0-20250116071241-3f1eaaafd49c/go.mod h1:uawkX9EIkXKJHD2n7LNPOzbjXCFEU6jc1fdoIl4yYm8= +github.com/cloudwego/eino-ext/components/embedding/ark v0.0.0-20250116071241-3f1eaaafd49c h1:chiTGELzHuTDK/ZMWg9WznNJ+qlCuCEEcNvI4jjmLPs= +github.com/cloudwego/eino-ext/components/embedding/ark v0.0.0-20250116071241-3f1eaaafd49c/go.mod h1:RCwPJYYY9DnhuGyIWCjaicX1ajWf3XooS92En6fW18o= +github.com/cloudwego/eino-ext/components/indexer/redis v0.0.0-20250116071241-3f1eaaafd49c h1:ugOzWE2dvJnuXyuOPX7N3q05wT3oP3IHQASVfik8nrs= +github.com/cloudwego/eino-ext/components/indexer/redis v0.0.0-20250116071241-3f1eaaafd49c/go.mod h1:7z1agcgwS3CAO+ADgf0QCu0lOh5Owe+/DaI33hfcr+g= +github.com/cloudwego/eino-ext/components/model/ark v0.0.0-20250117061805-cd80d1780d76 h1:EttuZVs4Ysd2r0L+8Ki2H3cy+t3rIJ0ad2yTi3OI2PE= +github.com/cloudwego/eino-ext/components/model/ark v0.0.0-20250117061805-cd80d1780d76/go.mod h1:+xktpoBbBriT3bDBONMXDd4LNs6Z2tSI/fWzjFIMmgc= +github.com/cloudwego/eino-ext/components/retriever/redis v0.0.0-20250117061805-cd80d1780d76 h1:Y22yHaxUvl4NfN3ESDG/BcNrNIC4hL3A3DreNqpES0I= +github.com/cloudwego/eino-ext/components/retriever/redis v0.0.0-20250117061805-cd80d1780d76/go.mod h1:2WrVfYFjZHSmjA+8iSwXcS0CW3oaC2XM/XzFh/1bW4Q= +github.com/cloudwego/eino-ext/components/tool/duckduckgo v0.0.0-20250117061805-cd80d1780d76 h1:ueBCollhWzpdZ5KN1UPuytgko03y3UvikChKYCc7KYU= +github.com/cloudwego/eino-ext/components/tool/duckduckgo v0.0.0-20250117061805-cd80d1780d76/go.mod h1:Do8C+KMH+3PiF/jYV/8oFQz+UvCTrThswB9fZWiqfgI= +github.com/cloudwego/eino-ext/devops v0.0.0-20250117061805-cd80d1780d76 h1:mE2pxr1sVUz/ucRZCMflE2O5B1JfPEqO3V8BgJ3WQYU= +github.com/cloudwego/eino-ext/devops v0.0.0-20250117061805-cd80d1780d76/go.mod h1:MzPGghc4J7rSevtxeZpqAejpaFbkoCNzG6AQLG93WmE= +github.com/cloudwego/eino-ext/libs/acl/langfuse v0.0.0-20250113033825-eb19b2b6b386 h1:dF//5iW+PCS8ZnZ0PwmO2enn3Oek++mbgB6dmaJAz6o= +github.com/cloudwego/eino-ext/libs/acl/langfuse v0.0.0-20250113033825-eb19b2b6b386/go.mod h1:77jqGUJZjxg+V/sJ8S6dd0JtRLO782yVWHmhuFgb9ig= +github.com/cloudwego/hertz v0.9.5 h1:FXV2YFLrNHRdpwT+OoIvv0wEHUC0Bo68CDPujr6VnWo= +github.com/cloudwego/hertz v0.9.5/go.mod h1:UUBt8N8hSTStz7NEvLZ5mnALpBSofNL4DoYzIIp8UaY= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/netpoll v0.6.4 h1:z/dA4sOTUQof6zZIO4QNnLBXsDFFFEos9OOGloR6kno= +github.com/cloudwego/netpoll v0.6.4/go.mod h1:BtM+GjKTdwKoC8IOzD08/+8eEn2gYoiNLipFca6BVXQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM= +github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hertz-contrib/sse v0.0.6-0.20240617114443-10a844794bf3 h1:k4flETJPaiM2v4zsmYl/MrDnUeJfcZ1cgFB3wWrSrIk= +github.com/hertz-contrib/sse v0.0.6-0.20240617114443-10a844794bf3/go.mod h1:hCL17JP8wGf4l3zvbkSdwtYV+3Ikdu3VvpTdeOKM2uE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= +github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8= +github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= +github.com/volcengine/volcengine-go-sdk v1.0.160 h1:Jpa8MJ/V9Dh5zCbIKgF8AOKhfigVIJqYpSV4pW1tjy0= +github.com/volcengine/volcengine-go-sdk v1.0.160/go.mod h1:oht5AKDJsk0fY6tV2ViqaVlOO14KSRmXZlI8ikK60Tg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/quickstart/eino_assistant/pkg/mem/simple.go b/quickstart/eino_assistant/pkg/mem/simple.go new file mode 100644 index 0000000..2495bc7 --- /dev/null +++ b/quickstart/eino_assistant/pkg/mem/simple.go @@ -0,0 +1,208 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mem + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/cloudwego/eino/schema" +) + +func GetDefaultMemory() *SimpleMemory { + return NewSimpleMemory(SimpleMemoryConfig{ + Dir: "data/memory", + MaxWindowSize: 6, + }) +} + +type SimpleMemoryConfig struct { + Dir string + MaxWindowSize int +} + +func NewSimpleMemory(cfg SimpleMemoryConfig) *SimpleMemory { + if cfg.Dir == "" { + cfg.Dir = "/tmp/eino/memory" + } + if err := os.MkdirAll(cfg.Dir, 0755); err != nil { + return nil + } + + return &SimpleMemory{ + dir: cfg.Dir, + maxWindowSize: cfg.MaxWindowSize, + conversations: make(map[string]*Conversation), + } +} + +// simple memory can store messages of each conversation +type SimpleMemory struct { + mu sync.Mutex + dir string + maxWindowSize int + conversations map[string]*Conversation +} + +func (m *SimpleMemory) GetConversation(id string, createIfNotExist bool) *Conversation { + m.mu.Lock() + defer m.mu.Unlock() + + _, ok := m.conversations[id] + + filePath := filepath.Join(m.dir, id+".jsonl") + if !ok { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + if createIfNotExist { + if err := os.WriteFile(filePath, []byte(""), 0644); err != nil { + return nil + } + m.conversations[id] = &Conversation{ + ID: id, + Messages: make([]*schema.Message, 0), + filePath: filePath, + maxWindowSize: m.maxWindowSize, + } + } + } + + con := &Conversation{ + ID: id, + Messages: make([]*schema.Message, 0), + filePath: filePath, + maxWindowSize: m.maxWindowSize, + } + con.load() + m.conversations[id] = con + } + + return m.conversations[id] +} + +func (m *SimpleMemory) ListConversations() []string { + m.mu.Lock() + defer m.mu.Unlock() + + files, err := os.ReadDir(m.dir) + if err != nil { + return nil + } + + ids := make([]string, 0, len(files)) + for _, file := range files { + if file.IsDir() { + continue + } + ids = append(ids, strings.TrimSuffix(file.Name(), ".jsonl")) + } + + return ids +} + +func (m *SimpleMemory) DeleteConversation(id string) error { + m.mu.Lock() + defer m.mu.Unlock() + + filePath := filepath.Join(m.dir, id+".jsonl") + if err := os.Remove(filePath); err != nil { + return fmt.Errorf("failed to delete file: %w", err) + } + + delete(m.conversations, id) + return nil +} + +type Conversation struct { + mu sync.Mutex + + ID string `json:"id"` + Messages []*schema.Message `json:"messages"` + + filePath string + + maxWindowSize int +} + +func (c *Conversation) Append(msg *schema.Message) { + c.mu.Lock() + defer c.mu.Unlock() + + c.Messages = append(c.Messages, msg) + + c.save(msg) +} + +func (c *Conversation) GetFullMessages() []*schema.Message { + c.mu.Lock() + defer c.mu.Unlock() + + return c.Messages +} + +// get messages with max window size +func (c *Conversation) GetMessages() []*schema.Message { + c.mu.Lock() + defer c.mu.Unlock() + + if len(c.Messages) > c.maxWindowSize { + return c.Messages[len(c.Messages)-c.maxWindowSize:] + } + + return c.Messages +} + +func (c *Conversation) load() error { + reader, err := os.Open(c.filePath) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer reader.Close() + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + var msg schema.Message + if err := json.Unmarshal([]byte(line), &msg); err != nil { + return fmt.Errorf("failed to unmarshal message: %w", err) + } + c.Messages = append(c.Messages, &msg) + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("scanner error: %w", err) + } + + return nil +} + +func (c *Conversation) save(msg *schema.Message) { + str, _ := json.Marshal(msg) + + // Append to file + f, err := os.OpenFile(c.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + f.Write(str) + f.WriteString("\n") +} diff --git a/quickstart/eino_assistant/pkg/redis/redis.go b/quickstart/eino_assistant/pkg/redis/redis.go new file mode 100644 index 0000000..9371a0b --- /dev/null +++ b/quickstart/eino_assistant/pkg/redis/redis.go @@ -0,0 +1,114 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package redis + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/redis/go-redis/v9" +) + +const ( + RedisPrefix = "eino:doc:" + IndexName = "vector_index" + + ContentField = "content" + MetadataField = "metadata" + VectorField = "content_vector" + DistanceField = "distance" +) + +var initOnce sync.Once + +func Init() error { + var err error + initOnce.Do(func() { + err = InitRedisIndex(context.Background(), &Config{ + RedisAddr: "localhost:6379", + Dimension: 4096, + }) + }) + return err +} + +type Config struct { + RedisAddr string + Dimension int +} + +func InitRedisIndex(ctx context.Context, config *Config) (err error) { + if config.Dimension <= 0 { + return fmt.Errorf("dimension must be positive") + } + + client := redis.NewClient(&redis.Options{ + Addr: config.RedisAddr, + Protocol: 2, + }) + + defer func() { + if err != nil { + client.Close() + } + }() + + if err = client.Ping(ctx).Err(); err != nil { + return fmt.Errorf("failed to connect to Redis: %w", err) + } + + indexName := fmt.Sprintf("%s%s", RedisPrefix, IndexName) + + // 检查是否存在索引 + exists, err := client.Do(ctx, "FT.INFO", indexName).Result() + if err != nil { + if !strings.Contains(err.Error(), "Unknown index name") { + return fmt.Errorf("failed to check if index exists: %w", err) + } + err = nil + } else if exists != nil { + return nil + } + + // Create new index + createIndexArgs := []interface{}{ + "FT.CREATE", indexName, + "ON", "HASH", + "PREFIX", "1", RedisPrefix, + "SCHEMA", + ContentField, "TEXT", + MetadataField, "TEXT", + VectorField, "VECTOR", "FLAT", + "6", + "TYPE", "FLOAT32", + "DIM", config.Dimension, + "DISTANCE_METRIC", "COSINE", + } + + if err = client.Do(ctx, createIndexArgs...).Err(); err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + + // 验证索引是否创建成功 + if _, err = client.Do(ctx, "FT.INFO", indexName).Result(); err != nil { + return fmt.Errorf("failed to verify index creation: %w", err) + } + + return nil +} diff --git a/quickstart/eino_assistant/pkg/tool/einotool/einotool.go b/quickstart/eino_assistant/pkg/tool/einotool/einotool.go new file mode 100644 index 0000000..b283d4d --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/einotool/einotool.go @@ -0,0 +1,191 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package einotool + +import ( + "context" + "embed" + "os" + "path/filepath" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" +) + +//go:embed templates/* +var templateFS embed.FS + +const desc = `eino tool can get eino project info, +action: +- get_example_project: get the example project url, path of eino-examples +- get_github_repo: get the github repo url, e.g. eino, eino-ext, eino-examples +- get_doc_url: get the doc url of eino website +- init_template: init the eino project template, to create files from template +` + +type EinoAssistantToolImpl struct { + config *EinoAssistantToolConfig +} + +type EinoAssistantToolConfig struct { + BaseDir string +} + +func defaultEinoAssistantToolConfig(ctx context.Context) (*EinoAssistantToolConfig, error) { + config := &EinoAssistantToolConfig{ + BaseDir: "./data/eino", + } + return config, nil +} + +func NewEinoAssistantTool(ctx context.Context, config *EinoAssistantToolConfig) (tn tool.BaseTool, err error) { + if config == nil { + config, err = defaultEinoAssistantToolConfig(ctx) + if err != nil { + return nil, err + } + } + t := &EinoAssistantToolImpl{config: config} + tn, err = t.ToEinoTool() + if err != nil { + return nil, err + } + return tn, nil +} + +var ( + EinoRepo = map[string]string{ + "eino": "https://github.com/cloudwego/eino", + "eino-ext": "https://github.com/cloudwego/eino-ext", + "eino-examples": "https://github.com/cloudwego/eino-examples", + } + + EinoDoc = map[string]string{ + "eino_index": "https://www.cloudwego.io/zh/docs/eino/", + "quickstart": "https://www.cloudwego.io/zh/docs/eino/quick_start/", + "graph": "https://www.cloudwego.io/zh/docs/eino/core_modules/chain_and_graph_orchestration/", + "agent": "https://www.cloudwego.io/zh/docs/eino/core_modules/flow_integration_components/", + "components": "https://www.cloudwego.io/zh/docs/eino/core_modules/components/", + "integrate": "https://www.cloudwego.io/zh/docs/eino/ecosystem_integration/", + } + + EinoExample = map[string][]string{ + "agent": {"https://github.com/cloudwego/eino-examples/tree/main/flow/agent/react"}, + "components": {"https://github.com/cloudwego/eino-examples/tree/main/components"}, + "graph": {"https://github.com/cloudwego/eino-examples/tree/main/compose/graph/tool_call_agent.go"}, + "quickstart": {"https://github.com/cloudwego/eino-examples/tree/main/quickstart"}, + } + + Template = map[string][]string{ + "react_agent": {"react_agent/main.go"}, + "simple_llm": {"simple_llm/main.go"}, + "http_agent": {"http_agent/main.go", "http_agent/README.md", "http_agent/client/main.go"}, + } +) + +func (e *EinoAssistantToolImpl) ToEinoTool() (tool.BaseTool, error) { + return utils.InferTool("eino_tool", desc, e.Invoke) +} + +func (e *EinoAssistantToolImpl) Invoke(ctx context.Context, req *EinoToolRequest) (res *EinoToolResponse, err error) { + res = &EinoToolResponse{} + + switch req.Action { + case EinoToolActionGetExampleProject: + exampleURL := EinoExample[req.ExampleType] + if len(exampleURL) == 0 { + res.Error = "invalid example type, can be one of: agent, components, graph, quickstart. example repo is " + EinoRepo["eino-examples"] + return + } + res.Message = exampleURL[0] + case EinoToolActionGetGithubRepo: + repoURL := EinoRepo[req.RepoType] + if repoURL == "" { + res.Error = "invalid repo type, can be one of: eino, eino-ext, eino-examples. eino repo url is " + EinoRepo["eino"] + return + } + res.Message = repoURL + case EinoToolActionGetDocURL: + docURL := EinoDoc[req.DocType] + if docURL == "" { + res.Error = "invalid doc type, can be one of: eino_index, quickstart, graph, agent, components, integrate. eino doc url is " + EinoDoc["eino_index"] + return + } + res.Message = docURL + case EinoToolActionInitTemplate: + templateURL := Template[req.TemplateType] + if len(templateURL) == 0 { + res.Error = "invalid template type, can be one of: react_agent, simple_llm, http_agent" + return res, nil + } + + baseDir := e.config.BaseDir + for _, file := range templateURL { + // Read template file + content, err := templateFS.ReadFile(filepath.Join("templates", file)) + if err != nil { + res.Error = "failed to read template file: " + err.Error() + return res, nil + } + + // Create target directory + targetPath := filepath.Join(baseDir, file) + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + res.Error = "failed to create directory: " + err.Error() + return res, nil + } + + // Write file + if err := os.WriteFile(targetPath, content, 0644); err != nil { + res.Error = "failed to write file: " + err.Error() + return res, nil + } + } + absPath, err := filepath.Abs(filepath.Join(baseDir, req.TemplateType)) + if err != nil { + absPath = filepath.Join(baseDir, req.TemplateType) + } + res.Message = "success, init template, path is: " + absPath + return res, nil + default: + res.Error = "invalid action, can be one of: get_example_project, get_github_repo, get_doc_url" + } + + return res, nil +} + +type EinoToolAction string + +const ( + EinoToolActionGetExampleProject EinoToolAction = "get_example_project" // 获取示例项目 + EinoToolActionGetGithubRepo EinoToolAction = "get_github_repo" // 获取 github 仓库 + EinoToolActionGetDocURL EinoToolAction = "get_doc_url" // 获取文档地址 + EinoToolActionInitTemplate EinoToolAction = "init_template" // 初始化项目模板 +) + +type EinoToolRequest struct { + Action EinoToolAction `json:"action" jsonschema:"description='The action of the request',enum=get_example_project,enum=get_github_repo,enum=get_doc_url,enum=init_template"` + ExampleType string `json:"example_type,omitempty" jsonschema:"description='The type of the example project, only for action: get_example_project',enum=agent,enum=components,enum=graph,enum=quickstart"` + RepoType string `json:"repo_type,omitempty" jsonschema:"description='The type of the repo, only for action: get_github_repo',enum=eino,enum=eino-ext,enum=eino-examples"` + DocType string `json:"doc_type,omitempty" jsonschema:"description='The type of the doc, only for action: get_doc_url',enum=eino_index,enum=quickstart,enum=graph,enum=agent,enum=components,enum=integrate"` + TemplateType string `json:"template_type,omitempty" jsonschema:"description='The template of the project, only for action: init_template',enum=react_agent,enum=simple_llm,enum=http_agent"` +} + +type EinoToolResponse struct { + Message string `json:"message" jsonschema:"description=The message of the response"` + Error string `json:"error" jsonschema:"description=The error of the response"` +} diff --git a/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/README.md b/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/README.md new file mode 100644 index 0000000..637191e --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/README.md @@ -0,0 +1,28 @@ +# http_agent + +## 简介 + +http_agent 是一个基于 eino 的 http 服务构建的一个简单的 llm 应用。 + +## 使用 + +### 启动 http server + +```bash +go run main.go -model=ep-xxxx -apikey=xxx +``` + +### 使用 curl 访问 http server + +```bash +curl 'http://127.0.0.1:8888/chat?id=123&msg=hello' +``` +> 注意,由于采用了 sse 的格式,结果中会有 `data:` 前缀 + +### 使用 client + +client 是一个简单的交互式客户端,可以与 http server 进行交互,并打印结果。 + +```bash +go run client/main.go +``` diff --git a/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/client/main.go b/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/client/main.go new file mode 100644 index 0000000..7eb4db2 --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/client/main.go @@ -0,0 +1,103 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "bufio" + "flag" + "fmt" + "math/rand" + "net/http" + "net/url" + "os" + "strconv" + "strings" +) + +var id = flag.String("id", "", "conversation id") + +func main() { + flag.Parse() + + if *id == "" { + *id = strconv.Itoa(rand.Intn(1000000)) + } + + // 开始交互式对话 + reader := bufio.NewReader(os.Stdin) + for { + fmt.Printf("🧑‍ : ") + input, err := reader.ReadString('\n') + if err != nil { + fmt.Printf("Error reading input: %v\n", err) + return + } + + input = strings.TrimSpace(input) + if input == "" || input == "exit" || input == "quit" { + return + } + + sendMessage(*id, input) + } +} + +func sendMessage(id, message string) { + baseURL := "http://127.0.0.1:8888/chat" + params := url.Values{} + params.Add("id", id) + params.Add("msg", message) + reqURL := baseURL + "?" + params.Encode() + + resp, err := http.Get(reqURL) + if err != nil { + fmt.Printf("Error making request: %v\n", err) + return + } + defer resp.Body.Close() + + fmt.Print("🤖 : ") + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "data:") { + content := strings.TrimPrefix(line, "data:") + content = strings.TrimSpace(content) + if content != "" { + fmt.Print(content) + } + } + } + fmt.Println() + fmt.Println() +} diff --git a/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/main.go b/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/main.go new file mode 100644 index 0000000..840e920 --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/einotool/templates/http_agent/main.go @@ -0,0 +1,223 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "sync" + + "github.com/cloudwego/eino-ext/components/model/ark" + "github.com/cloudwego/eino-ext/components/tool/duckduckgo" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent/react" + "github.com/cloudwego/eino/schema" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/hertz-contrib/sse" +) + +var ( + modelName = flag.String("model", "", "The model to use, eg. ep-xxxx") + apiKey = flag.String("apikey", "", "The apikey of the model, eg. xxx") + prompt = flag.String("prompt", "you are a helpful assistant", "The system prompt to use") +) + +func main() { + flag.Parse() + if *modelName == "" || *apiKey == "" { + panic("model and apikey are required, you may get doubao model from: https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k") + } + + h := server.Default() + memory := &SimpleMemory{conversations: make(map[string]*Conversation)} + + h.GET("/chat", func(ctx context.Context, c *app.RequestContext) { + id := c.Query("id") + if id == "" { + c.JSON(consts.StatusBadRequest, map[string]string{"error": "missing id, it's required for saving conversation, example: /chat?id=123"}) + return + } + + msgString := c.Query("msg") + if msgString == "" { + c.JSON(consts.StatusBadRequest, map[string]string{"error": "missing msg, it's required for saving conversation, example: /chat?id=123&msg=hello"}) + return + } + + conv := memory.GetOrCreateConversation(id) + msg := schema.UserMessage(msgString) + conv.Append(msg) + + msgs := conv.GetMessages() + + agent, err := NewAgent(ctx) + if err != nil { + c.JSON(consts.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + sr, err := agent.Stream(ctx, msgs) + if err != nil { + c.JSON(consts.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + c.SetStatusCode(consts.StatusOK) + c.Response.Header.Set("Content-Type", "text/event-stream") + c.Response.Header.Set("Cache-Control", "no-cache") + c.Response.Header.Set("Connection", "keep-alive") + + s := sse.NewStream(c) + fullMsgs := make([]*schema.Message, 0) + + defer func() { + sr.Close() + c.Flush() + + if err != nil && !errors.Is(err, io.EOF) { + c.AbortWithStatusJSON(consts.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + fullMsg, err := schema.ConcatMessages(fullMsgs) + if err != nil { + fmt.Println("error concatenating messages: ", err.Error()) + return + } + conv.Append(fullMsg) + }() + + for { + var chunk *schema.Message + chunk, err = sr.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + fmt.Println("error receiving chunk: ", err.Error()) + return + } + fullMsgs = append(fullMsgs, chunk) + err = s.Publish(&sse.Event{ + Data: []byte(chunk.Content), + }) + if err != nil { + fmt.Println("error publishing event: ", err.Error()) + return + } + } + }) + + h.Spin() +} + +func NewAgent(ctx context.Context) (*react.Agent, error) { + + // 初始化模型 + model, err := PrepareModel(ctx) + if err != nil { + return nil, err + } + + // 初始化各种 tool + tools, err := PrepareTools(ctx) + if err != nil { + return nil, err + } + + // 初始化 agent + agent, err := react.NewAgent(ctx, &react.AgentConfig{ + Model: model, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: tools, + }, + MessageModifier: react.NewPersonaModifier(*prompt), + }) + if err != nil { + return nil, err + } + return agent, nil +} + +func PrepareModel(ctx context.Context) (model.ChatModel, error) { + + // eg. 使用 ark 豆包大模型, or openai: openai.NewChatModel at github.com/cloudwego/eino-ext/components/model/openai + arkModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ + Model: *modelName, // replace with your model + APIKey: *apiKey, // replace with your api key + }) + if err != nil { + return nil, err + } + return arkModel, nil +} + +func PrepareTools(ctx context.Context) ([]tool.BaseTool, error) { + duckduckgo, err := duckduckgo.NewTool(ctx, &duckduckgo.Config{}) + if err != nil { + return nil, err + } + return []tool.BaseTool{duckduckgo}, nil +} + +// simple memory can store messages of each conversation +type SimpleMemory struct { + mu sync.Mutex + conversations map[string]*Conversation +} + +func (m *SimpleMemory) GetOrCreateConversation(id string) *Conversation { + m.mu.Lock() + defer m.mu.Unlock() + + if _, ok := m.conversations[id]; !ok { + m.conversations[id] = &Conversation{ + ID: id, + Messages: make([]*schema.Message, 0), + } + } + + return m.conversations[id] +} + +type Conversation struct { + mu sync.Mutex + + ID string + Messages []*schema.Message +} + +func (c *Conversation) Append(msg *schema.Message) { + c.mu.Lock() + defer c.mu.Unlock() + + c.Messages = append(c.Messages, msg) +} + +func (c *Conversation) GetMessages() []*schema.Message { + c.mu.Lock() + defer c.mu.Unlock() + + return c.Messages +} diff --git a/quickstart/eino_assistant/pkg/tool/einotool/templates/react_agent/main.go b/quickstart/eino_assistant/pkg/tool/einotool/templates/react_agent/main.go new file mode 100644 index 0000000..22722fb --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/einotool/templates/react_agent/main.go @@ -0,0 +1,145 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + + "github.com/cloudwego/eino-ext/components/model/ark" + "github.com/cloudwego/eino-ext/components/tool/duckduckgo" + "github.com/cloudwego/eino/callbacks" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/flow/agent" + "github.com/cloudwego/eino/flow/agent/react" + "github.com/cloudwego/eino/schema" +) + +// usage: +// go run main.go -model=ep-xxxx -apikey=xxx 'do you know cloudwego, and what is the url of cloudwego? search for me please' + +var ( + // you can get model from: https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k + modelName = flag.String("model", "", "The model to use, eg. ep-xxxx") + apiKey = flag.String("apikey", "", "The apikey of the model, eg. xxx") +) + +func main() { + flag.Parse() + + ctx := context.Background() + reactAgent, err := NewAgent(ctx) + if err != nil { + panic(err) + } + + arg := flag.Arg(0) + if arg == "" { + panic("message is required, eg: ./llm -model=ep-xxxx -apikey=xxx 'do you know cloudwego?'") + } + + sr, err := reactAgent.Stream(ctx, []*schema.Message{ + schema.UserMessage(arg), + }, agent.WithComposeOptions(compose.WithCallbacks(LogCallback()))) + if err != nil { + panic(err) + } + + for { + msg, err := sr.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + panic(err) + } + fmt.Print(msg.Content) + } + fmt.Printf("\n\n=== %sFINISHED%s ===\n\n", green, reset) +} + +func NewAgent(ctx context.Context) (*react.Agent, error) { + + // 初始化模型 + model, err := PrepareModel(ctx) + if err != nil { + return nil, err + } + + // 初始化各种 tool + tools, err := PrepareTools(ctx) + if err != nil { + return nil, err + } + + // 初始化 agent + agent, err := react.NewAgent(ctx, &react.AgentConfig{ + Model: model, + ToolsConfig: compose.ToolsNodeConfig{ + Tools: tools, + }, + }) + if err != nil { + return nil, err + } + return agent, nil +} + +func PrepareModel(ctx context.Context) (model.ChatModel, error) { + + // eg. 使用 ark 豆包大模型, or openai: openai.NewChatModel at github.com/cloudwego/eino-ext/components/model/openai + arkModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ + Model: *modelName, // replace with your model + APIKey: *apiKey, // replace with your api key + }) + if err != nil { + return nil, err + } + return arkModel, nil +} + +func PrepareTools(ctx context.Context) ([]tool.BaseTool, error) { + duckduckgo, err := duckduckgo.NewTool(ctx, &duckduckgo.Config{}) + if err != nil { + return nil, err + } + return []tool.BaseTool{duckduckgo}, nil +} + +// log with color +var ( + green = "\033[32m" + reset = "\033[0m" +) + +func LogCallback() callbacks.Handler { + builder := callbacks.NewHandlerBuilder() + builder.OnStartFn(func(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context { + fmt.Printf("%s[view]%s: start [%s:%s:%s]\n", green, reset, info.Component, info.Type, info.Name) + return ctx + }) + builder.OnEndFn(func(ctx context.Context, info *callbacks.RunInfo, output callbacks.CallbackOutput) context.Context { + fmt.Printf("%s[view]%s: end [%s:%s:%s]\n", green, reset, info.Component, info.Type, info.Name) + return ctx + }) + return builder.Build() +} diff --git a/quickstart/eino_assistant/pkg/tool/einotool/templates/simple_llm/main.go b/quickstart/eino_assistant/pkg/tool/einotool/templates/simple_llm/main.go new file mode 100644 index 0000000..b940358 --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/einotool/templates/simple_llm/main.go @@ -0,0 +1,130 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "time" + + "github.com/cloudwego/eino-ext/components/model/ark" + "github.com/cloudwego/eino/components/model" + "github.com/cloudwego/eino/components/prompt" + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// usage: +// go run main.go -model=ep-xxxx -apikey=xxx -role=code_expert 'do you know cloudwego?' + +var ( + modelName = flag.String("model", "", "The model to use, eg. ep-xxxx") + apiKey = flag.String("apikey", "", "The apikey of the model, eg. xxx") + role = flag.String("role", "code_expert", "The role to use, eg. code_expert") +) + +func main() { + flag.Parse() + if *modelName == "" || *apiKey == "" { + panic("model and apikey are required, you may get doubao model from: https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k") + } + + ctx := context.Background() + chain, err := NewSimpleLLM(ctx) + if err != nil { + panic(err) + } + + arg1 := flag.Arg(0) + if arg1 == "" { + panic("message is required, eg: ./llm -model=ep-xxxx -apikey=xxx 'do you know cloudwego?'") + } + + runner, err := chain.Compile(ctx) + if err != nil { + panic(err) + } + + fmt.Printf("\n=== START ===\n\n") + + sr, err := runner.Stream(ctx, map[string]any{ + "role": *role, + "date": time.Now().Format("2006-01-02 15:04:05"), + "conversations": []*schema.Message{ + schema.UserMessage(arg1), + }, + }) + if err != nil { + panic(err) + } + + for { + msg, err := sr.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + panic(err) + } + fmt.Print(msg.Content) + } + fmt.Printf("\n\n=== FINISH ===\n") +} + +func NewSimpleLLM(ctx context.Context) (*compose.Chain[map[string]any, *schema.Message], error) { + chain := compose.NewChain[map[string]any, *schema.Message]() + + // replace with your model: https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k + model, err := PrepareModel(ctx, *modelName, *apiKey) + if err != nil { + return nil, err + } + + template, err := PrepareTemplate(ctx) + if err != nil { + return nil, err + } + + chain.AppendChatTemplate(template).AppendChatModel(model) + + return chain, nil +} + +func PrepareTemplate(ctx context.Context) (prompt.ChatTemplate, error) { + promptTemplate := `You are acting as a {role}. +You can only answer questions related to {role}, politely decline questions outside of this scope. +base info: time: {date}.` + + template := prompt.FromMessages(schema.FString, schema.SystemMessage(promptTemplate), schema.MessagesPlaceholder("conversations", false)) + + return template, nil +} + +func PrepareModel(ctx context.Context, modelName string, apiKey string) (model.ChatModel, error) { + // 使用 ark 豆包大模型, or openai: openai.NewChatModel at github.com/cloudwego/eino-ext/components/model/openai + model, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ + Model: modelName, + APIKey: apiKey, + }) + if err != nil { + return nil, err + } + return model, nil +} diff --git a/quickstart/eino_assistant/pkg/tool/gitclone/gitclone.go b/quickstart/eino_assistant/pkg/tool/gitclone/gitclone.go new file mode 100644 index 0000000..235bd6e --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/gitclone/gitclone.go @@ -0,0 +1,184 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gitclone + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" +) + +type GitCloneFileImpl struct { + config *GitCloneFileConfig +} + +type GitCloneFileConfig struct { + BaseDir string +} + +func defaultGitCloneFileConfig(ctx context.Context) (*GitCloneFileConfig, error) { + config := &GitCloneFileConfig{ + BaseDir: "./data/repos", + } + return config, nil +} + +func NewGitCloneFile(ctx context.Context, config *GitCloneFileConfig) (tn tool.BaseTool, err error) { + if config == nil { + config, err = defaultGitCloneFileConfig(ctx) + if err != nil { + return nil, err + } + } + if config.BaseDir == "" { + return nil, fmt.Errorf("base dir cannot be empty") + } + t := &GitCloneFileImpl{config: config} + tn, err = t.ToEinoTool() + if err != nil { + return nil, err + } + return tn, nil +} + +func (g *GitCloneFileImpl) ToEinoTool() (tool.BaseTool, error) { + return utils.InferTool("gitclone", "git clone or pull a repository", g.Invoke) +} + +func (g *GitCloneFileImpl) Invoke(ctx context.Context, req *GitCloneRequest) (res *GitCloneResponse, err error) { + res = &GitCloneResponse{} + + if req.Url == "" { + res.Error = "URL cannot be empty" + return res, nil + } + + valid, cloneURL := isValidGitURL(req.Url) + if !valid { + res.Error = fmt.Sprintf("Invalid Git URL format: %s", req.Url) + return res, nil + } + + repoDir, repoName := extractRepoDir(cloneURL) + repoDir = filepath.Join(g.config.BaseDir, repoDir) + repoPath := filepath.Join(repoDir, repoName) + + if err := os.MkdirAll(g.config.BaseDir, 0755); err != nil { + res.Error = fmt.Sprintf("Failed to create directory: %v", err) + return res, nil + } + + if req.Action == GitCloneActionClone { + if _, err := os.Stat(repoPath); err == nil { + res.Error = "Repository already exists" + return res, nil + } + + cmd := exec.CommandContext(ctx, "git", "clone", cloneURL, repoPath) + if output, err := cmd.CombinedOutput(); err != nil { + res.Error = fmt.Sprintf("Clone failed: %v, output: %s", err, output) + return res, nil + } + } else if req.Action == GitCloneActionPull { + if _, err := os.Stat(repoPath); os.IsNotExist(err) { + res.Error = fmt.Sprintf("repo does not exist: %s", repoPath) + return res, nil + } + + cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "pull") + if output, err := cmd.CombinedOutput(); err != nil { + res.Error = fmt.Sprintf("Pull failed: %v, output: %s", err, output) + return res, nil + } + + } + + absPath, err := filepath.Abs(repoPath) + if err != nil { + res.Error = fmt.Sprintf("failed to get absolute [%s] path: %v", repoPath, err) + return res, nil + } + res.Message = fmt.Sprintf("success, repo path: %s", absPath) + return res, nil +} + +// 辅助函数:验证 Git URL 格式 +func isValidGitURL(url string) (bool, string) { + cleanURL := strings.TrimSuffix(url, ".git") + + parts := strings.Split(cleanURL, "/") + if len(parts) < 2 { + return false, "" + } + + var standardURL string + switch { + // SSH 格式: git@domain:group/repo + case strings.HasPrefix(url, "git@"): + if strings.Contains(url, ":") { + return true, withGit(url) // 已经是标准 SSH 格式 + } + return false, "" + + // 完整 HTTPS 格式: https://domain/group/repo + case strings.HasPrefix(url, "http://"), strings.HasPrefix(url, "https://"): + return true, withGit(url) // 已经是标准 HTTPS 格式 + + default: + standardURL = "https://" + withGit(url) + } + + return true, standardURL +} + +func withGit(url string) string { + if !strings.HasSuffix(url, ".git") { + url += ".git" + } + return url +} + +// 辅助函数:从 URL 提取 group 和 repo +func extractRepoDir(url string) (string, string) { + parts := strings.Split(url, "/") + repoDir := parts[len(parts)-2] + repoName := strings.TrimSuffix(parts[len(parts)-1], ".git") + return repoDir, repoName +} + +type GitCloneAction string + +const ( + GitCloneActionClone GitCloneAction = "clone" + GitCloneActionPull GitCloneAction = "pull" +) + +type GitCloneRequest struct { + Url string `json:"url" jsonschema:"description=The URL of the repository to clone"` + Action GitCloneAction `json:"action" jsonschema:"description=The action to perform, 'clone' or 'pull'"` +} + +type GitCloneResponse struct { + Message string `json:"message"` + Error string `json:"error"` +} diff --git a/quickstart/eino_assistant/pkg/tool/open/open.go b/quickstart/eino_assistant/pkg/tool/open/open.go new file mode 100644 index 0000000..7a24d28 --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/open/open.go @@ -0,0 +1,95 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package open + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" +) + +type OpenFileToolImpl struct { + config *OpenFileToolConfig +} + +type OpenFileToolConfig struct { +} + +func defaultOpenFileToolConfig(ctx context.Context) (*OpenFileToolConfig, error) { + config := &OpenFileToolConfig{} + return config, nil +} + +func NewOpenFileTool(ctx context.Context, config *OpenFileToolConfig) (tn tool.BaseTool, err error) { + if config == nil { + config, err = defaultOpenFileToolConfig(ctx) + if err != nil { + return nil, err + } + } + t := &OpenFileToolImpl{config: config} + tn, err = t.ToEinoTool() + if err != nil { + return nil, err + } + return tn, nil +} + +func (of *OpenFileToolImpl) ToEinoTool() (tool.InvokableTool, error) { + return utils.InferTool("open", "open a file/dir/web url in the system by default application", of.Invoke) +} + +func (of *OpenFileToolImpl) Invoke(ctx context.Context, req OpenReq) (res OpenRes, err error) { + if req.URI == "" { + res.Message = "uri is required" + return res, nil + } + + // if is file or dir, check if exists + if isFilePath(req.URI) { + if _, err := os.Stat(req.URI); os.IsNotExist(err) { + res.Message = fmt.Sprintf("file not exists: %s", req.URI) + return res, nil + } + } + + err = exec.Command("open", req.URI).Run() + if err != nil { + res.Message = fmt.Sprintf("failed to open %s: %s", req.URI, err.Error()) + return res, nil + } + + res.Message = fmt.Sprintf("success, open %s", req.URI) + return res, nil +} + +type OpenReq struct { + URI string `json:"uri" jsonschema:"description=The uri of the file/dir/web url to open"` +} + +type OpenRes struct { + Message string `json:"message" jsonschema:"description=The message of the operation"` +} + +func isFilePath(path string) bool { + return strings.HasPrefix(path, "file://") && !strings.Contains(path, "://") +} diff --git a/quickstart/eino_assistant/pkg/tool/task/README.md b/quickstart/eino_assistant/pkg/tool/task/README.md new file mode 100644 index 0000000..e089beb --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/task/README.md @@ -0,0 +1,125 @@ +# Task Tool + +一个简单的 Task 管理工具,支持 Web 界面和 API 接口。 + +## 功能特点 + +- 支持添加、更新、删除和列表查询 +- 支持按标题和内容搜索 +- 支持按完成状态筛选 +- 支持软删除 +- 支持按创建时间排序 +- 数据持久化到本地文件 +- 美观的 Web 界面 +- 实时自动更新 + +## 启动服务 + +```bash +go run cmd/web/main.go +``` + +服务默认在 8080 端口启动,可以通过环境变量 `PORT` 修改: + +```bash +PORT=3000 go run cmd/web/main.go +``` + +## API 使用示例 + +### 添加 Task + +```bash +curl -X POST http://127.0.0.1:8080/task/api \ + -H "Content-Type: application/json" \ + -d '{ + "action": "add", + "task": { + "title": "完成作业", + "content": "完成数学作业", + "deadline": "2024-01-15T18:00:00Z" + } + }' +``` + +### 更新 Task + +```bash +curl -X POST http://127.0.0.1:8080/task/api \ + -H "Content-Type: application/json" \ + -d '{ + "action": "update", + "task": { + "id": "task-id", + "completed": true + } + }' +``` + +### 删除 Task + +```bash +curl -X POST http://127.0.0.1:8080/task/api \ + -H "Content-Type: application/json" \ + -d '{ + "action": "delete", + "task": { + "id": "task-id" + } + }' +``` + +### 列出所有 Task + +```bash +curl -X POST http://127.0.0.1:8080/task/api \ + -H "Content-Type: application/json" \ + -d '{ + "action": "list", + "list": {} + }' +``` + +### 搜索和筛选 Task + +```bash +curl -X POST http://127.0.0.1:8080/task/api \ + -H "Content-Type: application/json" \ + -d '{ + "action": "list", + "list": { + "query": "作业", + "is_done": false, + "limit": 10 + } + }' +``` + +## API 响应格式 + +所有 API 响应都遵循以下格式: + +```json +{ + "status": "success", + "task_list": [ + { + "id": "uuid", + "title": "标题", + "content": "内容", + "completed": false, + "deadline": "2024-01-15T18:00:00Z", + "created_at": "2024-01-10T10:00:00Z" + } + ], + "error": "" +} +``` + +- `status`: 可能的值为 "success" 或 "error" +- `task_list`: Task 项列表,某些操作可能为空 +- `error`: 错误信息,成功时为空 + +## 数据存储 + +Task 数据以 JSON Lines 格式存储在 `.task/tasks.jsonl` 文件中。每行一个 Task 项,支持实时读写。 \ No newline at end of file diff --git a/quickstart/eino_assistant/pkg/tool/task/storage.go b/quickstart/eino_assistant/pkg/tool/task/storage.go new file mode 100644 index 0000000..8a5bb0b --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/task/storage.go @@ -0,0 +1,280 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package task + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" +) + +var defaultStorage *Storage + +type Storage struct { + filePath string + mu sync.RWMutex + cache map[string]*Task + dirty bool +} + +func GetDefaultStorage() *Storage { + if defaultStorage == nil { + InitDefaultStorage("./data/task") + } + return defaultStorage +} + +func InitDefaultStorage(dataDir string) error { + s, err := NewStorage(dataDir) + if err != nil { + return err + } + defaultStorage = s + return nil +} + +func NewStorage(dataDir string) (*Storage, error) { + if err := os.MkdirAll(dataDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create data directory: %v", err) + } + s := &Storage{ + filePath: filepath.Join(dataDir, "tasks.jsonl"), + cache: make(map[string]*Task), + } + + if err := s.loadFromDisk(); err != nil { + return nil, fmt.Errorf("failed to load from disk: %v", err) + } + + return s, nil +} + +func (s *Storage) loadFromDisk() error { + file, err := os.OpenFile(s.filePath, os.O_CREATE|os.O_RDONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + var task Task + if err := json.Unmarshal(scanner.Bytes(), &task); err != nil { + return fmt.Errorf("failed to unmarshal task: %v", err) + } + s.cache[task.ID] = &task + } + + return scanner.Err() +} + +func (s *Storage) Add(task *Task) error { + s.mu.Lock() + defer s.mu.Unlock() + + task.CreatedAt = time.Now().Format(time.RFC3339) + task.IsDeleted = false + s.cache[task.ID] = task + + // 直接追加到文件末尾 + file, err := os.OpenFile(s.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open file: %v", err) + } + defer file.Close() + + data, err := json.Marshal(task) + if err != nil { + return fmt.Errorf("failed to marshal task: %v", err) + } + + if _, err := file.Write(append(data, '\n')); err != nil { + return fmt.Errorf("failed to write task: %v", err) + } + + if err := file.Sync(); err != nil { + return fmt.Errorf("failed to sync file: %v", err) + } + + return nil +} + +func (s *Storage) List(params *ListParams) ([]*Task, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var activeTasks, completedTasks []*Task + for _, task := range s.cache { + if task.IsDeleted { + continue + } + + if params.Query != "" && !contains(task.Title, params.Query) && !contains(task.Content, params.Query) { + continue + } + + if params.IsDone != nil { + if task.Completed != *params.IsDone { + continue + } + } + + if task.Completed { + completedTasks = append(completedTasks, task) + } else { + activeTasks = append(activeTasks, task) + } + } + + // 按创建时间排序(最新的在前面) + sort.Slice(activeTasks, func(i, j int) bool { + return activeTasks[i].CreatedAt > activeTasks[j].CreatedAt + }) + sort.Slice(completedTasks, func(i, j int) bool { + return completedTasks[i].CreatedAt > completedTasks[j].CreatedAt + }) + + // 合并列表:未完成的在前,已完成的在后 + tasks := append(activeTasks, completedTasks...) + + if params.Limit != nil && len(tasks) > *params.Limit { + tasks = tasks[:*params.Limit] + } + + return tasks, nil +} + +func (s *Storage) Update(task *Task) error { + s.mu.Lock() + defer s.mu.Unlock() + + existing, exists := s.cache[task.ID] + if !exists || existing.IsDeleted { + return fmt.Errorf("task not found: %s", task.ID) + } + + // 只更新非空字段 + updated := *existing // 创建副本 + if task.Title != "" { + updated.Title = task.Title + } + if task.Content != "" { + updated.Content = task.Content + } + if task.Deadline != "" { + updated.Deadline = task.Deadline + } + // Completed 字段需要特殊处理,因为它是布尔值 + if task.Completed != existing.Completed { + updated.Completed = task.Completed + } + + s.cache[task.ID] = &updated + s.dirty = true + + return s.syncToDisk() +} + +func (s *Storage) Delete(id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + task, exists := s.cache[id] + if !exists || task.IsDeleted { + return fmt.Errorf("task not found: %s", id) + } + + // 标记删除 + task.IsDeleted = true + s.dirty = true + + return s.syncToDisk() +} + +func (s *Storage) syncToDisk() error { + if !s.dirty { + return nil + } + + // 创建临时文件 + tmpFile := s.filePath + ".tmp" + file, err := os.Create(tmpFile) + if err != nil { + return fmt.Errorf("failed to create temp file: %v", err) + } + defer file.Close() + + // 写入数据到临时文件 + for _, task := range s.cache { + data, err := json.Marshal(task) + if err != nil { + os.Remove(tmpFile) // 清理临时文件 + return fmt.Errorf("failed to marshal task: %v", err) + } + + if _, err := file.Write(append(data, '\n')); err != nil { + os.Remove(tmpFile) // 清理临时文件 + return fmt.Errorf("failed to write task: %v", err) + } + } + + // 确保所有数据都写入磁盘 + if err := file.Sync(); err != nil { + os.Remove(tmpFile) + return fmt.Errorf("failed to sync file: %v", err) + } + + // 关闭文件 + if err := file.Close(); err != nil { + os.Remove(tmpFile) + return fmt.Errorf("failed to close file: %v", err) + } + + // 备份现有文件(如果存在) + if _, err := os.Stat(s.filePath); err == nil { + backupFile := s.filePath + ".bak" + if err := os.Rename(s.filePath, backupFile); err != nil { + os.Remove(tmpFile) + return fmt.Errorf("failed to backup file: %v", err) + } + } + + // 将临时文件重命名为正式文件 + if err := os.Rename(tmpFile, s.filePath); err != nil { + // 如果重命名失败,尝试恢复备份 + if backupErr := os.Rename(s.filePath+".bak", s.filePath); backupErr != nil { + return fmt.Errorf("failed to rename temp file and restore backup: %v, backup error: %v", err, backupErr) + } + return fmt.Errorf("failed to rename temp file: %v", err) + } + + // 删除备份文件 + os.Remove(s.filePath + ".bak") + + s.dirty = false + return nil +} + +func contains(s, substr string) bool { + return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) +} diff --git a/quickstart/eino_assistant/pkg/tool/task/task.go b/quickstart/eino_assistant/pkg/tool/task/task.go new file mode 100644 index 0000000..3e769ae --- /dev/null +++ b/quickstart/eino_assistant/pkg/tool/task/task.go @@ -0,0 +1,198 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package task + +import ( + "context" + "fmt" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" + "github.com/google/uuid" +) + +type Action string + +const ( + ActionAdd Action = "add" + ActionGet Action = "get" + ActionUpdate Action = "update" + ActionDelete Action = "delete" + ActionList Action = "list" +) + +type Task struct { + ID string `json:"id" jsonschema:"description:id of the task"` + Title string `json:"title" jsonschema:"description:title of the task"` + Content string `json:"content" jsonschema:"description:content of the task"` + Completed bool `json:"completed" jsonschema:"description:completed status of the task"` + Deadline string `json:"deadline" jsonschema:"description:deadline of the task"` + IsDeleted bool `json:"is_deleted" jsonschema:"-"` + + CreatedAt string `json:"created_at" jsonschema:"description:created time of the task"` +} + +type TaskRequest struct { + Action Action `json:"action" jsonschema:"description:action to perform, enum:add,update,delete,list"` + Task *Task `json:"task" jsonschema:"description:task to add, update, or delete"` + List *ListParams `json:"list" jsonschema:"description:list parameters"` +} + +type ListParams struct { + Query string `json:"query" jsonschema:"description:query to search"` + IsDone *bool `json:"is_done" jsonschema:"description:filter by completed status"` + Limit *int `json:"limit" jsonschema:"description:limit the number of results"` +} + +type TaskResponse struct { + Status string `json:"status" jsonschema:"description:status of the response"` + + TaskList []*Task `json:"task_list" jsonschema:"description:list of tasks"` + + Error string `json:"error" jsonschema:"description:error message"` +} + +type TaskToolImpl struct { + config *TaskToolConfig +} + +type TaskToolConfig struct { + Storage *Storage +} + +func defaultTaskToolConfig(ctx context.Context) (*TaskToolConfig, error) { + config := &TaskToolConfig{ + Storage: GetDefaultStorage(), + } + return config, nil +} + +func NewTaskToolImpl(ctx context.Context, config *TaskToolConfig) (*TaskToolImpl, error) { + var err error + if config == nil { + config, err = defaultTaskToolConfig(ctx) + if err != nil { + return nil, err + } + } + + if config.Storage == nil { + return nil, fmt.Errorf("storage cannot be empty") + } + + t := &TaskToolImpl{config: config} + + return t, nil +} + +func NewTaskTool(ctx context.Context, config *TaskToolConfig) (tn tool.BaseTool, err error) { + if config == nil { + config, err = defaultTaskToolConfig(ctx) + if err != nil { + return nil, err + } + } + + if config.Storage == nil { + return nil, fmt.Errorf("storage cannot be empty") + } + + t := &TaskToolImpl{config: config} + tn, err = t.ToEinoTool() + if err != nil { + return nil, err + } + return tn, nil +} + +func (t *TaskToolImpl) ToEinoTool() (tool.BaseTool, error) { + return utils.InferTool("task_manager", "task manager tool, you can add, get, update, delete, list tasks", t.Invoke) +} + +func (t *TaskToolImpl) Invoke(ctx context.Context, req *TaskRequest) (res *TaskResponse, err error) { + res = &TaskResponse{} + + switch req.Action { + case ActionAdd: + if req.Task == nil { + res.Status = "error" + res.Error = "task is required for add action" + return res, nil + } + if req.Task.Title == "" { + res.Status = "error" + res.Error = "title is required" + return res, nil + } + req.Task.ID = uuid.New().String() + if err := t.config.Storage.Add(req.Task); err != nil { + res.Status = "error" + res.Error = fmt.Sprintf("failed to add task: %v", err) + return res, nil + } + res.TaskList = []*Task{req.Task} + + case ActionUpdate: + if req.Task == nil { + res.Status = "error" + res.Error = "task is required for update action" + return res, nil + } + if req.Task.ID == "" { + res.Status = "error" + res.Error = "id is required" + return res, nil + } + if err := t.config.Storage.Update(req.Task); err != nil { + res.Status = "error" + res.Error = fmt.Sprintf("failed to update task: %v", err) + return res, nil + } + res.TaskList = []*Task{req.Task} + + case ActionDelete: + if req.Task == nil || req.Task.ID == "" { + res.Status = "error" + res.Error = "task id is required for delete action" + return res, nil + } + if err := t.config.Storage.Delete(req.Task.ID); err != nil { + res.Status = "error" + res.Error = fmt.Sprintf("failed to delete task: %v", err) + return res, nil + } + + case ActionList: + if req.List == nil { + req.List = &ListParams{} + } + tasks, err := t.config.Storage.List(req.List) + if err != nil { + res.Status = "error" + res.Error = fmt.Sprintf("failed to list tasks: %v", err) + return res, nil + } + res.TaskList = tasks + + default: + res.Status = "error" + res.Error = fmt.Sprintf("unknown action: %s", req.Action) + } + + res.Status = "success" + return res, nil +}