Files
Agent-System-Backend/internal/app/regulations_tools.go
T
agent-exec-transport 992c36efc3
governance / governance (pull_request) Successful in 17s
ci / vet-and-test (pull_request) Successful in 1m16s
agent-exec transport 4d71b0c8fa88f2fd198634b9971b3ac6/4a10fd491b02
2026-10-19 03:39:49 +00:00

218 lines
8.4 KiB
Go

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
}