agent-exec transport ed457bf5f823c43a1d72b50b1f3cfeef/8e408ea19f4b
This commit is contained in:
+2
-10
@@ -290,11 +290,7 @@ func Run(opt Options) error {
|
||||
// 未配置时调用返回明确的「未配置」提示,不静默降级为空结果。
|
||||
if err := tools.RegisterLazySearchTools(reg, func() agenttools.SearchProvider {
|
||||
effNow := setSvc.Effective(store.OwnerDefault, "")
|
||||
tavilyNow := effNow.GetString("tools.tavily.apiKeys", "")
|
||||
if active, ok := st.ActiveSecret(store.OwnerDefault, "tools.tavily"); ok {
|
||||
tavilyNow = active
|
||||
}
|
||||
keys := splitKeyPool(tavilyNow)
|
||||
keys := tavilyKeyPool(st, effNow.GetString("tools.tavily.apiKeys", ""))
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -360,11 +356,7 @@ func Run(opt Options) error {
|
||||
},
|
||||
}
|
||||
cfg.Stream = runtime.GuardProjectStream(rc, "", cfg.Client.Stream)
|
||||
tavilyNow := effNow.GetString("tools.tavily.apiKeys", "")
|
||||
if active, ok := st.ActiveSecret(store.OwnerDefault, "tools.tavily"); ok {
|
||||
tavilyNow = active
|
||||
}
|
||||
if keys := splitKeyPool(tavilyNow); len(keys) > 0 {
|
||||
if keys := tavilyKeyPool(st, effNow.GetString("tools.tavily.apiKeys", "")); len(keys) > 0 {
|
||||
cfg.Search = agenttools.NewTavilyProvider(effNow.GetString("tools.tavily.endpoint", ""), keys)
|
||||
}
|
||||
if effNow.GetBool("runtime.exec", true) {
|
||||
|
||||
@@ -25,9 +25,60 @@ const (
|
||||
|
||||
toolRegulationsSearch = "regulations_search"
|
||||
toolRegulationEvidence = "regulation_evidence"
|
||||
toolKnowledgeSearch = "knowledge_search"
|
||||
|
||||
knowledgeRegulationsFallbackPrefix = "项目资料无命中;以下为法规知识库命中(可用 regulation_evidence 取原文):\n"
|
||||
)
|
||||
|
||||
func registerRegulationsTools(reg *tools.Registry, caps hostcaps) error {
|
||||
if err := registerRegulationsSearchTools(reg, caps); err != nil {
|
||||
return err
|
||||
}
|
||||
return wrapKnowledgeSearchWithRegulations(reg, caps)
|
||||
}
|
||||
|
||||
// wrapKnowledgeSearchWithRegulations 让 knowledge_search 在项目资料无命中时回退
|
||||
// 法规知识库;未注册 knowledge_search 时不做任何事。
|
||||
func wrapKnowledgeSearchWithRegulations(reg *tools.Registry, caps hostcaps) error {
|
||||
desc, inner, ok := reg.Get(toolKnowledgeSearch)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return reg.Register(desc, func(rc tools.ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
out, err := inner(rc, args)
|
||||
if err != nil || out != tools.KnowledgeSearchNoHits {
|
||||
return out, err
|
||||
}
|
||||
if fallback, ok := knowledgeRegulationsFallback(caps, rc, args); ok {
|
||||
return knowledgeRegulationsFallbackPrefix + fallback, nil
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
func knowledgeRegulationsFallback(caps hostcaps, rc tools.ToolExecCtx, args json.RawMessage) (string, bool) {
|
||||
var a struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
if json.Unmarshal(args, &a) != nil {
|
||||
return "", false
|
||||
}
|
||||
q, err := json.Marshal(map[string]string{"query": a.Query})
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
res, err := regulationsSearchTool(caps, rc, q)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
var parsed regulationsToolSearchOutput
|
||||
if json.Unmarshal([]byte(res), &parsed) != nil || parsed.State == extension.RegulationsStateUnavailable || len(parsed.Hits) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return res, true
|
||||
}
|
||||
|
||||
func registerRegulationsSearchTools(reg *tools.Registry, caps hostcaps) error {
|
||||
if err := reg.Register(core.ToolDesc{
|
||||
Name: toolRegulationsSearch,
|
||||
Description: "检索已发布的医疗器械法规、标准(GB/YY/YY/T 等)、指导原则、通告等共享法规知识库;用户提到标准号、法规名称、条款或监管要求时优先调用本工具,再考虑网络搜索;命中后可用 regulation_evidence 取条款原文以精确引用",
|
||||
|
||||
@@ -224,3 +224,95 @@ func TestRegulationsToolsReportUnavailable(t *testing.T) {
|
||||
t.Fatalf("provider failure reported as success: %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
type knowledgeHitsCtx struct {
|
||||
regulationsToolCtx
|
||||
hits []core.RetrievalHit
|
||||
}
|
||||
|
||||
func (c knowledgeHitsCtx) Retrieve(core.RetrievalQuery) ([]core.RetrievalHit, error) {
|
||||
return c.hits, nil
|
||||
}
|
||||
|
||||
func newKnowledgeFallbackRegistry(t *testing.T, h inprocHost) *tools.Registry {
|
||||
t.Helper()
|
||||
reg := tools.NewRegistry()
|
||||
if err := reg.RegisterBuiltin(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, _, _ := reg.Get(toolKnowledgeSearch)
|
||||
if err := registerRegulationsTools(reg, h.caps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after, _, _ := reg.Get(toolKnowledgeSearch); !reflect.DeepEqual(after, before) {
|
||||
t.Fatalf("knowledge_search desc changed: %+v", after)
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
func callKnowledgeSearch(reg *tools.Registry, hits []core.RetrievalHit) core.ToolCallResult {
|
||||
rc := knowledgeHitsCtx{regulationsToolCtx: regulationsToolCtx{owner: "alice"}, hits: hits}
|
||||
return reg.Execute(rc, core.ToolCallRequest{CallID: "c1", Name: toolKnowledgeSearch, Arguments: json.RawMessage(`{"query":"电气安全"}`)})
|
||||
}
|
||||
|
||||
func TestKnowledgeSearchFallsBackToRegulationsHits(t *testing.T) {
|
||||
h, _ := newRegulationsTestHost(t)
|
||||
p := registerRecordingRegulations(t, h)
|
||||
p.search = extension.RegulationsSearchResult{CorpusRevision: "rev-9", State: extension.RegulationsStateOK,
|
||||
Hits: []extension.RegulationHit{{RegulationID: "r1", EditionID: "e1", NodeID: "n1", DocNo: "GB 9706.1-2020",
|
||||
Excerpt: "法规条款片段", Disclosure: extension.RegulationDisclosureFull}}}
|
||||
reg := newKnowledgeFallbackRegistry(t, h)
|
||||
res := callKnowledgeSearch(reg, nil)
|
||||
if res.Status != "ok" || !strings.HasPrefix(res.Output, knowledgeRegulationsFallbackPrefix) ||
|
||||
!strings.Contains(res.Output, "法规条款片段") || !strings.Contains(res.Output, "GB 9706.1-2020") {
|
||||
t.Fatalf("fallback result = %+v", res)
|
||||
}
|
||||
if len(p.searches) != 1 || p.searches[0].Query != "电气安全" {
|
||||
t.Fatalf("regulations request = %+v", p.searches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeSearchKeepsNoHitsWhenRegulationsEmpty(t *testing.T) {
|
||||
h, _ := newRegulationsTestHost(t)
|
||||
p := registerRecordingRegulations(t, h)
|
||||
p.search = extension.RegulationsSearchResult{CorpusRevision: "rev-9", State: extension.RegulationsStateOK}
|
||||
reg := newKnowledgeFallbackRegistry(t, h)
|
||||
if res := callKnowledgeSearch(reg, nil); res.Status != "ok" || res.Output != tools.KnowledgeSearchNoHits {
|
||||
t.Fatalf("empty regulations result = %+v", res)
|
||||
}
|
||||
if len(p.searches) != 1 {
|
||||
t.Fatalf("regulations not consulted: %+v", p.searches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeSearchKeepsNoHitsWhenRegulationsUnavailable(t *testing.T) {
|
||||
h, _ := newRegulationsTestHost(t)
|
||||
reg := newKnowledgeFallbackRegistry(t, h)
|
||||
if res := callKnowledgeSearch(reg, nil); res.Status != "ok" || res.Output != tools.KnowledgeSearchNoHits {
|
||||
t.Fatalf("no provider result = %+v", res)
|
||||
}
|
||||
p := registerRecordingRegulations(t, h)
|
||||
for _, err := range []error{extension.ErrRegulationsUnavailable, errors.New("provider failure")} {
|
||||
p.err = err
|
||||
if res := callKnowledgeSearch(reg, nil); res.Status != "ok" || res.Output != tools.KnowledgeSearchNoHits {
|
||||
t.Fatalf("provider %v result = %+v", err, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeSearchProjectHitSkipsRegulations(t *testing.T) {
|
||||
h, _ := newRegulationsTestHost(t)
|
||||
p := registerRecordingRegulations(t, h)
|
||||
p.search = extension.RegulationsSearchResult{CorpusRevision: "rev-9", State: extension.RegulationsStateOK,
|
||||
Hits: []extension.RegulationHit{{RegulationID: "r1", EditionID: "e1", NodeID: "n1", Excerpt: "法规条款片段"}}}
|
||||
reg := newKnowledgeFallbackRegistry(t, h)
|
||||
hits := []core.RetrievalHit{{Doc: core.KnowledgeDoc{Title: "项目说明书", Source: "upload", Chunk: "项目片段"}, Score: 0.8}}
|
||||
res := callKnowledgeSearch(reg, hits)
|
||||
if res.Status != "ok" || !strings.Contains(res.Output, "项目片段") ||
|
||||
strings.Contains(res.Output, knowledgeRegulationsFallbackPrefix) || strings.Contains(res.Output, "法规条款片段") {
|
||||
t.Fatalf("project hit result = %+v", res)
|
||||
}
|
||||
if len(p.searches) != 0 {
|
||||
t.Fatalf("regulations consulted on project hit: %+v", p.searches)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"agent-system-backend/internal/settings"
|
||||
"agent-system-backend/internal/store"
|
||||
)
|
||||
|
||||
const tavilySecretService = "tools.tavily"
|
||||
|
||||
type activeSecretSource interface {
|
||||
ActiveSecret(owner, service string) (string, bool)
|
||||
}
|
||||
|
||||
// tavilyKeyPool 解析 Tavily 密钥池:默认属主受管密钥 → 系统行受管密钥
|
||||
// (owner=settings.OwnerSystemRow)→ 设置明文 tools.tavily.apiKeys。
|
||||
func tavilyKeyPool(secrets activeSecretSource, plaintext string) []string {
|
||||
for _, owner := range []string{store.OwnerDefault, settings.OwnerSystemRow} {
|
||||
if active, ok := secrets.ActiveSecret(owner, tavilySecretService); ok {
|
||||
return splitKeyPool(active)
|
||||
}
|
||||
}
|
||||
return splitKeyPool(plaintext)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/internal/settings"
|
||||
"agent-system-backend/internal/store"
|
||||
)
|
||||
|
||||
func newTavilyKeysStore(t *testing.T) *store.Store {
|
||||
t.Helper()
|
||||
st, err := store.OpenSQLite(filepath.Join(t.TempDir(), "tavily.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { st.Close() })
|
||||
return st
|
||||
}
|
||||
|
||||
func TestTavilyKeyPoolFallsBackToSystemRowSecret(t *testing.T) {
|
||||
st := newTavilyKeysStore(t)
|
||||
if _, err := st.AddSecret(settings.OwnerSystemRow, tavilySecretService, "system", "fake-sys-a, fake-sys-b"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := tavilyKeyPool(st, "fake-plain"), []string{"fake-sys-a", "fake-sys-b"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("system-row key pool = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTavilyKeyPoolPrefersOwnerRowSecret(t *testing.T) {
|
||||
st := newTavilyKeysStore(t)
|
||||
if _, err := st.AddSecret(settings.OwnerSystemRow, tavilySecretService, "system", "fake-sys"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.AddSecret(store.OwnerDefault, tavilySecretService, "owner", "fake-owner"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := tavilyKeyPool(st, "fake-plain"), []string{"fake-owner"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("owner-row key pool = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTavilyKeyPoolFallsBackToPlaintextSetting(t *testing.T) {
|
||||
st := newTavilyKeysStore(t)
|
||||
if got, want := tavilyKeyPool(st, " fake-plain-a ,, fake-plain-b"), []string{"fake-plain-a", "fake-plain-b"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("plaintext key pool = %v, want %v", got, want)
|
||||
}
|
||||
if got := tavilyKeyPool(st, ""); len(got) != 0 {
|
||||
t.Fatalf("empty key pool = %v", got)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ description: 何时用工具、如何并行调用、如何处理工具结果
|
||||
---
|
||||
使用工具时遵循:
|
||||
|
||||
- 优先专用工具而非通用手段:读文件用 read_file(行号分页)、找内容用 grep、找文件用 glob、改文件用 edit_file/multi_edit(须先读过、old_string 唯一);会话工作区内的文件操作一律用这些工具,不用 run_shell 的 cat/sed 代替。检索知识用检索工具、查时间用时间工具;能用结构化工具完成的,不绕道自由文本。
|
||||
- 优先专用工具而非通用手段:读文件用 read_file(行号分页)、找内容用 grep、找文件用 glob、改文件用 edit_file/multi_edit(须先读过、old_string 唯一);会话工作区内的文件操作一律用这些工具,不用 run_shell 的 cat/sed 代替。检索项目资料用 knowledge_search;法规、标准、标准号(GB、YY、YY/T、IEC 等)、指导原则类问题优先用 regulations_search,需条款原文再用 regulation_evidence;查时间用时间工具;能用结构化工具完成的,不绕道自由文本。
|
||||
- 相互独立的调用并行发起;有依赖的调用按依赖顺序串行。
|
||||
- 调用前确认参数完整、类型正确;不确定参数含义时先查阅工具描述,不猜测字段。
|
||||
- 工具结果是事实来源:基于结果作答并如实转述;结果为空或报错时说明情况,不假装拿到了数据。
|
||||
|
||||
@@ -5,4 +5,4 @@ version: 1
|
||||
title: knowledge_search 工具描述
|
||||
description: 知识检索工具的模型侧说明文案
|
||||
---
|
||||
在项目知识与插件知识库中做全文检索,返回命中片段。用法:一次一个明确的检索意图;换角度检索时改关键词再查,不重复同一查询。命中为空说明库中没有该表述,可换同义词再试一到两次;仍为空就承认未命中,不虚构结果。回答"项目里有没有/是什么"类问题优先用本工具。
|
||||
在项目知识与插件知识库中做全文检索,返回命中片段。用法:一次一个明确的检索意图;换角度检索时改关键词再查,不重复同一查询。命中为空说明库中没有该表述,可换同义词再试一到两次;仍为空就承认未命中,不虚构结果。回答"项目里有没有/是什么"类项目资料问题优先用本工具;法规、标准、标准号(GB、YY、YY/T、IEC 等)或指导原则类问题优先用 regulations_search,再用 regulation_evidence 取条款原文。
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// KnowledgeSearchNoHits 是 knowledge_search 项目资料无命中时的输出。
|
||||
const KnowledgeSearchNoHits = "(无命中)"
|
||||
|
||||
// Executor 是无状态执行器:入参已校验,返回输出文本或错误。
|
||||
type Executor func(rc ToolExecCtx, args json.RawMessage) (string, error)
|
||||
|
||||
@@ -121,7 +124,7 @@ func (r *Registry) RegisterBuiltin() error {
|
||||
return err
|
||||
}
|
||||
if err := r.Register(core.ToolDesc{
|
||||
Name: "knowledge_search", Description: "在项目知识与插件知识库中做全文检索,返回命中片段", Source: core.SourceBuiltin, ReadOnly: true,
|
||||
Name: "knowledge_search", Description: "在项目资料(项目知识与插件知识库)中做全文检索,返回命中片段;法规、标准、标准号(GB/YY/YY/T/IEC 等)或指导原则类问题优先用 regulations_search", Source: core.SourceBuiltin, ReadOnly: true,
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{"query":{"type":"string","description":"检索词"}},"required":["query"]}`),
|
||||
}, func(rc ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
@@ -141,7 +144,7 @@ func (r *Registry) RegisterBuiltin() error {
|
||||
return "", err
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
return "(无命中)", nil
|
||||
return KnowledgeSearchNoHits, nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
for i, h := range hits {
|
||||
|
||||
Reference in New Issue
Block a user