218 lines
8.4 KiB
Go
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
|
|
}
|