From 6114f39861fd9101ca006c3510b343d8b1d9b9d4 Mon Sep 17 00:00:00 2001 From: Megumin Date: Tue, 10 Mar 2026 16:25:10 +0800 Subject: [PATCH] feat(adk): add search tool middleware example (#167) (#180) --- .../dynamictool/toolsearch/tools.go | 135 ++++++++++++++++++ .../dynamictool/toolsearch/toolsearch.go | 81 +++++++++++ adk/middlewares/skill/main.go | 105 ++++++++++++++ .../workdir/skills/log_analyzer/SKILL.md | 24 ++++ .../skills/log_analyzer/scripts/analyze.py | 46 ++++++ adk/middlewares/skill/workdir/test.log | 6 + go.mod | 4 +- go.sum | 8 +- 8 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 adk/middlewares/dynamictool/toolsearch/tools.go create mode 100644 adk/middlewares/dynamictool/toolsearch/toolsearch.go create mode 100644 adk/middlewares/skill/main.go create mode 100644 adk/middlewares/skill/workdir/skills/log_analyzer/SKILL.md create mode 100644 adk/middlewares/skill/workdir/skills/log_analyzer/scripts/analyze.py create mode 100644 adk/middlewares/skill/workdir/test.log diff --git a/adk/middlewares/dynamictool/toolsearch/tools.go b/adk/middlewares/dynamictool/toolsearch/tools.go new file mode 100644 index 0000000..63a211d --- /dev/null +++ b/adk/middlewares/dynamictool/toolsearch/tools.go @@ -0,0 +1,135 @@ +/* + * 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" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" +) + +type GetWeatherInput struct { + Location string `json:"location" jsonschema:"description=The city and state, e.g. San Francisco, CA"` + Unit string `json:"unit,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,description=The unit of temperature"` +} + +type GetWeatherOutput struct { + Temperature float64 `json:"temperature"` + Unit string `json:"unit"` + Condition string `json:"condition"` +} + +type GetForecastInput struct { + Location string `json:"location" jsonschema:"description=The city and state"` + Days int `json:"days" jsonschema:"description=Number of days to forecast (1-10)"` +} + +type GetForecastOutput struct { + Forecasts []DayForecast `json:"forecasts"` +} + +type DayForecast struct { + Day string `json:"day"` + Temperature float64 `json:"temperature"` + Condition string `json:"condition"` +} + +type GetStockPriceInput struct { + Ticker string `json:"ticker" jsonschema:"description=Stock ticker symbol (e.g., AAPL, GOOGL)"` + IncludeHistory bool `json:"include_history,omitempty" jsonschema:"description=Include historical data"` +} + +type GetStockPriceOutput struct { + Ticker string `json:"ticker"` + Price float64 `json:"price"` + Change float64 `json:"change"` +} + +type ConvertCurrencyInput struct { + Amount float64 `json:"amount" jsonschema:"description=Amount to convert"` + FromCurrency string `json:"from_currency" jsonschema:"description=Source currency code (e.g., USD)"` + ToCurrency string `json:"to_currency" jsonschema:"description=Target currency code (e.g., EUR)"` +} + +type ConvertCurrencyOutput struct { + OriginalAmount float64 `json:"original_amount"` + ConvertedAmount float64 `json:"converted_amount"` + ExchangeRate float64 `json:"exchange_rate"` +} + +func createWeatherTools() []tool.BaseTool { + getWeather, _ := utils.InferTool( + "get_weather", + "Get the current weather in a given location", + func(ctx context.Context, input *GetWeatherInput) (*GetWeatherOutput, error) { + return &GetWeatherOutput{ + Temperature: 22.5, + Unit: input.Unit, + Condition: "Sunny", + }, nil + }, + ) + + getForecast, _ := utils.InferTool( + "get_forecast", + "Get the weather forecast for multiple days ahead", + func(ctx context.Context, input *GetForecastInput) (*GetForecastOutput, error) { + forecasts := make([]DayForecast, input.Days) + for i := 0; i < input.Days; i++ { + forecasts[i] = DayForecast{ + Day: fmt.Sprintf("Day %d", i+1), + Temperature: 20.0 + float64(i), + Condition: "Partly Cloudy", + } + } + return &GetForecastOutput{Forecasts: forecasts}, nil + }, + ) + + return []tool.BaseTool{getWeather, getForecast} +} + +func createFinanceTools() []tool.BaseTool { + getStockPrice, _ := utils.InferTool( + "get_stock_price", + "Get the current stock price and market data for a given ticker symbol", + func(ctx context.Context, input *GetStockPriceInput) (*GetStockPriceOutput, error) { + return &GetStockPriceOutput{ + Ticker: input.Ticker, + Price: 150.25, + Change: 2.5, + }, nil + }, + ) + + convertCurrency, _ := utils.InferTool( + "convert_currency", + "Convert an amount from one currency to another using current exchange rates", + func(ctx context.Context, input *ConvertCurrencyInput) (*ConvertCurrencyOutput, error) { + rate := 0.85 + return &ConvertCurrencyOutput{ + OriginalAmount: input.Amount, + ConvertedAmount: input.Amount * rate, + ExchangeRate: rate, + }, nil + }, + ) + + return []tool.BaseTool{getStockPrice, convertCurrency} +} diff --git a/adk/middlewares/dynamictool/toolsearch/toolsearch.go b/adk/middlewares/dynamictool/toolsearch/toolsearch.go new file mode 100644 index 0000000..a592ea7 --- /dev/null +++ b/adk/middlewares/dynamictool/toolsearch/toolsearch.go @@ -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 main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino-ext/components/model/ark" + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/middlewares/dynamictool/toolsearch" + + "github.com/cloudwego/eino-examples/adk/common/prints" +) + +func main() { + ctx := context.Background() + + weatherTools := createWeatherTools() + financeTools := createFinanceTools() + allDynamicTools := append(weatherTools, financeTools...) + + toolSearchMiddleware, err := toolsearch.New(ctx, &toolsearch.Config{ + DynamicTools: allDynamicTools, + }) + if err != nil { + fmt.Printf("failed to create tool search middleware: %v\n", err) + return + } + + chatModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ + APIKey: os.Getenv("ARK_API_KEY"), + Model: os.Getenv("ARK_MODEL"), + }) + if err != nil { + log.Fatal(err) + } + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "tool_search_agent", + Description: "An agent that can dynamically search and use tools from a large tool library", + Instruction: `You are a helpful assistant.`, + Model: chatModel, + Handlers: []adk.ChatModelAgentMiddleware{ + toolSearchMiddleware, + }, + }) + if err != nil { + fmt.Printf("failed to create agent: %v\n", err) + return + } + + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: agent, + EnableStreaming: true, + }) + iter := runner.Query(ctx, "What's the weather in Beijing?") + for { + event, ok := iter.Next() + if !ok { + break + } + prints.Event(event) + } +} diff --git a/adk/middlewares/skill/main.go b/adk/middlewares/skill/main.go new file mode 100644 index 0000000..3247bb3 --- /dev/null +++ b/adk/middlewares/skill/main.go @@ -0,0 +1,105 @@ +/* + * 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" + "log" + "os" + "path/filepath" + + "github.com/cloudwego/eino-ext/adk/backend/local" + "github.com/cloudwego/eino-ext/components/model/ark" + "github.com/cloudwego/eino/adk" + "github.com/cloudwego/eino/adk/middlewares/filesystem" + "github.com/cloudwego/eino/adk/middlewares/skill" + + "github.com/cloudwego/eino-examples/adk/common/prints" +) + +func main() { + ctx := context.Background() + pwd, _ := os.Getwd() + workDir := filepath.Join(pwd, "adk", "middlewares", "skill", "workdir") + skillsDir := filepath.Join(workDir, "skills") + + cm, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{ + APIKey: os.Getenv("ARK_API_KEY"), + Model: os.Getenv("ARK_MODEL"), + }) + if err != nil { + log.Fatal(err) + } + + be, err := local.NewBackend(ctx, &local.Config{}) + if err != nil { + log.Fatal(err) + } + fsm, err := filesystem.New(ctx, &filesystem.MiddlewareConfig{ + Backend: be, + }) + if err != nil { + log.Fatal(err) + } + + skillBackend, err := skill.NewBackendFromFilesystem(ctx, &skill.BackendFromFilesystemConfig{ + Backend: be, + BaseDir: skillsDir, + }) + if err != nil { + log.Fatalf("Failed to create skill backend: %v", err) + } + + sm, err := skill.NewMiddleware(ctx, &skill.Config{ + Backend: skillBackend, + }) + if err != nil { + log.Fatalf("Failed to create skill middleware: %v", err) + } + + agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{ + Name: "LogAnalysisAgent", + Description: "An agent that can analyze logs", + Instruction: "You are a helpful assistant.", + Model: cm, + Handlers: []adk.ChatModelAgentMiddleware{fsm, sm}, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + runner := adk.NewRunner(ctx, adk.RunnerConfig{ + Agent: agent, + }) + + input := fmt.Sprintf("Analyze the %s file", filepath.Join(workDir, "test.log")) + log.Println("User: ", input) + + iterator := runner.Query(ctx, input) + for { + event, ok := iterator.Next() + if !ok { + break + } + if event.Err != nil { + log.Printf("Error: %v\n", event.Err) + break + } + + prints.Event(event) + } +} diff --git a/adk/middlewares/skill/workdir/skills/log_analyzer/SKILL.md b/adk/middlewares/skill/workdir/skills/log_analyzer/SKILL.md new file mode 100644 index 0000000..0ecc318 --- /dev/null +++ b/adk/middlewares/skill/workdir/skills/log_analyzer/SKILL.md @@ -0,0 +1,24 @@ +--- +name: log_analyzer +description: A skill to analyze log files for errors and warnings using a Python script. +--- + +# Log Analyzer Skill + +This skill analyzes a log file to count the occurrences of "ERROR" and "WARNING" and lists the lines where they appear. + +## Capability + +The skill provides a Python script named `analyze.py` located in this directory. You can use this script to analyze any text file. + +## Usage + +To use this skill, execute the `analyze.py` script with the target log file as an argument. + +### Example + +```bash +python3 {{.BaseDirectory}}/scripts/analyze.py /path/to/logfile.log +``` + +**Note**: Replace `/path/to/logfile.log` with the actual path of the file you want to analyze. diff --git a/adk/middlewares/skill/workdir/skills/log_analyzer/scripts/analyze.py b/adk/middlewares/skill/workdir/skills/log_analyzer/scripts/analyze.py new file mode 100644 index 0000000..d85d65a --- /dev/null +++ b/adk/middlewares/skill/workdir/skills/log_analyzer/scripts/analyze.py @@ -0,0 +1,46 @@ +import sys +import os + +def analyze_log(file_path): + if not os.path.exists(file_path): + print(f"Error: File '{file_path}' not found.") + return + + error_count = 0 + warning_count = 0 + error_lines = [] + warning_lines = [] + + try: + with open(file_path, 'r') as f: + for i, line in enumerate(f, 1): + content = line.strip() + if "ERROR" in content: + error_count += 1 + error_lines.append(f"Line {i}: {content}") + elif "WARNING" in content: + warning_count += 1 + warning_lines.append(f"Line {i}: {content}") + + print(f"Analysis Result for {file_path}:") + print(f"Total Errors: {error_count}") + print(f"Total Warnings: {warning_count}") + + if error_count > 0: + print("\nError Details:") + for line in error_lines: + print(line) + + if warning_count > 0: + print("\nWarning Details:") + for line in warning_lines: + print(line) + + except Exception as e: + print(f"An error occurred while reading the file: {e}") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 analyze.py ") + else: + analyze_log(sys.argv[1]) diff --git a/adk/middlewares/skill/workdir/test.log b/adk/middlewares/skill/workdir/test.log new file mode 100644 index 0000000..85f779d --- /dev/null +++ b/adk/middlewares/skill/workdir/test.log @@ -0,0 +1,6 @@ +[2024-05-20 10:00:00] INFO: Service started. +[2024-05-20 10:01:23] WARNING: High memory usage detected. +[2024-05-20 10:02:15] ERROR: Database connection failed. +[2024-05-20 10:03:00] INFO: Retry connection. +[2024-05-20 10:03:05] ERROR: Connection timed out. +[2024-05-20 10:05:00] INFO: Service stopped. diff --git a/go.mod b/go.mod index 9fb57af..ffc2764 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,8 @@ require ( github.com/alicebob/miniredis/v2 v2.35.0 github.com/bytedance/sonic v1.15.0 github.com/chromedp/chromedp v0.9.5 - github.com/cloudwego/eino v0.7.37 + github.com/cloudwego/eino v0.8.0 + github.com/cloudwego/eino-ext/adk/backend/local v0.2.1 github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.8 github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20251117090452-bd6375a0b3cf @@ -41,6 +42,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bluele/gcache v0.0.2 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect diff --git a/go.sum b/go.sum index e420c05..9a211f4 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 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= @@ -127,8 +129,10 @@ github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5P github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/cloudwego/eino v0.7.37 h1:T73Y/8X7ERW4h3jP+brB/I4+N5ATDyGLx5bs2H4ev8I= -github.com/cloudwego/eino v0.7.37/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ= +github.com/cloudwego/eino v0.8.0 h1:DLbrgEAloA+l7aR2qim7qQocQB48DjPrb8LzG3PYMHY= +github.com/cloudwego/eino v0.8.0/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU= +github.com/cloudwego/eino-ext/adk/backend/local v0.2.1 h1:sZ4f21SFzygzVXI6ppkkZom6JOibAjvS+YT2GZMqIy0= +github.com/cloudwego/eino-ext/adk/backend/local v0.2.1/go.mod h1:os5Tq5FuSoz/MLqAdZER3ip49Oef9prc0kVsKsPYO48= github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.8 h1:lqOAH/EWWRJuv9awmMxETEfcer2Gq2AVuZrrKqP+CzA= github.com/cloudwego/eino-ext/callbacks/cozeloop v0.1.8/go.mod h1:1FgtKIRv/LrUVuA7ojeViyRQhuIKaUDQOf+KjHuW+cg= github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf h1:Uwh3VT+xPrfDjM677dj1pSidCzBFoTrYlC274kEci5w=