mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-30 04:29:24 +08:00
fix(go-agent): align GitHub and Invoke Canvas components (#16849)
## Summary - Add the GitHub Canvas component with tool registration and reference propagation. - Align the Invoke component with the Python contract for node config, input form, response output, and timing fields. - GitHub search and HTTP Invoke now work correctly in the Go Canvas runtime. ## Tests - `bash build.sh --test ./internal/agent/tool/...` - `bash build.sh --test ./internal/agent/component/...` Note: the untracked go_ragflow_cli file is not part of the PR changes. <img width="1813" height="1102" alt="image" src="https://github.com/user-attachments/assets/f69cef32-59a0-4287-a06b-6843d85198cf" /> <img width="1813" height="1102" alt="image" src="https://github.com/user-attachments/assets/b37dfc31-bc9b-4937-a38e-d2184bb157fe" />
This commit is contained in:
230
internal/agent/component/github.go
Normal file
230
internal/agent/component/github.go
Normal file
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 component
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
einotool "github.com/cloudwego/eino/components/tool"
|
||||
|
||||
"ragflow/internal/agent/runtime"
|
||||
agenttool "ragflow/internal/agent/tool"
|
||||
"ragflow/internal/tokenizer"
|
||||
)
|
||||
|
||||
const githubPromptMaxTokens = 200000
|
||||
|
||||
type githubInvoker interface {
|
||||
InvokableRun(ctx context.Context, argsJSON string, opts ...einotool.Option) (string, error)
|
||||
}
|
||||
|
||||
// githubComponent is the Canvas-facing GitHub repository search component.
|
||||
// It mirrors agent/tools/github.py: query is a runtime input, while top_n is
|
||||
// validated once from node parameters and defaults to ten.
|
||||
type githubComponent struct {
|
||||
inner githubInvoker
|
||||
}
|
||||
|
||||
func newGitHubComponent(params map[string]any) (Component, error) {
|
||||
toolParams := make(map[string]any, 1)
|
||||
for _, key := range []string{"top_n"} {
|
||||
if value, ok := params[key]; ok {
|
||||
toolParams[key] = value
|
||||
}
|
||||
}
|
||||
inner, err := agenttool.BuildByName("github", toolParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invoker, ok := inner.(githubInvoker)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GitHub: tool does not implement InvokableRun")
|
||||
}
|
||||
return newGitHubComponentWithInvoker(invoker), nil
|
||||
}
|
||||
|
||||
func newGitHubComponentWithInvoker(inner githubInvoker) Component {
|
||||
return &githubComponent{inner: inner}
|
||||
}
|
||||
|
||||
func (c *githubComponent) Name() string { return "GitHub" }
|
||||
|
||||
func (c *githubComponent) Inputs() map[string]string {
|
||||
return map[string]string{
|
||||
"query": "The search keywords to execute with GitHub. Use the most important terms and synonyms from the original request.",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *githubComponent) GetInputForm() map[string]any {
|
||||
return map[string]any{
|
||||
"query": map[string]any{
|
||||
"type": "line",
|
||||
"name": "Query",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *githubComponent) Outputs() map[string]string {
|
||||
return map[string]string{
|
||||
"formalized_content": "GitHub repositories formatted for downstream prompts.",
|
||||
"json": "Raw GitHub repository items.",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *githubComponent) Invoke(ctx context.Context, inputs map[string]any) (map[string]any, error) {
|
||||
query := stringParam(inputs["query"])
|
||||
if query == "" {
|
||||
return map[string]any{"formalized_content": "", "json": []any{}}, nil
|
||||
}
|
||||
argsJSON, err := json.Marshal(map[string]any{"query": query})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("canvas: GitHub: encode query: %w", err)
|
||||
}
|
||||
out, err := c.inner.InvokableRun(ctx, string(argsJSON))
|
||||
decoded := parseToolEnvelope(out)
|
||||
items := anySlice(decoded["results"])
|
||||
if existing, _ := decoded["_ERROR"].(string); strings.TrimSpace(existing) != "" {
|
||||
return map[string]any{"formalized_content": "", "json": items, "_ERROR": existing}, nil
|
||||
}
|
||||
if err != nil {
|
||||
if len(decoded) > 0 {
|
||||
return map[string]any{"formalized_content": "", "json": items, "_ERROR": decoded["_ERROR"]}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("canvas: GitHub: %w", err)
|
||||
}
|
||||
chunks, docAggs := buildGitHubReferences(items)
|
||||
if state, _, stateErr := runtime.GetStateFromContext[*runtime.CanvasState](ctx); stateErr == nil && state != nil {
|
||||
state.SetRetrievalReferences(chunks, docAggs)
|
||||
}
|
||||
return map[string]any{
|
||||
"formalized_content": renderGitHubReferences(chunks),
|
||||
"json": items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *githubComponent) Stream(_ context.Context, _ map[string]any) (<-chan map[string]any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func buildGitHubReferences(items []any) ([]map[string]any, []map[string]any) {
|
||||
chunks := make([]map[string]any, 0, len(items))
|
||||
docAggs := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
repository, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
content := truncateRunes(githubValueString(repository["description"])+"\n stars:"+githubValueString(repository["watchers"]), 10000)
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
documentID := strconv.FormatInt(githubHashInt(content, 100000000), 10)
|
||||
title := githubValueString(repository["name"])
|
||||
url := githubValueString(repository["html_url"])
|
||||
displayID := strconv.FormatInt(githubHashInt(documentID, 500), 10)
|
||||
chunks = append(chunks, map[string]any{
|
||||
"id": displayID,
|
||||
"chunk_id": documentID,
|
||||
"content": content,
|
||||
"doc_id": documentID,
|
||||
"document_id": documentID,
|
||||
"docnm_kwd": title,
|
||||
"document_name": title,
|
||||
"similarity": 1,
|
||||
"score": 1,
|
||||
"url": url,
|
||||
})
|
||||
docAggs = append(docAggs, map[string]any{
|
||||
"doc_name": title,
|
||||
"doc_id": documentID,
|
||||
"count": 1,
|
||||
"url": url,
|
||||
})
|
||||
}
|
||||
return chunks, docAggs
|
||||
}
|
||||
|
||||
// renderGitHubReferences mirrors the Python
|
||||
// "\n".join(kb_prompt({"chunks": chunks, "doc_aggs": aggs}, 200000, True))
|
||||
// layout for the GitHub chunks built above.
|
||||
func renderGitHubReferences(chunks []map[string]any) string {
|
||||
chunks = limitGitHubReferences(chunks, githubPromptMaxTokens)
|
||||
blocks := make([]string, 0, len(chunks))
|
||||
for _, chunk := range chunks {
|
||||
blocks = append(blocks, strings.Join([]string{
|
||||
"\nID: " + githubValueString(chunk["id"]),
|
||||
"├── Title: " + githubValueString(chunk["docnm_kwd"]),
|
||||
"├── URL: " + githubValueString(chunk["url"]),
|
||||
"└── Content:\n" + githubValueString(chunk["content"]),
|
||||
}, "\n"))
|
||||
}
|
||||
return strings.Join(blocks, "\n")
|
||||
}
|
||||
|
||||
// limitGitHubReferences mirrors kb_prompt's 200000-token guard. References
|
||||
// are recorded before this step, as Python calls add_reference before
|
||||
// formatting the prompt.
|
||||
func limitGitHubReferences(chunks []map[string]any, maxTokens int) []map[string]any {
|
||||
if maxTokens <= 0 {
|
||||
return nil
|
||||
}
|
||||
usedTokens := 0
|
||||
for index, chunk := range chunks {
|
||||
content := githubValueString(chunk["content"])
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
usedTokens += tokenizer.NumTokensFromString(content)
|
||||
if float64(maxTokens)*0.97 < float64(usedTokens) {
|
||||
return chunks[:index+1]
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func githubValueString(value any) string {
|
||||
if value == nil {
|
||||
return "None"
|
||||
}
|
||||
if boolean, ok := value.(bool); ok {
|
||||
if boolean {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
}
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
|
||||
func githubHashInt(value string, modulus int64) int64 {
|
||||
sum := sha1.Sum([]byte(value))
|
||||
number := new(big.Int).SetBytes(sum[:])
|
||||
return new(big.Int).Mod(number, big.NewInt(modulus)).Int64()
|
||||
}
|
||||
|
||||
func truncateRunes(value string, limit int) string {
|
||||
if utf8.RuneCountInString(value) <= limit {
|
||||
return value
|
||||
}
|
||||
return string([]rune(value)[:limit])
|
||||
}
|
||||
Reference in New Issue
Block a user