feat(adk): deepagents example (#131)
parent
550f5b504c
commit
09c7e3515e
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/cloudwego/eino/components/prompt"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/params"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/tools"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/utils"
|
||||
)
|
||||
|
||||
func NewCodeAgent(ctx context.Context, operator commandline.Operator) (adk.Agent, error) {
|
||||
cm, err := utils.NewChatModel(ctx,
|
||||
utils.WithMaxTokens(14125),
|
||||
utils.WithTemperature(float32(1)),
|
||||
utils.WithTopP(float32(1)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preprocess := []tools.ToolRequestPreprocess{tools.ToolRequestRepairJSON}
|
||||
|
||||
ca, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
Name: "CodeAgent",
|
||||
Description: `This sub-agent is a code agent specialized in handling Excel files.
|
||||
It receives a clear task and accomplish the task by generating Python code and execute it.
|
||||
The agent leverages pandas for data analysis and manipulation, matplotlib for plotting and visualization, and openpyxl for reading and writing Excel files.
|
||||
The React agent should invoke this sub-agent whenever stepwise Python coding for Excel file operations is required, ensuring precise and efficient task execution.`,
|
||||
Instruction: `You are a code agent. Your workflow is as follows:
|
||||
1. You will be given a clear task to handle Excel files.
|
||||
2. You should analyse the task and use right tools to help coding.
|
||||
3. You should write python code to finish the task.
|
||||
4. You are preferred to write code execution result to another file for further usages.
|
||||
|
||||
You are in a react mode, and you should use the following libraries to help you finish the task:
|
||||
- pandas: for data analysis and manipulation
|
||||
- matplotlib: for plotting and visualization
|
||||
- openpyxl: for reading and writing Excel files
|
||||
|
||||
Notice:
|
||||
1. Tool Calls argument must be a valid json.
|
||||
2. Tool Calls argument should do not contains invalid suffix like ']<|FunctionCallEnd|>'.
|
||||
`,
|
||||
Model: cm,
|
||||
ToolsConfig: adk.ToolsConfig{
|
||||
ToolsNodeConfig: compose.ToolsNodeConfig{
|
||||
Tools: []tool.BaseTool{
|
||||
tools.NewWrapTool(tools.NewBashTool(operator), preprocess, []tools.ToolResponsePostprocess{tools.FilePostProcess}),
|
||||
tools.NewWrapTool(tools.NewTreeTool(operator), preprocess, nil),
|
||||
tools.NewWrapTool(tools.NewEditFileTool(operator), preprocess, []tools.ToolResponsePostprocess{tools.EditFilePostProcess}),
|
||||
tools.NewWrapTool(tools.NewReadFileTool(operator), preprocess, nil), // TODO: compress post process
|
||||
tools.NewWrapTool(tools.NewPythonRunnerTool(operator), preprocess, []tools.ToolResponsePostprocess{tools.FilePostProcess}),
|
||||
},
|
||||
},
|
||||
},
|
||||
GenModelInput: func(ctx context.Context, instruction string, input *adk.AgentInput) ([]adk.Message, error) {
|
||||
wd, ok := params.GetTypedContextParams[string](ctx, params.WorkDirSessionKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("work dir not found")
|
||||
}
|
||||
|
||||
tpl := prompt.FromMessages(schema.Jinja2,
|
||||
schema.SystemMessage(instruction),
|
||||
schema.UserMessage(`WorkingDirectory: {{ working_dir }}
|
||||
UserQuery: {{ user_query }}
|
||||
CurrentTime: {{ current_time }}
|
||||
`))
|
||||
|
||||
msgs, err := tpl.Format(ctx, map[string]any{
|
||||
"working_dir": wd,
|
||||
"user_query": utils.FormatInput(input.Messages),
|
||||
"current_time": utils.GetCurrentTime(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
},
|
||||
OutputKey: "",
|
||||
MaxIterations: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ca, nil
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/duckduckgo/v2"
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/utils"
|
||||
)
|
||||
|
||||
func NewWebSearchAgent(ctx context.Context) (adk.Agent, error) {
|
||||
cm, err := utils.NewChatModel(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchTool, err := duckduckgo.NewTextSearchTool(ctx, &duckduckgo.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
|
||||
Name: "WebSearchAgent",
|
||||
Description: "WebSearchAgent utilizes the ReAct model to analyze input information and accomplish tasks using web search tools.",
|
||||
Model: cm,
|
||||
ToolsConfig: adk.ToolsConfig{
|
||||
ToolsNodeConfig: compose.ToolsNodeConfig{
|
||||
Tools: []tool.BaseTool{searchTool},
|
||||
},
|
||||
},
|
||||
MaxIterations: 10,
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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 generic
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type PreviewFile struct {
|
||||
FilePath string `json:"file_path,omitempty" xml:"file_path"`
|
||||
SingleFilePreviews []*SingleFilePreview `json:"single_file_previews,omitempty" xml:"single_file_previews>single_file_preview"`
|
||||
}
|
||||
|
||||
type SingleFilePreview struct {
|
||||
SheetName string `json:"sheet_name,omitempty" xml:"sheet_name"`
|
||||
Header []*ExcelCell `json:"header" xml:"header"`
|
||||
Content [][]*ExcelCell `json:"content,omitempty" xml:"content"`
|
||||
MergedCells []*ExcelCell `json:"merged_cells,omitempty" xml:"merged_cells>merged_cell"`
|
||||
}
|
||||
|
||||
type ExcelCell struct {
|
||||
Address string `json:"address,omitempty" xml:"address"` // B1:D1 表示 B1 到 D1 范围的单元格, B1 表示单个单元格
|
||||
Value string `json:"value,omitempty" xml:"value"` // 单元格的值
|
||||
}
|
||||
|
||||
func PreviewPath(path string) ([]*PreviewFile, error) {
|
||||
filePaths, err := getAllFiles(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(filePaths) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
resp := make([]*PreviewFile, len(filePaths))
|
||||
eg := errgroup.Group{}
|
||||
eg.SetLimit(10)
|
||||
for i := range filePaths {
|
||||
idx := i
|
||||
fp := filePaths[idx]
|
||||
eg.Go(func() error {
|
||||
var (
|
||||
pf *PreviewFile
|
||||
e error
|
||||
)
|
||||
if ext := filepath.Ext(fp); ext != ".xlsx" { // .xls not support
|
||||
pf = &PreviewFile{FilePath: fp}
|
||||
} else {
|
||||
pf, e = previewExcelDocument(path)
|
||||
}
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
resp[idx] = pf
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func getAllFiles(path string) ([]string, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
if isHiddenFile(info.Name()) {
|
||||
return nil, nil
|
||||
}
|
||||
return []string{path}, nil
|
||||
}
|
||||
|
||||
var files []string
|
||||
if err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isHiddenFile(info.Name()) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func previewExcelDocument(filePath string) (*PreviewFile, error) {
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
pf := &PreviewFile{
|
||||
FilePath: filePath,
|
||||
SingleFilePreviews: nil,
|
||||
}
|
||||
for _, sheetName := range f.GetSheetList() {
|
||||
sfp, err := parseSheet(f, sheetName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pf.SingleFilePreviews = append(pf.SingleFilePreviews, sfp)
|
||||
}
|
||||
|
||||
return pf, nil
|
||||
}
|
||||
|
||||
func parseSheet(f *excelize.File, sheetName string) (*SingleFilePreview, error) {
|
||||
preview := &SingleFilePreview{
|
||||
SheetName: sheetName,
|
||||
Header: make([]*ExcelCell, 0),
|
||||
Content: make([][]*ExcelCell, 0),
|
||||
MergedCells: make([]*ExcelCell, 0),
|
||||
}
|
||||
mcs := make([][][]int, 20)
|
||||
mergedCells, err := f.GetMergeCells(sheetName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, cell := range mergedCells {
|
||||
lcol, lrow, err := excelize.CellNameToCoordinates(cell.GetStartAxis())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lrow >= 20 {
|
||||
continue
|
||||
}
|
||||
|
||||
rcol, rrow, err := excelize.CellNameToCoordinates(cell.GetEndAxis())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
preview.MergedCells = append(preview.MergedCells, &ExcelCell{
|
||||
Address: cell[0],
|
||||
Value: cell.GetCellValue(),
|
||||
})
|
||||
r := mcs[lrow]
|
||||
r = append(r, []int{lcol, lrow, rcol, rrow})
|
||||
}
|
||||
|
||||
rowIter, err := f.Rows(sheetName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := 0
|
||||
for rowIter.Next() && i < 20 {
|
||||
values, err := rowIter.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rowValues []*ExcelCell
|
||||
for j, val := range values {
|
||||
col, row := j+1, i+1
|
||||
skip := false
|
||||
if r := mcs[i]; len(r) > 0 {
|
||||
for _, coverage := range r {
|
||||
if col >= coverage[0] && row >= coverage[1] && col <= coverage[2] && row <= coverage[3] {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
addr, err := excelize.CoordinatesToCellName(col, row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rowValues = append(rowValues, &ExcelCell{Address: addr, Value: val})
|
||||
}
|
||||
if i == 0 {
|
||||
preview.Header = rowValues
|
||||
} else {
|
||||
preview.Content = append(preview.Content, rowValues)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return preview, nil
|
||||
}
|
||||
|
||||
func isHiddenFile(name string) bool {
|
||||
return len(name) > 0 && name[0] == '.'
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 generic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
)
|
||||
|
||||
type FullPlan struct {
|
||||
TaskID int `json:"task_id,omitempty"`
|
||||
Status PlanStatus `json:"status,omitempty"`
|
||||
AgentName string `json:"agent_name,omitempty"`
|
||||
Desc string `json:"desc,omitempty"`
|
||||
ExecResult *SubmitResult `json:"exec_result,omitempty"`
|
||||
}
|
||||
|
||||
type PlanStatus string
|
||||
|
||||
const (
|
||||
PlanStatusTodo PlanStatus = "todo"
|
||||
PlanStatusDoing PlanStatus = "doing"
|
||||
PlanStatusDone PlanStatus = "done"
|
||||
PlanStatusFailed PlanStatus = "failed"
|
||||
PlanStatusSkipped PlanStatus = "skipped"
|
||||
)
|
||||
|
||||
var (
|
||||
PlanStatusMapping = map[PlanStatus]string{
|
||||
PlanStatusTodo: "待执行",
|
||||
PlanStatusDoing: "执行中",
|
||||
PlanStatusDone: "已完成",
|
||||
PlanStatusFailed: "执行失败",
|
||||
PlanStatusSkipped: "已跳过",
|
||||
}
|
||||
)
|
||||
|
||||
func (p *FullPlan) String() string {
|
||||
status, ok := PlanStatusMapping[p.Status]
|
||||
if !ok {
|
||||
status = string(p.Status)
|
||||
}
|
||||
res := fmt.Sprintf("%d. **[%s]** %s", p.TaskID, status, p.Desc)
|
||||
if p.ExecResult != nil {
|
||||
res += fmt.Sprintf("\n%s", p.ExecResult.String())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (p *FullPlan) PlanString(n int) string {
|
||||
if p.Status != PlanStatusDoing && p.Status != PlanStatusTodo {
|
||||
return fmt.Sprintf("- [x] %d. %s", n, p.Desc)
|
||||
}
|
||||
return fmt.Sprintf("- [ ] %d. %s", n, p.Desc)
|
||||
}
|
||||
|
||||
func FullPlan2String(plan []*FullPlan) string {
|
||||
var planStr = "### 任务计划\n"
|
||||
for i, p := range plan {
|
||||
planStr += p.PlanString(i+1) + "\n"
|
||||
}
|
||||
return planStr
|
||||
}
|
||||
|
||||
func Write2PlanMD(ctx context.Context, op commandline.Operator, wd string, plan []*FullPlan) error {
|
||||
planStr := FullPlan2String(plan)
|
||||
filePath := filepath.Join(wd, "plan.md")
|
||||
return op.WriteFile(ctx, filePath, planStr)
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 generic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
type Step struct {
|
||||
Index int `json:"index"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
type Plan struct {
|
||||
Steps []Step `json:"steps"`
|
||||
}
|
||||
|
||||
func (p *Plan) FirstStep() string {
|
||||
if len(p.Steps) == 0 {
|
||||
return ""
|
||||
}
|
||||
stepStr, _ := sonic.MarshalString(p.Steps[0])
|
||||
return stepStr
|
||||
}
|
||||
|
||||
func (p *Plan) MarshalJSON() ([]byte, error) {
|
||||
type Alias Plan
|
||||
return json.Marshal((*Alias)(p))
|
||||
}
|
||||
|
||||
func (p *Plan) UnmarshalJSON(bytes []byte) error {
|
||||
type Alias Plan
|
||||
a := (*Alias)(p)
|
||||
return json.Unmarshal(bytes, a)
|
||||
}
|
||||
|
||||
var PlanToolInfo = &schema.ToolInfo{
|
||||
Name: "create_plan",
|
||||
Desc: "Generates a structured, step-by-step execution plan to solve a given complex task. Each step in the plan must be assigned to a specialized agent and must have a clear, actionable description.",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(
|
||||
map[string]*schema.ParameterInfo{
|
||||
"steps": {
|
||||
Type: schema.Array,
|
||||
ElemInfo: &schema.ParameterInfo{
|
||||
Type: schema.Object,
|
||||
SubParams: map[string]*schema.ParameterInfo{
|
||||
"index": {
|
||||
Type: schema.Integer,
|
||||
Desc: "The sequential number of this step in the overall plan. **Must start from 1 and increment by exactly 1 for each subsequent step.**",
|
||||
Required: true,
|
||||
},
|
||||
"desc": {
|
||||
Type: schema.String,
|
||||
Desc: "A clear, concise, and actionable description of the specific task to be performed in this step. It should be a direct instruction for the assigned agent.",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Desc: "different steps to follow, should be in sorted order",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 generic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SubmitResult struct {
|
||||
IsSuccess *bool `json:"is_success,omitempty"`
|
||||
Result string `json:"result,omitempty"`
|
||||
Files []*SubmitResultFile `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
type SubmitResultFile struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
Desc string `json:"desc,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SubmitResult) String() string {
|
||||
res := fmt.Sprintf("### 执行结果\n%s", s.Result)
|
||||
if len(s.Files) > 0 {
|
||||
res += "\n#### 中间产物"
|
||||
}
|
||||
for _, f := range s.Files {
|
||||
res += fmt.Sprintf("\n- 描述:%s, 路径:%s", f.Desc, f.Path)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func ListDir(dir string) ([]*SubmitResultFile, error) {
|
||||
var resp []*SubmitResultFile
|
||||
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(d.Name(), ".") {
|
||||
return nil
|
||||
}
|
||||
if path == dir {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
next := filepath.Join(dir, d.Name())
|
||||
nextResp, err := ListDir(next)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp = append(resp, nextResp...)
|
||||
return nil
|
||||
}
|
||||
resp = append(resp, &SubmitResultFile{
|
||||
Path: filepath.Join(filepath.Dir(dir), d.Name()),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* 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"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/cloudwego/eino/adk/prebuilt/deep"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudwego/eino-examples/adk/common/prints"
|
||||
"github.com/cloudwego/eino-examples/adk/common/trace"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/agents"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/generic"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/params"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/tools"
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set your own query here. e.g.
|
||||
// query := schema.UserMessage("统计附件文件中推荐的小说名称及推荐次数,并将结果写到文件中。凡是带有《》内容都是小说名称,形成表格,表头为小说名称和推荐次数,同名小说只列一行,推荐次数相加")
|
||||
// query := schema.UserMessage("Count the recommended novel names and recommended times in the attachment file, and write the results into the file. The content with "" is the name of the novel, forming a table. The header is the name of the novel and the number of recommendations. The novels with the same name are listed in one row, and the number of recommendations is added")
|
||||
|
||||
// query := schema.UserMessage("读取 模拟出题.csv 中的表格内容,规范格式将题目、答案、解析、选项放在同一行,简答题只把答案写入解析即可")
|
||||
// query := schema.UserMessage("Read the table content in the 模拟出题.csv, put the question, answer, resolution and options in the same line in a standardized format, and simply write the answer into the resolution")
|
||||
|
||||
query := schema.UserMessage("请帮我将 questions.csv 表格中的第一列提取到一个新的 csv 中")
|
||||
// query := schema.UserMessage("Please help me extract the first column in question.csv table into a new csv")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
traceCloseFn, startSpanFn := trace.AppendCozeLoopCallbackIfConfigured(ctx)
|
||||
defer traceCloseFn(ctx)
|
||||
|
||||
agent, err := newExcelAgent(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// uuid as task id
|
||||
id := uuid.New().String()
|
||||
|
||||
runner := adk.NewRunner(ctx, adk.RunnerConfig{
|
||||
Agent: agent,
|
||||
EnableStreaming: true,
|
||||
})
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var inputFileDir, workdir string
|
||||
if env := os.Getenv("EXCEL_AGENT_INPUT_DIR"); env != "" {
|
||||
inputFileDir = env
|
||||
} else {
|
||||
inputFileDir = filepath.Join(wd, "adk/multiagent/deep/playground/input")
|
||||
}
|
||||
|
||||
if env := os.Getenv("EXCEL_AGENT_WORK_DIR"); env != "" {
|
||||
workdir = filepath.Join(env, id)
|
||||
} else {
|
||||
workdir = filepath.Join(wd, "adk/multiagent/deep/playground", id)
|
||||
}
|
||||
|
||||
if err = os.Mkdir(workdir, 0755); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.CopyFS(workdir, os.DirFS(inputFileDir)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
previews, err := generic.PreviewPath(workdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx = params.InitContextParams(ctx)
|
||||
params.AppendContextParams(ctx, map[string]interface{}{
|
||||
params.FilePathSessionKey: inputFileDir,
|
||||
params.WorkDirSessionKey: workdir,
|
||||
params.UserAllPreviewFilesSessionKey: utils.ToJSONString(previews),
|
||||
params.TaskIDKey: id,
|
||||
})
|
||||
|
||||
ctx, endSpanFn := startSpanFn(ctx, "plan-execute-replan", query)
|
||||
|
||||
iter := runner.Run(ctx, []*schema.Message{query})
|
||||
|
||||
var (
|
||||
lastMessage adk.Message
|
||||
lastMessageStream *schema.StreamReader[adk.Message]
|
||||
)
|
||||
|
||||
for {
|
||||
event, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if event.Output != nil && event.Output.MessageOutput != nil {
|
||||
if lastMessageStream != nil {
|
||||
lastMessageStream.Close()
|
||||
}
|
||||
if event.Output.MessageOutput.IsStreaming {
|
||||
cpStream := event.Output.MessageOutput.MessageStream.Copy(2)
|
||||
event.Output.MessageOutput.MessageStream = cpStream[0]
|
||||
lastMessage = nil
|
||||
lastMessageStream = cpStream[1]
|
||||
} else {
|
||||
lastMessage = event.Output.MessageOutput.Message
|
||||
lastMessageStream = nil
|
||||
}
|
||||
}
|
||||
prints.Event(event)
|
||||
}
|
||||
|
||||
if lastMessage != nil {
|
||||
endSpanFn(ctx, lastMessage)
|
||||
} else if lastMessageStream != nil {
|
||||
msg, _ := schema.ConcatMessageStream(lastMessageStream)
|
||||
endSpanFn(ctx, msg)
|
||||
} else {
|
||||
endSpanFn(ctx, "finished without output message")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 30)
|
||||
}
|
||||
|
||||
func newExcelAgent(ctx context.Context) (adk.Agent, error) {
|
||||
operator := &LocalOperator{}
|
||||
|
||||
cm, err := utils.NewChatModel(ctx,
|
||||
utils.WithMaxTokens(4096),
|
||||
utils.WithTemperature(float32(0)),
|
||||
utils.WithTopP(float32(0)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ca, err := agents.NewCodeAgent(ctx, operator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wa, err := agents.NewWebSearchAgent(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deepAgent, err := deep.New(ctx, &deep.Config{
|
||||
Name: "ExcelAgent",
|
||||
Description: "an agent for excel task",
|
||||
ChatModel: cm,
|
||||
SubAgents: []adk.Agent{ca, wa},
|
||||
ToolsConfig: adk.ToolsConfig{
|
||||
ToolsNodeConfig: compose.ToolsNodeConfig{
|
||||
Tools: []tool.BaseTool{
|
||||
tools.NewWrapTool(tools.NewReadFileTool(operator), nil, nil),
|
||||
tools.NewWrapTool(tools.NewTreeTool(operator), nil, nil),
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxIteration: 100,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return deepAgent, nil
|
||||
}
|
||||
@ -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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/params"
|
||||
)
|
||||
|
||||
type LocalOperator struct{}
|
||||
|
||||
func (l *LocalOperator) ReadFile(ctx context.Context, path string) (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err.Error(), nil
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (l *LocalOperator) WriteFile(ctx context.Context, path string, content string) error {
|
||||
return os.WriteFile(path, []byte(content), 0666)
|
||||
}
|
||||
|
||||
func (l *LocalOperator) IsDirectory(ctx context.Context, path string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l *LocalOperator) Exists(ctx context.Context, path string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (l *LocalOperator) RunCommand(ctx context.Context, command string) (string, error) {
|
||||
wd, ok := params.GetTypedContextParams[string](ctx, params.WorkDirSessionKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("work dir not found")
|
||||
}
|
||||
|
||||
var shellCmd []string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
shellCmd = []string{"cmd.exe", "/C", command}
|
||||
default:
|
||||
shellCmd = []string{"/bin/sh", "-c", command}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, shellCmd[0], shellCmd[1:]...)
|
||||
cmd.Dir = wd
|
||||
|
||||
outBuf := new(bytes.Buffer)
|
||||
errBuf := new(bytes.Buffer)
|
||||
cmd.Stdout = outBuf
|
||||
cmd.Stderr = errBuf
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("internal error:\ncommand: %v\n\nexec error: %v", cmd.String(), errBuf.String()), nil
|
||||
}
|
||||
return outBuf.String(), nil
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 params
|
||||
|
||||
const (
|
||||
FilePathSessionKey = "file_path_session_key"
|
||||
UserAllPreviewFilesSessionKey = "user_all_preview_files_session_key"
|
||||
WorkDirSessionKey = "work_dir_session_key"
|
||||
TaskIDKey = "task_id"
|
||||
)
|
||||
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 params
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const customBizKey = "biz"
|
||||
|
||||
func InitContextParams(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, customBizKey, &sync.Map{}) // nolint
|
||||
}
|
||||
|
||||
func AppendContextParams(ctx context.Context, values map[string]interface{}) {
|
||||
params, ok := ctx.Value(customBizKey).(*sync.Map)
|
||||
if !ok {
|
||||
log.Printf("[params.AppendContextParams] Failed to get params from context")
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range values {
|
||||
params.Store(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func GetTypedContextParams[T any](ctx context.Context, mapKey string) (T, bool) {
|
||||
var empty T
|
||||
value, ok := getContextParams(ctx, mapKey)
|
||||
if !ok {
|
||||
return empty, false
|
||||
}
|
||||
valueT, ok := value.(T)
|
||||
if !ok {
|
||||
return empty, false
|
||||
}
|
||||
return valueT, true
|
||||
}
|
||||
|
||||
func MustGetContextParams[T any](ctx context.Context, mapKey string) T {
|
||||
var empty T
|
||||
value, ok := getContextParams(ctx, mapKey)
|
||||
if !ok {
|
||||
log.Printf("[params.MustGetContextParams] cannot get key: %v", mapKey)
|
||||
return empty
|
||||
}
|
||||
valueT, ok := value.(T)
|
||||
if !ok {
|
||||
log.Printf("[params.MustGetContextParams] value not string, key: %v", mapKey)
|
||||
return empty
|
||||
}
|
||||
return valueT
|
||||
}
|
||||
|
||||
func getContextParams(ctx context.Context, mapKey string) (interface{}, bool) {
|
||||
params, ok := ctx.Value(customBizKey).(*sync.Map)
|
||||
if !ok {
|
||||
log.Printf("[params.GetContextParams] Failed to get params from context")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return params.Load(mapKey)
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
bashToolInfo = &schema.ToolInfo{
|
||||
Name: "bash",
|
||||
Desc: `Run commands in a bash shell
|
||||
* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.
|
||||
* You don't have access to the internet via this tool.
|
||||
* You do have access to a mirror of common linux and python packages via apt and pip.
|
||||
* State is persistent across command calls and discussions with the user.
|
||||
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
|
||||
* Please avoid commands that may produce a very large amount of output.
|
||||
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.`,
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"command": {
|
||||
Type: "string",
|
||||
Desc: "The command to execute",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func NewBashTool(op commandline.Operator) tool.InvokableTool {
|
||||
return &bashTool{op: op}
|
||||
}
|
||||
|
||||
type bashTool struct {
|
||||
op commandline.Operator
|
||||
}
|
||||
|
||||
func (b *bashTool) Info(_ context.Context) (*schema.ToolInfo, error) {
|
||||
return bashToolInfo, nil
|
||||
}
|
||||
|
||||
type shellInput struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
func (b *bashTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
input := &shellInput{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(input.Command) == 0 {
|
||||
return "command cannot be empty", nil
|
||||
}
|
||||
o := tool.GetImplSpecificOptions(&options{b.op}, opts...)
|
||||
return o.op.RunCommand(ctx, input.Command)
|
||||
}
|
||||
@ -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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
editFileToolInfo = &schema.ToolInfo{
|
||||
Name: "edit_file",
|
||||
Desc: `This is a tool for editing file, with parameters including the file path and the content to be edited.
|
||||
During task processing, if there is a need to create a file or overwrite file content, this tool can be used.
|
||||
|
||||
Notice:
|
||||
- If the file does not exist, this tool creates it with permissions perm (0666); otherwise it will truncates it before writing, without changing permissions.
|
||||
- When using this tool, be sure that the file content is the complete full text; otherwise, it may cause loss or errors in the file content.
|
||||
- Only supports writing to text file s; writing to xls/xlsx files is not supported.`,
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"path": {
|
||||
Type: schema.String,
|
||||
Desc: "file absolute path",
|
||||
Required: true,
|
||||
},
|
||||
"content": {
|
||||
Type: schema.String,
|
||||
Desc: "file content",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func NewEditFileTool(op commandline.Operator) tool.InvokableTool {
|
||||
return &editFile{op: op}
|
||||
}
|
||||
|
||||
type editFile struct {
|
||||
op commandline.Operator
|
||||
}
|
||||
|
||||
func (e *editFile) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return editFileToolInfo, nil
|
||||
}
|
||||
|
||||
type editFileInput struct {
|
||||
Path string `name:"path"`
|
||||
Content string `name:"content"`
|
||||
}
|
||||
|
||||
func (e *editFile) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
input := &editFileInput{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(input.Path) == 0 {
|
||||
return "path can not be empty", nil
|
||||
}
|
||||
o := tool.GetImplSpecificOptions(&options{op: e.op}, opts...)
|
||||
err = o.op.WriteFile(ctx, input.Path, input.Content)
|
||||
if err != nil {
|
||||
return err.Error(), nil
|
||||
}
|
||||
return "edit file success", nil
|
||||
}
|
||||
@ -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 tools
|
||||
|
||||
import (
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
op commandline.Operator
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/params"
|
||||
)
|
||||
|
||||
var (
|
||||
toolPythonRunnerInfo = &schema.ToolInfo{
|
||||
Name: "python_runner",
|
||||
Desc: `Write Python code to a file and execute it, and return the execution result.
|
||||
Code would be overwritten to the same file when this tool is called multiple times.`,
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"code": {
|
||||
Type: "string",
|
||||
Desc: "Python code to be executed. " +
|
||||
"The code MUST be enclosed in a Markdown code block starting with ```python and ending with ```. " +
|
||||
"CRITICAL: The code within the block must be a raw, multi-line string with real newlines. " +
|
||||
"It MUST NOT be a JSON-escaped string containing literal '\\n' or '\\\"' sequences. " +
|
||||
"The code must be ready for direct execution without any unescaping. " +
|
||||
"Do not generate code comments.",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func NewPythonRunnerTool(op commandline.Operator) tool.InvokableTool {
|
||||
return &pythonRunnerTool{op: op}
|
||||
}
|
||||
|
||||
type pythonRunnerTool struct {
|
||||
op commandline.Operator
|
||||
}
|
||||
|
||||
func (p *pythonRunnerTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return toolPythonRunnerInfo, nil
|
||||
}
|
||||
|
||||
func (p *pythonRunnerTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
code := tryExtractCodeSnippet(argumentsInJSON)
|
||||
if code == "" {
|
||||
return "", fmt.Errorf("python code is empty, original=%s", argumentsInJSON)
|
||||
}
|
||||
taskID := params.MustGetContextParams[string](ctx, params.TaskIDKey)
|
||||
wd, ok := params.GetTypedContextParams[string](ctx, params.WorkDirSessionKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("work dir not found")
|
||||
}
|
||||
filePath := filepath.Join(wd, fmt.Sprintf("%s.py", taskID))
|
||||
|
||||
if err := p.op.WriteFile(ctx, filePath, code); err != nil {
|
||||
return fmt.Sprintf("failed to create python file %s: %v", filePath, err), nil
|
||||
}
|
||||
|
||||
pyExecutablePath := os.Getenv("EXCEL_AGENT_PYTHON_EXECUTABLE_PATH")
|
||||
if pyExecutablePath == "" {
|
||||
pyExecutablePath = "python"
|
||||
}
|
||||
result, err := p.op.RunCommand(ctx, fmt.Sprintf("%s %s", pyExecutablePath, filePath))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("execute error: %w", err)
|
||||
}
|
||||
if strings.HasPrefix(result, "internal error") {
|
||||
result = fmt.Sprintf("%s\n\n code after parse:\n%v", result, code)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func tryExtractCodeSnippet(res string) string {
|
||||
var input struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
var codeToProcess string
|
||||
err := jsoniter.Unmarshal([]byte(res), &input)
|
||||
if err != nil {
|
||||
codeToProcess = res
|
||||
} else {
|
||||
codeToProcess = input.Code
|
||||
}
|
||||
|
||||
rawCode := extractCodeSnippet(codeToProcess)
|
||||
unescapedCode := strings.NewReplacer(
|
||||
"\\\\", "\\",
|
||||
"\\n", "\n",
|
||||
"\\r", "\r",
|
||||
"\\t", "\t",
|
||||
"\\\"", "\"",
|
||||
"\\'", "'",
|
||||
).Replace(rawCode)
|
||||
|
||||
return unescapedCode
|
||||
}
|
||||
|
||||
func extractCodeSnippet(res string) string {
|
||||
codePattern := regexp.MustCompile("(?s)```python\\s*(.*?)\\s*```")
|
||||
codeMatch := codePattern.FindStringSubmatch(res)
|
||||
|
||||
if len(codeMatch) > 1 {
|
||||
return strings.TrimSpace(codeMatch[1])
|
||||
} else {
|
||||
fallbackPattern := regexp.MustCompile("(?s)```\\s*(.*?)\\s*```")
|
||||
fallbackMatch := fallbackPattern.FindStringSubmatch(res)
|
||||
if len(fallbackMatch) > 1 {
|
||||
return strings.TrimSpace(fallbackMatch[1])
|
||||
} else {
|
||||
return strings.TrimSpace(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
readFileToolInfo = &schema.ToolInfo{
|
||||
Name: "read_file",
|
||||
Desc: `This tool is used for reading file content, with parameters including the file path, starting line, and the number of lines to read. Content will be truncated if it is too long.
|
||||
For xlsx files, each sheet's information will be returned sequentially upon a single call. If multiple sheets' information is needed, only one call is required. The call will return the headers, merged cell information, and the first n_rows of data for each sheet.`,
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"path": {
|
||||
Type: schema.String,
|
||||
Desc: "file absolute path",
|
||||
Required: true,
|
||||
},
|
||||
"start_row": {
|
||||
Type: schema.Integer,
|
||||
Desc: "The starting line defaults to 1, meaning reading begins from the first line.",
|
||||
},
|
||||
"n_rows": {
|
||||
Type: schema.Integer,
|
||||
Desc: "Number of rows to read, -1 means reading from start_row to the end of the file, default is 20 rows. For xlsx, xls, and xlsm files, the default is 10 rows.",
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func NewReadFileTool(op commandline.Operator) tool.InvokableTool {
|
||||
return &readFile{op: op}
|
||||
}
|
||||
|
||||
type readFile struct {
|
||||
op commandline.Operator
|
||||
}
|
||||
|
||||
func (r *readFile) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return readFileToolInfo, nil
|
||||
}
|
||||
|
||||
type readFileInput struct {
|
||||
Path string `json:"path"`
|
||||
StartRow int `json:"start_row"`
|
||||
NRows int `json:"n_rows"`
|
||||
}
|
||||
|
||||
func (r *readFile) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
input := &readFileInput{}
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if input.Path == "" {
|
||||
return "path can not be empty", nil
|
||||
}
|
||||
if input.StartRow <= 0 {
|
||||
input.StartRow = 1
|
||||
}
|
||||
if input.NRows <= 0 {
|
||||
input.NRows = 20
|
||||
}
|
||||
o := tool.GetImplSpecificOptions(&options{op: r.op})
|
||||
cmd := fmt.Sprintf("python3 -c \"import sys; lines = (line for idx, line in enumerate(open(sys.argv[1], encoding='utf-8')) if %d <= idx < %d); print(''.join(lines))\" %s",
|
||||
input.StartRow, input.StartRow+input.NRows, input.Path)
|
||||
content, err := o.op.RunCommand(ctx, cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var (
|
||||
toolImageReaderInfo = &schema.ToolInfo{
|
||||
Name: "image_reader",
|
||||
Desc: "Tool for describing image content",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"query": {
|
||||
Type: "string",
|
||||
Desc: "Questions posed about the image",
|
||||
Required: true,
|
||||
},
|
||||
"image_path": {
|
||||
Type: "string",
|
||||
Desc: "The path of the image file",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func NewToolImageReader(visionModel model.BaseChatModel) tool.InvokableTool {
|
||||
return &localToolImageReader{visionModel: visionModel}
|
||||
}
|
||||
|
||||
type localToolImageReader struct {
|
||||
visionModel model.BaseChatModel
|
||||
}
|
||||
|
||||
func (t *localToolImageReader) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return toolImageReaderInfo, nil
|
||||
}
|
||||
|
||||
func (t *localToolImageReader) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
var params struct {
|
||||
Query string `json:"query"`
|
||||
ImagePath string `json:"image_path"`
|
||||
}
|
||||
if err := jsoniter.Unmarshal([]byte(argumentsInJSON), ¶ms); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if params.Query == "" || params.ImagePath == "" {
|
||||
return "", errors.New("missing parameters")
|
||||
}
|
||||
|
||||
f, err := os.Open(params.ImagePath)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("open file error: %v, file path: %v", err, params.ImagePath), nil
|
||||
}
|
||||
defer f.Close()
|
||||
fc, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("read file error: %v, file path: %v", err, params.ImagePath), nil
|
||||
}
|
||||
|
||||
mimeType := http.DetectContentType(fc)
|
||||
b64 := base64.StdEncoding.EncodeToString(fc)
|
||||
url := fmt.Sprintf("data:%s;base64,%s", mimeType, b64)
|
||||
msgs := []*schema.Message{
|
||||
schema.SystemMessage(""), // TODO: fill system prompt
|
||||
schema.UserMessage(params.Query),
|
||||
{
|
||||
Role: schema.User,
|
||||
UserInputMultiContent: []schema.MessageInputPart{
|
||||
{
|
||||
Type: schema.ChatMessagePartTypeImageURL,
|
||||
Image: &schema.MessageInputImage{
|
||||
MessagePartCommon: schema.MessagePartCommon{
|
||||
URL: &url,
|
||||
MIMEType: mimeType,
|
||||
},
|
||||
Detail: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := t.visionModel.Generate(ctx, msgs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.Content == "" {
|
||||
return "", errors.New("response is empty")
|
||||
}
|
||||
|
||||
return resp.Content, nil
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/tool/commandline"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
treeToolInfo = &schema.ToolInfo{
|
||||
Name: "tree",
|
||||
Desc: "This tool is used to view the directory tree structure; the parameter is the path to be viewed, and it returns the complete directory tree structure under that path.",
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||
"path": {
|
||||
Type: schema.String,
|
||||
Desc: "absolute path",
|
||||
Required: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func NewTreeTool(op commandline.Operator) tool.InvokableTool {
|
||||
return &tree{op: op}
|
||||
}
|
||||
|
||||
type tree struct {
|
||||
op commandline.Operator
|
||||
}
|
||||
|
||||
func (t *tree) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return treeToolInfo, nil
|
||||
}
|
||||
|
||||
type treeInput struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (t *tree) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
input := &treeInput{}
|
||||
|
||||
err := json.Unmarshal([]byte(argumentsInJSON), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(input.Path) == 0 {
|
||||
return "path can not be empty", nil
|
||||
}
|
||||
o := tool.GetImplSpecificOptions(&options{t.op}, opts...)
|
||||
return o.op.RunCommand(ctx, fmt.Sprintf("find %s", input.Path))
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/cloudwego/eino-examples/adk/multiagent/deep/utils"
|
||||
)
|
||||
|
||||
type ToolRequestPreprocess func(ctx context.Context, baseTool tool.InvokableTool, toolArguments string) (string, error)
|
||||
|
||||
type ToolResponsePostprocess func(ctx context.Context, baseTool tool.InvokableTool, toolResponse, toolArguments string) (string, error)
|
||||
|
||||
func NewWrapTool(t tool.InvokableTool, preprocess []ToolRequestPreprocess, postprocess []ToolResponsePostprocess) tool.InvokableTool {
|
||||
return &wrapTool{
|
||||
baseTool: t,
|
||||
preprocess: preprocess,
|
||||
postprocess: postprocess,
|
||||
}
|
||||
}
|
||||
|
||||
type wrapTool struct {
|
||||
baseTool tool.InvokableTool
|
||||
preprocess []ToolRequestPreprocess
|
||||
postprocess []ToolResponsePostprocess
|
||||
}
|
||||
|
||||
func (w *wrapTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||
return w.baseTool.Info(ctx)
|
||||
}
|
||||
|
||||
func (w *wrapTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||
for _, pre := range w.preprocess {
|
||||
var err error
|
||||
argumentsInJSON, err = pre(ctx, w.baseTool, argumentsInJSON)
|
||||
if err != nil {
|
||||
log.Printf("[WrapTool.PreProcess] failed to process response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := w.baseTool.InvokableRun(ctx, argumentsInJSON, opts...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, post := range w.postprocess {
|
||||
resp, err = post(ctx, w.baseTool, resp, argumentsInJSON)
|
||||
if err != nil {
|
||||
log.Printf("[WrapTool.PostProcess] failed to process response: %v", err)
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func ToolRequestRepairJSON(ctx context.Context, baseTool tool.InvokableTool, toolArguments string) (string, error) {
|
||||
return utils.RepairJSON(toolArguments), nil
|
||||
}
|
||||
|
||||
type runResult struct {
|
||||
Command string `json:"command"`
|
||||
StdOut []*stdoutData `json:"stdout"`
|
||||
StdErr []*stderrData `json:"stderr"`
|
||||
FileChange []*fileChangeData `json:"file_change"`
|
||||
ErrData []*errData `json:"err_data"`
|
||||
}
|
||||
|
||||
type stdoutData struct {
|
||||
Stdout string `json:"stdout"`
|
||||
}
|
||||
|
||||
type stderrData struct {
|
||||
Stderr string `json:"stderr"`
|
||||
}
|
||||
|
||||
type errData struct {
|
||||
Err string `json:"err"`
|
||||
}
|
||||
|
||||
type fileChangeData struct {
|
||||
FileType string `json:"file_type"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Uri string `json:"uri"`
|
||||
Url string `json:"url"`
|
||||
MultiMediaInfo *multiMediaInfo `json:"multi_media_info,omitempty"`
|
||||
}
|
||||
|
||||
type multiMediaInfo struct {
|
||||
MediaType string `json:"media_type"`
|
||||
AdditionalType string `json:"additional_type"`
|
||||
AdditionalInfo string `json:"additional_info"`
|
||||
}
|
||||
|
||||
func FilePostProcess(ctx context.Context, baseTool tool.InvokableTool, toolResponse, toolArguments string) (string, error) {
|
||||
rawResult := runResult{}
|
||||
if err := json.Unmarshal([]byte(toolResponse), &rawResult); err != nil {
|
||||
return toolResponse, nil
|
||||
}
|
||||
|
||||
type fileOutputFormat struct {
|
||||
FileType string `json:"Change subject (file/directory)"`
|
||||
Path string `json:"File/directory relative path"`
|
||||
Type string `json:"Change type (create/delete/update)"`
|
||||
}
|
||||
var (
|
||||
stdOut []string
|
||||
fileChange []fileOutputFormat
|
||||
stdErr []string
|
||||
)
|
||||
for _, item := range rawResult.StdOut {
|
||||
if item != nil {
|
||||
stdOut = append(stdOut, item.Stdout)
|
||||
}
|
||||
}
|
||||
for _, item := range rawResult.FileChange {
|
||||
if item != nil {
|
||||
fileChange = append(fileChange, fileOutputFormat{
|
||||
FileType: item.FileType,
|
||||
Path: item.Path,
|
||||
Type: item.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, item := range rawResult.StdErr {
|
||||
if item != nil {
|
||||
stdErr = append(stdErr, item.Stderr)
|
||||
}
|
||||
}
|
||||
for _, item := range rawResult.ErrData {
|
||||
if item != nil {
|
||||
stdErr = append(stdErr, item.Err)
|
||||
}
|
||||
}
|
||||
|
||||
var output string
|
||||
if len(fileChange) > 0 {
|
||||
fcText, _ := jsoniter.MarshalToString(fileChange)
|
||||
output += "fileChange: \n" + fcText + "\n"
|
||||
}
|
||||
if len(stdErr) > 0 {
|
||||
output += "shell command stderr and warnings:" + strings.Join(stdErr, "\n") + "\n"
|
||||
}
|
||||
if len(rawResult.StdOut) > 0 {
|
||||
output += "shell command stdout: " + strings.Join(stdOut, "\n") + "\n"
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func EditFilePostProcess(ctx context.Context, baseTool tool.InvokableTool, toolResponse, toolArguments string) (string, error) {
|
||||
return fmt.Sprintf("Write file: %s success!", toolResponse), nil
|
||||
}
|
||||
|
||||
func isImage(uri string) bool {
|
||||
ext := filepath.Ext(uri)
|
||||
for _, e := range []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic"} {
|
||||
if ext == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
arkmodel "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
|
||||
)
|
||||
|
||||
type CreateChatModelOption func(o *option)
|
||||
|
||||
func NewChatModel(ctx context.Context, opts ...CreateChatModelOption) (cm model.ToolCallingChatModel, err error) {
|
||||
o := &option{}
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
if modelName := os.Getenv("ARK_MODEL"); modelName != "" {
|
||||
conf := &ark.ChatModelConfig{
|
||||
APIKey: os.Getenv("ARK_API_KEY"),
|
||||
BaseURL: os.Getenv("ARK_BASE_URL"),
|
||||
Region: os.Getenv("ARK_REGION"),
|
||||
Model: modelName,
|
||||
MaxTokens: o.MaxTokens,
|
||||
Temperature: o.Temperature,
|
||||
TopP: o.TopP,
|
||||
}
|
||||
if o.DisableThinking != nil && *o.DisableThinking {
|
||||
conf.Thinking = &arkmodel.Thinking{
|
||||
Type: arkmodel.ThinkingTypeDisabled,
|
||||
}
|
||||
}
|
||||
if o.JsonSchema != nil {
|
||||
conf.ResponseFormat = &ark.ResponseFormat{
|
||||
Type: arkmodel.ResponseFormatJSONSchema,
|
||||
JSONSchema: &arkmodel.ResponseFormatJSONSchemaJSONSchemaParam{
|
||||
Name: o.JsonSchema.Name,
|
||||
Description: o.JsonSchema.Description,
|
||||
Schema: o.JsonSchema.JSONSchema,
|
||||
Strict: o.JsonSchema.Strict,
|
||||
},
|
||||
}
|
||||
}
|
||||
cm, err = ark.NewChatModel(ctx, conf)
|
||||
|
||||
} else if modelName = os.Getenv("OPENAI_MODEL"); modelName != "" {
|
||||
conf := &openai.ChatModelConfig{
|
||||
APIKey: os.Getenv("OPENAI_API_KEY"),
|
||||
ByAzure: func() bool {
|
||||
return os.Getenv("OPENAI_BY_AZURE") == "true"
|
||||
}(),
|
||||
BaseURL: os.Getenv("OPENAI_BASE_URL"),
|
||||
Model: modelName,
|
||||
MaxTokens: o.MaxTokens,
|
||||
Temperature: o.Temperature,
|
||||
TopP: o.TopP,
|
||||
}
|
||||
if o.JsonSchema != nil {
|
||||
conf.ResponseFormat = &openai.ChatCompletionResponseFormat{
|
||||
Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
|
||||
JSONSchema: o.JsonSchema,
|
||||
}
|
||||
}
|
||||
cm, err = openai.NewChatModel(ctx, conf)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cm == nil {
|
||||
return nil, fmt.Errorf("no model config")
|
||||
}
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
type option struct {
|
||||
MaxTokens *int
|
||||
Temperature *float32
|
||||
TopP *float32
|
||||
DisableThinking *bool
|
||||
JsonSchema *openai.ChatCompletionResponseFormatJSONSchema
|
||||
}
|
||||
|
||||
func WithMaxTokens(maxTokens int) CreateChatModelOption {
|
||||
return func(o *option) {
|
||||
o.MaxTokens = &maxTokens
|
||||
}
|
||||
}
|
||||
|
||||
func WithTemperature(temp float32) CreateChatModelOption {
|
||||
return func(o *option) {
|
||||
o.Temperature = &temp
|
||||
}
|
||||
}
|
||||
|
||||
func WithTopP(topP float32) CreateChatModelOption {
|
||||
return func(o *option) {
|
||||
o.TopP = &topP
|
||||
}
|
||||
}
|
||||
|
||||
func WithDisableThinking(disable bool) CreateChatModelOption {
|
||||
return func(o *option) {
|
||||
o.DisableThinking = &disable
|
||||
}
|
||||
}
|
||||
|
||||
func WithResponseFormatJsonSchema(schema *openai.ChatCompletionResponseFormatJSONSchema) CreateChatModelOption {
|
||||
return func(o *option) {
|
||||
o.JsonSchema = schema
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/cloudwego/eino/adk"
|
||||
"github.com/cloudwego/eino/adk/prebuilt/planexecute"
|
||||
"github.com/kaptinlin/jsonrepair"
|
||||
)
|
||||
|
||||
type panicErr struct {
|
||||
info any
|
||||
stack []byte
|
||||
}
|
||||
|
||||
func (p *panicErr) Error() string {
|
||||
return fmt.Sprintf("panic error: %v, \nstack: %s", p.info, string(p.stack))
|
||||
}
|
||||
|
||||
// NewPanicErr creates a new panic error.
|
||||
// panicErr is a wrapper of panic info and stack trace.
|
||||
// it implements the error interface, can print error message of info and stack trace.
|
||||
func NewPanicErr(info any, stack []byte) error {
|
||||
return &panicErr{
|
||||
info: info,
|
||||
stack: stack,
|
||||
}
|
||||
}
|
||||
|
||||
func PtrOf[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func FormatInput(input []adk.Message) string {
|
||||
var sb strings.Builder
|
||||
for _, msg := range input {
|
||||
sb.WriteString(msg.Content)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type TaskGroup interface {
|
||||
Go(f func() error)
|
||||
Wait() error
|
||||
}
|
||||
|
||||
func ToJSONString(v interface{}) string {
|
||||
str, _ := sonic.MarshalString(v)
|
||||
return str
|
||||
}
|
||||
|
||||
func GetCurrentTime() string {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
if err != nil {
|
||||
// 出现错误时 fallback 到本地时间
|
||||
return time.Now().Format("2006-01-02 15:04:05 MST")
|
||||
}
|
||||
return time.Now().In(loc).Format("2006-01-02 15:04:05 MST")
|
||||
}
|
||||
|
||||
func RepairJSON(input string) string {
|
||||
input = strings.TrimPrefix(input, "<|FunctionCallBegin|>")
|
||||
input = strings.TrimSuffix(input, "<|FunctionCallEnd|>")
|
||||
input = strings.TrimPrefix(input, "<think>")
|
||||
output, err := jsonrepair.JSONRepair(input)
|
||||
if err != nil {
|
||||
return input
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func GetSessionValue[T any](ctx context.Context, key string) (T, bool) {
|
||||
v, ok := adk.GetSessionValue(ctx, key)
|
||||
if !ok {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
t, ok := v.(T)
|
||||
if !ok {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return t, true
|
||||
}
|
||||
|
||||
func FormatExecutedSteps(in []planexecute.ExecutedStep) string {
|
||||
var sb strings.Builder
|
||||
for idx, m := range in {
|
||||
sb.WriteString(fmt.Sprintf("## %d. Step: %v\n Result: %v\n\n", idx+1, m.Step, m.Result))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
Loading…
Reference in New Issue