mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-15 01:18:26 +08:00
## 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" />
231 lines
6.8 KiB
Go
231 lines
6.8 KiB
Go
//
|
|
// 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])
|
|
}
|