Merge pull request 'Expose the regulations provider to the chat model as built-in read-only tools' (#215) from feature/regulations-chat-tools into main
This commit was merged in pull request #215.
This commit is contained in:
@@ -675,6 +675,10 @@ func Run(opt Options) error {
|
||||
// EXT-11:公开受限 Host 工厂(下游恢复路径经 extension.NewScopedHost 重建句柄)。
|
||||
wireScopedHostFactory(caps)
|
||||
compiledSummary := applyCompiled(skillsRoot, bus, caps)
|
||||
// EXT-20:法规检索作为内置只读注册表工具暴露给模型(走 inprocHost 审计路径)。
|
||||
if err := registerRegulationsTools(reg, caps); err != nil {
|
||||
return fmt.Errorf("regulations tools: %w", err)
|
||||
}
|
||||
if err := requireProjectContextProvider(opt.RequireProjectContext, contextSlot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agent-system-backend/internal/tools"
|
||||
"agent-system-backend/pkg/extension"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
// EXT-20 内置对话工具:模型经工具注册表只读检索共享法规知识库。执行走
|
||||
// inprocHost 的审计路径(有效账号校验 + regulations.* 审计),调用方身份为
|
||||
// 运行属主本人与固定的内置工具调用方 ID。
|
||||
|
||||
const (
|
||||
regulationsToolCallerID = "agent-tools"
|
||||
regulationsToolTimeout = 60 * time.Second
|
||||
regulationsSearchDefaultLimit = 5
|
||||
regulationsSearchMaxLimit = 10
|
||||
|
||||
toolRegulationsSearch = "regulations_search"
|
||||
toolRegulationEvidence = "regulation_evidence"
|
||||
)
|
||||
|
||||
func registerRegulationsTools(reg *tools.Registry, caps hostcaps) error {
|
||||
if err := reg.Register(core.ToolDesc{
|
||||
Name: toolRegulationsSearch,
|
||||
Description: "检索已发布的医疗器械法规、标准(GB/YY/YY/T 等)、指导原则、通告等共享法规知识库;用户提到标准号、法规名称、条款或监管要求时优先调用本工具,再考虑网络搜索;命中后可用 regulation_evidence 取条款原文以精确引用",
|
||||
Source: core.SourceBuiltin, ReadOnly: true,
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{` +
|
||||
`"query":{"type":"string","description":"检索词"},` +
|
||||
`"docNo":{"type":"string","description":"标准号/文号,如 GB 9706.1-2020"},` +
|
||||
`"clauseNo":{"type":"string","description":"条款号,如 7.2.1"},` +
|
||||
`"kinds":{"type":"array","items":{"type":"string"},"description":"文献类别过滤"},` +
|
||||
`"currentOnly":{"type":"boolean","description":"仅现行有效版本"},` +
|
||||
`"limit":{"type":"integer","minimum":1,"maximum":10,"description":"返回条数,默认 5"}},` +
|
||||
`"required":["query"]}`),
|
||||
}, func(rc tools.ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
return regulationsSearchTool(caps, rc, args)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return reg.Register(core.ToolDesc{
|
||||
Name: toolRegulationEvidence,
|
||||
Description: "按 regulations_search 返回的 corpusRevision 与 nodeId 取条款原文区间与哈希,用于精确引用",
|
||||
Source: core.SourceBuiltin, ReadOnly: true,
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{` +
|
||||
`"corpusRevision":{"type":"string","description":"regulations_search 返回的语料修订号"},` +
|
||||
`"nodeId":{"type":"string","description":"regulations_search 命中的 nodeId"},` +
|
||||
`"start":{"type":"integer","description":"原文区间起点(可选)"},` +
|
||||
`"end":{"type":"integer","description":"原文区间终点(可选)"}},` +
|
||||
`"required":["corpusRevision","nodeId"]}`),
|
||||
}, func(rc tools.ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
return regulationEvidenceTool(caps, rc, args)
|
||||
})
|
||||
}
|
||||
|
||||
type regulationsToolHit struct {
|
||||
RegulationID string `json:"regulationId"`
|
||||
EditionID string `json:"editionId"`
|
||||
NodeID string `json:"nodeId"`
|
||||
DocNo string `json:"docNo,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
EditionLabel string `json:"editionLabel,omitempty"`
|
||||
LegalStatus string `json:"legalStatus,omitempty"`
|
||||
ClauseNo string `json:"clauseNo,omitempty"`
|
||||
HeadingPath []string `json:"headingPath,omitempty"`
|
||||
Excerpt string `json:"excerpt,omitempty"`
|
||||
Disclosure string `json:"disclosure"`
|
||||
WithheldReason string `json:"withheldReason,omitempty"`
|
||||
PageStart int `json:"pageStart,omitempty"`
|
||||
PageEnd int `json:"pageEnd,omitempty"`
|
||||
IsSummary bool `json:"isSummary,omitempty"`
|
||||
MatchKinds []string `json:"matchKinds,omitempty"`
|
||||
}
|
||||
|
||||
type regulationsToolSearchOutput struct {
|
||||
CorpusRevision string `json:"corpusRevision"`
|
||||
State string `json:"state"`
|
||||
DegradedReasons []string `json:"degradedReasons,omitempty"`
|
||||
Hits []regulationsToolHit `json:"hits"`
|
||||
}
|
||||
|
||||
type regulationsToolUnavailable struct {
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func regulationsSearchTool(caps hostcaps, rc tools.ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Query string `json:"query"`
|
||||
DocNo string `json:"docNo"`
|
||||
ClauseNo string `json:"clauseNo"`
|
||||
Kinds []string `json:"kinds"`
|
||||
CurrentOnly bool `json:"currentOnly"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("%s: bad args: %w", toolRegulationsSearch, err)
|
||||
}
|
||||
}
|
||||
a.Query = strings.TrimSpace(a.Query)
|
||||
if a.Query == "" {
|
||||
return "", fmt.Errorf("%s: query is required", toolRegulationsSearch)
|
||||
}
|
||||
limit := a.Limit
|
||||
if limit <= 0 {
|
||||
limit = regulationsSearchDefaultLimit
|
||||
} else if limit > regulationsSearchMaxLimit {
|
||||
limit = regulationsSearchMaxLimit
|
||||
}
|
||||
h, err := regulationsToolHost(caps, rc, toolRegulationsSearch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ctx, cancel := regulationsToolContext(rc)
|
||||
defer cancel()
|
||||
res, err := h.SearchRegulations(ctx, extension.RegulationsSearchRequest{
|
||||
SchemaVersion: extension.RegulationsSchemaVersion,
|
||||
Query: a.Query,
|
||||
Kinds: a.Kinds,
|
||||
CurrentOnly: a.CurrentOnly,
|
||||
DocNo: strings.TrimSpace(a.DocNo),
|
||||
ClauseNo: strings.TrimSpace(a.ClauseNo),
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
return regulationsToolError(toolRegulationsSearch, err)
|
||||
}
|
||||
out := regulationsToolSearchOutput{CorpusRevision: res.CorpusRevision, State: res.State,
|
||||
DegradedReasons: res.DegradedReasons, Hits: make([]regulationsToolHit, 0, len(res.Hits))}
|
||||
for _, hit := range res.Hits {
|
||||
out.Hits = append(out.Hits, regulationsToolHit{
|
||||
RegulationID: hit.RegulationID, EditionID: hit.EditionID, NodeID: hit.NodeID,
|
||||
DocNo: hit.DocNo, Title: hit.Title, EditionLabel: hit.EditionLabel, LegalStatus: hit.LegalStatus,
|
||||
ClauseNo: hit.ClauseNo, HeadingPath: hit.HeadingPath, Excerpt: hit.Excerpt,
|
||||
Disclosure: hit.Disclosure, WithheldReason: hit.WithheldReason,
|
||||
PageStart: hit.PageStart, PageEnd: hit.PageEnd, IsSummary: hit.IsSummary, MatchKinds: hit.MatchKinds,
|
||||
})
|
||||
}
|
||||
return regulationsToolJSON(out)
|
||||
}
|
||||
|
||||
func regulationEvidenceTool(caps hostcaps, rc tools.ToolExecCtx, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
CorpusRevision string `json:"corpusRevision"`
|
||||
NodeID string `json:"nodeId"`
|
||||
Start int `json:"start"`
|
||||
End int `json:"end"`
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("%s: bad args: %w", toolRegulationEvidence, err)
|
||||
}
|
||||
}
|
||||
a.CorpusRevision, a.NodeID = strings.TrimSpace(a.CorpusRevision), strings.TrimSpace(a.NodeID)
|
||||
if a.CorpusRevision == "" || a.NodeID == "" {
|
||||
return "", fmt.Errorf("%s: corpusRevision and nodeId are required", toolRegulationEvidence)
|
||||
}
|
||||
h, err := regulationsToolHost(caps, rc, toolRegulationEvidence)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ctx, cancel := regulationsToolContext(rc)
|
||||
defer cancel()
|
||||
ev, err := h.ResolveRegulationEvidence(ctx, extension.RegulationEvidenceRequest{
|
||||
SchemaVersion: extension.RegulationsSchemaVersion,
|
||||
CorpusRevision: a.CorpusRevision,
|
||||
NodeID: a.NodeID,
|
||||
Start: a.Start,
|
||||
End: a.End,
|
||||
})
|
||||
if err != nil {
|
||||
return regulationsToolError(toolRegulationEvidence, err)
|
||||
}
|
||||
return regulationsToolJSON(ev)
|
||||
}
|
||||
|
||||
// regulationsToolHost binds the audited host to the run owner, who is both the
|
||||
// owner and the actor of an agent tool call.
|
||||
func regulationsToolHost(caps hostcaps, rc tools.ToolExecCtx, name string) (inprocHost, error) {
|
||||
owner := rc.OwnerID()
|
||||
if owner == "" {
|
||||
return inprocHost{}, fmt.Errorf("%s: run owner is required", name)
|
||||
}
|
||||
return inprocHost{caps: caps, pluginID: regulationsToolCallerID, owner: owner, actor: owner}, nil
|
||||
}
|
||||
|
||||
func regulationsToolContext(rc tools.ToolExecCtx) (context.Context, context.CancelFunc) {
|
||||
parent := context.Background()
|
||||
if c, ok := rc.(interface{ Context() context.Context }); ok && c.Context() != nil {
|
||||
parent = c.Context()
|
||||
}
|
||||
return context.WithTimeout(parent, regulationsToolTimeout)
|
||||
}
|
||||
|
||||
func regulationsToolError(name string, err error) (string, error) {
|
||||
if errors.Is(err, extension.ErrRegulationsUnavailable) {
|
||||
return regulationsToolJSON(regulationsToolUnavailable{State: extension.RegulationsStateUnavailable,
|
||||
Reason: "shared regulations knowledge base is not available"})
|
||||
}
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
|
||||
func regulationsToolJSON(v any) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-system-backend/internal/tools"
|
||||
"agent-system-backend/pkg/extension"
|
||||
core "agent-system-core"
|
||||
)
|
||||
|
||||
type regulationsToolCtx struct{ owner string }
|
||||
|
||||
func (c regulationsToolCtx) ProjectID() string { return "" }
|
||||
func (c regulationsToolCtx) ConversationID() string { return "" }
|
||||
func (c regulationsToolCtx) Retrieve(core.RetrievalQuery) ([]core.RetrievalHit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c regulationsToolCtx) WriteArtifact(string, string, string, []byte) (core.Artifact, error) {
|
||||
return core.Artifact{}, errors.New("read-only")
|
||||
}
|
||||
func (c regulationsToolCtx) PeerConversations() ([]core.Conversation, error) { return nil, nil }
|
||||
func (c regulationsToolCtx) PeerConversationMessages(string, int) ([]core.Message, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c regulationsToolCtx) OwnerID() string { return c.owner }
|
||||
func (c regulationsToolCtx) WorkspaceRoot() string { return "" }
|
||||
func (c regulationsToolCtx) SettingBool(_ string, d bool) bool { return d }
|
||||
|
||||
type recordingRegulationsProvider struct {
|
||||
*fakeRegulationsProvider
|
||||
searches []extension.RegulationsSearchRequest
|
||||
evidences []extension.RegulationEvidenceRequest
|
||||
}
|
||||
|
||||
func (p *recordingRegulationsProvider) SearchRegulations(ctx context.Context, c extension.RegulationsCaller, req extension.RegulationsSearchRequest) (extension.RegulationsSearchResult, error) {
|
||||
p.searches = append(p.searches, req)
|
||||
return p.fakeRegulationsProvider.SearchRegulations(ctx, c, req)
|
||||
}
|
||||
|
||||
func (p *recordingRegulationsProvider) ResolveRegulationEvidence(ctx context.Context, c extension.RegulationsCaller, req extension.RegulationEvidenceRequest) (extension.RegulationEvidence, error) {
|
||||
p.evidences = append(p.evidences, req)
|
||||
return p.fakeRegulationsProvider.ResolveRegulationEvidence(ctx, c, req)
|
||||
}
|
||||
|
||||
func newRegulationsToolsRegistry(t *testing.T, h inprocHost) *tools.Registry {
|
||||
t.Helper()
|
||||
reg := tools.NewRegistry()
|
||||
if err := registerRegulationsTools(reg, h.caps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
func registerRecordingRegulations(t *testing.T, h inprocHost) *recordingRegulationsProvider {
|
||||
t.Helper()
|
||||
p := &recordingRegulationsProvider{fakeRegulationsProvider: &fakeRegulationsProvider{}}
|
||||
if err := (pluginRegistry{pluginID: "kbreg", caps: h.caps}).RegisterRegulationsProvider(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func callRegulationsTool(reg *tools.Registry, name, args string) core.ToolCallResult {
|
||||
return reg.Execute(regulationsToolCtx{owner: "alice"}, core.ToolCallRequest{CallID: "c1", Name: name, Arguments: json.RawMessage(args)})
|
||||
}
|
||||
|
||||
func TestRegulationsToolsRegisteredAsReadOnlyBuiltins(t *testing.T) {
|
||||
h, _ := newRegulationsTestHost(t)
|
||||
reg := newRegulationsToolsRegistry(t, h)
|
||||
for _, name := range []string{toolRegulationsSearch, toolRegulationEvidence} {
|
||||
desc, _, ok := reg.Get(name)
|
||||
if !ok || desc.Source != core.SourceBuiltin || !desc.ReadOnly || desc.Edits || desc.Hidden {
|
||||
t.Fatalf("%s desc = %+v ok=%v", name, desc, ok)
|
||||
}
|
||||
var schema struct {
|
||||
Type string `json:"type"`
|
||||
Required []string `json:"required"`
|
||||
Props map[string]any `json:"properties"`
|
||||
}
|
||||
if err := json.Unmarshal(desc.Parameters, &schema); err != nil || schema.Type != "object" || len(schema.Required) == 0 {
|
||||
t.Fatalf("%s schema = %s %v", name, desc.Parameters, err)
|
||||
}
|
||||
for _, req := range schema.Required {
|
||||
if _, ok := schema.Props[req]; !ok {
|
||||
t.Fatalf("%s required %q not declared", name, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegulationsSearchToolMapsHitsAndClampsLimit(t *testing.T) {
|
||||
h, st := newRegulationsTestHost(t)
|
||||
p := registerRecordingRegulations(t, h)
|
||||
p.search = extension.RegulationsSearchResult{CorpusRevision: "rev-3", State: extension.RegulationsStateDegraded,
|
||||
DegradedReasons: []string{"vector_index_unavailable"},
|
||||
Hits: []extension.RegulationHit{{RegulationID: "r1", EditionID: "e1", NodeID: "n1", DocNo: "GB 9706.1-2020",
|
||||
Title: "医用电气设备", EditionLabel: "2020", LegalStatus: "current", ClauseNo: "7.2.1",
|
||||
HeadingPath: []string{"7", "7.2"}, Excerpt: "条款片段", ExcerptStart: 3, NodeSHA256: "abc",
|
||||
Disclosure: extension.RegulationDisclosureWithheld, WithheldReason: "rights", PageStart: 12, PageEnd: 13,
|
||||
IsSummary: true, MatchKinds: []string{"docNo", "text"}, Score: 0.9}}}
|
||||
reg := newRegulationsToolsRegistry(t, h)
|
||||
res := callRegulationsTool(reg, toolRegulationsSearch,
|
||||
`{"query":" 电气安全 ","docNo":"GB 9706.1-2020","clauseNo":"7.2.1","kinds":["standard"],"currentOnly":true,"limit":50}`)
|
||||
if res.Status != "ok" {
|
||||
t.Fatalf("search result = %+v", res)
|
||||
}
|
||||
want := regulationsToolSearchOutput{CorpusRevision: "rev-3", State: extension.RegulationsStateDegraded,
|
||||
DegradedReasons: []string{"vector_index_unavailable"},
|
||||
Hits: []regulationsToolHit{{RegulationID: "r1", EditionID: "e1", NodeID: "n1", DocNo: "GB 9706.1-2020",
|
||||
Title: "医用电气设备", EditionLabel: "2020", LegalStatus: "current", ClauseNo: "7.2.1",
|
||||
HeadingPath: []string{"7", "7.2"}, Excerpt: "条款片段",
|
||||
Disclosure: extension.RegulationDisclosureWithheld, WithheldReason: "rights", PageStart: 12, PageEnd: 13,
|
||||
IsSummary: true, MatchKinds: []string{"docNo", "text"}}}}
|
||||
var got regulationsToolSearchOutput
|
||||
if err := json.Unmarshal([]byte(res.Output), &got); err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("search output = %s %v", res.Output, err)
|
||||
}
|
||||
if strings.Contains(res.Output, "score") || strings.Contains(res.Output, "nodeSha256") {
|
||||
t.Fatalf("search output leaks unlisted fields: %s", res.Output)
|
||||
}
|
||||
wantReq := extension.RegulationsSearchRequest{SchemaVersion: regV, Query: "电气安全", Kinds: []string{"standard"},
|
||||
CurrentOnly: true, DocNo: "GB 9706.1-2020", ClauseNo: "7.2.1", Limit: regulationsSearchMaxLimit}
|
||||
if len(p.searches) != 1 || !reflect.DeepEqual(p.searches[0], wantReq) {
|
||||
t.Fatalf("provider request = %+v", p.searches)
|
||||
}
|
||||
wantCaller := extension.RegulationsCaller{PluginID: regulationsToolCallerID, Actor: "alice", Owner: "alice"}
|
||||
if len(p.callers) != 1 || p.callers[0] != wantCaller {
|
||||
t.Fatalf("caller = %+v", p.callers)
|
||||
}
|
||||
callRegulationsTool(reg, toolRegulationsSearch, `{"query":"q"}`)
|
||||
callRegulationsTool(reg, toolRegulationsSearch, `{"query":"q","limit":-3}`)
|
||||
callRegulationsTool(reg, toolRegulationsSearch, `{"query":"q","limit":7}`)
|
||||
for i, want := range []int{regulationsSearchDefaultLimit, regulationsSearchDefaultLimit, 7} {
|
||||
if got := p.searches[i+1].Limit; got != want {
|
||||
t.Fatalf("limit case %d = %d, want %d", i, got, want)
|
||||
}
|
||||
}
|
||||
evs, err := st.ListAuditEvents("alice", 10)
|
||||
if err != nil || len(evs) != 4 {
|
||||
t.Fatalf("audit events = %+v %v", evs, err)
|
||||
}
|
||||
for _, ev := range evs {
|
||||
if ev.Action != auditRegulationsSearch || ev.Actor != "plugin:"+regulationsToolCallerID || strings.Contains(ev.Evidence, "电气安全") {
|
||||
t.Fatalf("search audit = %+v", ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegulationEvidenceToolMapsFields(t *testing.T) {
|
||||
h, st := newRegulationsTestHost(t)
|
||||
p := registerRecordingRegulations(t, h)
|
||||
p.evidence = extension.RegulationEvidence{CorpusRevision: "rev-3", RegulationID: "r1", EditionID: "e1", NodeID: "n1",
|
||||
ClauseNo: "7.2.1", Excerpt: "原文", Start: 4, End: 10, NodeSHA256: "abc", SourceAssetSHA256: "def",
|
||||
PageStart: 12, PageEnd: 12, Disclosure: extension.RegulationDisclosureFull, HashVerified: true}
|
||||
reg := newRegulationsToolsRegistry(t, h)
|
||||
res := callRegulationsTool(reg, toolRegulationEvidence, `{"corpusRevision":"rev-3","nodeId":"n1","start":4,"end":10}`)
|
||||
if res.Status != "ok" {
|
||||
t.Fatalf("evidence result = %+v", res)
|
||||
}
|
||||
var got extension.RegulationEvidence
|
||||
if err := json.Unmarshal([]byte(res.Output), &got); err != nil || got != p.evidence {
|
||||
t.Fatalf("evidence output = %s %v", res.Output, err)
|
||||
}
|
||||
wantReq := extension.RegulationEvidenceRequest{SchemaVersion: regV, CorpusRevision: "rev-3", NodeID: "n1", Start: 4, End: 10}
|
||||
if len(p.evidences) != 1 || p.evidences[0] != wantReq {
|
||||
t.Fatalf("provider request = %+v", p.evidences)
|
||||
}
|
||||
if evs, err := st.ListAuditEvents("alice", 10); err != nil || len(evs) != 1 || evs[0].Action != auditRegulationsEvidence {
|
||||
t.Fatalf("evidence audit = %+v %v", evs, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegulationsToolsRejectMissingArgs(t *testing.T) {
|
||||
h, _ := newRegulationsTestHost(t)
|
||||
p := registerRecordingRegulations(t, h)
|
||||
reg := newRegulationsToolsRegistry(t, h)
|
||||
for _, c := range []struct{ name, args string }{
|
||||
{toolRegulationsSearch, `{}`},
|
||||
{toolRegulationsSearch, `{"query":" "}`},
|
||||
{toolRegulationEvidence, `{"corpusRevision":"rev-3"}`},
|
||||
{toolRegulationEvidence, `{"nodeId":"n1"}`},
|
||||
{toolRegulationEvidence, `{"corpusRevision":"rev-3","nodeId":" "}`},
|
||||
} {
|
||||
if res := callRegulationsTool(reg, c.name, c.args); res.Status != "error" {
|
||||
t.Fatalf("%s %s accepted: %+v", c.name, c.args, res)
|
||||
}
|
||||
}
|
||||
if len(p.callers) != 0 {
|
||||
t.Fatal("provider reached with missing arguments")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegulationsToolsReportUnavailable(t *testing.T) {
|
||||
h, st := newRegulationsTestHost(t)
|
||||
reg := newRegulationsToolsRegistry(t, h)
|
||||
assertUnavailable := func(label string) {
|
||||
t.Helper()
|
||||
for _, c := range []struct{ name, args string }{
|
||||
{toolRegulationsSearch, `{"query":"q"}`},
|
||||
{toolRegulationEvidence, `{"corpusRevision":"rev-3","nodeId":"n1"}`},
|
||||
} {
|
||||
res := callRegulationsTool(reg, c.name, c.args)
|
||||
var out regulationsToolUnavailable
|
||||
if res.Status != "ok" || json.Unmarshal([]byte(res.Output), &out) != nil ||
|
||||
out.State != extension.RegulationsStateUnavailable || out.Reason == "" {
|
||||
t.Fatalf("%s %s = %+v", label, c.name, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
assertUnavailable("no provider")
|
||||
if evs, err := st.ListAuditEvents("alice", 10); err != nil || len(evs) != 0 {
|
||||
t.Fatalf("unavailable calls audited: %+v %v", evs, err)
|
||||
}
|
||||
p := registerRecordingRegulations(t, h)
|
||||
p.err = extension.ErrRegulationsUnavailable
|
||||
assertUnavailable("provider unavailable")
|
||||
p.err = errors.New("provider failure")
|
||||
if res := callRegulationsTool(reg, toolRegulationsSearch, `{"query":"q"}`); res.Status != "error" {
|
||||
t.Fatalf("provider failure reported as success: %+v", res)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const materialSubagentID = "agent.sub.material"
|
||||
|
||||
func fallbackSubagentSpec() prompts.SubagentSpec {
|
||||
return prompts.SubagentSpec{
|
||||
Tools: []string{"knowledge_search", "web_search", "tavily_search", "time_now", "list_conversations", "read_conversation", "fs_read_file", "fs_list_dir"},
|
||||
Tools: []string{"knowledge_search", "regulations_search", "regulation_evidence", "web_search", "tavily_search", "time_now", "list_conversations", "read_conversation", "fs_read_file", "fs_list_dir"},
|
||||
Model: "lightweight",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user