Add vendor assessment agent

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-04-22 22:36:14 +02:00
parent 25c590ffe6
commit 509d0c88b1
108 changed files with 9445 additions and 645 deletions

View File

@@ -614,6 +614,13 @@ input CreateVendorRiskAssessmentInput {
input AssessVendorInput {
id: ID!
websiteUrl: String!
procedure: String
}
type VendorSubprocessor {
name: String!
country: String!
purpose: String!
}
type CreateVendorPayload {
@@ -690,4 +697,6 @@ type CreateVendorRiskAssessmentPayload {
type AssessVendorPayload {
vendor: Vendor!
report: String!
subprocessors: [VendorSubprocessor!]!
}

View File

@@ -18,6 +18,7 @@ import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
)
type (
@@ -103,3 +104,15 @@ func NewVendor(v *coredata.Vendor) *Vendor {
return object
}
func NewVendorSubprocessors(sps []probo.Subprocessor) []*VendorSubprocessor {
result := make([]*VendorSubprocessor, len(sps))
for i, sp := range sps {
result[i] = &VendorSubprocessor{
Name: sp.Name,
Country: sp.Country,
Purpose: sp.Purpose,
}
}
return result
}

View File

@@ -541,20 +541,27 @@ func (r *mutationResolver) AssessVendor(ctx context.Context, input types.AssessV
prb := r.ProboService(ctx, input.ID.TenantID())
vendor, err := prb.Vendors.Assess(
result, err := prb.Vendors.Assess(
ctx,
probo.AssessVendorRequest{
ID: input.ID,
WebsiteURL: input.WebsiteURL,
Procedure: input.Procedure,
},
)
if err != nil {
if errors.Is(err, probo.ErrVendorAssessmentDisabled) {
return nil, gqlutils.Unavailable(ctx, probo.ErrVendorAssessmentDisabled)
}
r.logger.ErrorCtx(ctx, "cannot assess vendor", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.AssessVendorPayload{
Vendor: types.NewVendor(vendor),
Vendor: types.NewVendor(result.Vendor),
Report: result.Report,
Subprocessors: types.NewVendorSubprocessors(result.Subprocessors),
}, nil
}

View File

@@ -4763,3 +4763,23 @@ func (r *Resolver) DeleteCustomDomainTool(ctx context.Context, req *mcp.CallTool
return nil, types.DeleteCustomDomainOutput{DeletedCustomDomain: deletedDomain}, nil
}
func (r *Resolver) AssessVendorTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AssessVendorInput) (*mcp.CallToolResult, types.AssessVendorOutput, error) {
r.MustAuthorize(ctx, input.ID, probo.ActionVendorAssess)
svc := r.ProboService(ctx, input.ID)
result, err := svc.Vendors.Assess(
ctx,
probo.AssessVendorRequest{
ID: input.ID,
WebsiteURL: input.WebsiteURL,
Procedure: input.Procedure,
},
)
if err != nil {
return nil, types.AssessVendorOutput{}, fmt.Errorf("cannot assess vendor: %w", err)
}
return nil, types.NewAssessVendorOutput(result), nil
}

View File

@@ -1122,6 +1122,57 @@ components:
$ref: "#/components/schemas/GID"
description: Deleted vendor service ID
AssessVendorInput:
type: object
required:
- id
- website_url
properties:
id:
$ref: "#/components/schemas/GID"
description: Vendor ID to assess
website_url:
type: string
description: Vendor website URL to crawl and assess
procedure:
type: string
description: Optional custom assessment procedure (overrides the default)
VendorSubprocessor:
type: object
required:
- name
- country
- purpose
properties:
name:
type: string
description: Sub-processor name
country:
type: string
description: Country where the sub-processor operates
purpose:
type: string
description: Purpose of the sub-processor
AssessVendorOutput:
type: object
required:
- vendor
- report
- subprocessors
properties:
vendor:
$ref: "#/components/schemas/Vendor"
report:
type: string
description: Markdown-formatted vendor assessment report
subprocessors:
type: array
items:
$ref: "#/components/schemas/VendorSubprocessor"
description: Sub-processors discovered during the assessment
GetUserInput:
type: object
required:
@@ -9232,6 +9283,14 @@ tools:
$ref: "#/components/schemas/DeleteVendorServiceInput"
outputSchema:
$ref: "#/components/schemas/DeleteVendorServiceOutput"
- name: assessVendor
description: Run an AI-powered assessment on a vendor by crawling its website. Returns a markdown report, the discovered sub-processors, and an enriched vendor record. Long-running (up to 20 minutes).
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/AssessVendorInput"
outputSchema:
$ref: "#/components/schemas/AssessVendorOutput"
- name: listRisks
description: List all risks for the organization
hints:

View File

@@ -17,6 +17,7 @@ package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
)
func NewVendorRiskAssessment(v *coredata.VendorRiskAssessment) *VendorRiskAssessment {
@@ -205,3 +206,23 @@ func NewListVendorServicesOutput(p *page.Page[*coredata.VendorService, coredata.
VendorServices: services,
}
}
func NewVendorSubprocessors(sps []probo.Subprocessor) []*VendorSubprocessor {
result := make([]*VendorSubprocessor, len(sps))
for i, sp := range sps {
result[i] = &VendorSubprocessor{
Name: sp.Name,
Country: sp.Country,
Purpose: sp.Purpose,
}
}
return result
}
func NewAssessVendorOutput(result *probo.AssessVendorResult) AssessVendorOutput {
return AssessVendorOutput{
Vendor: NewVendor(result.Vendor),
Report: result.Report,
Subprocessors: NewVendorSubprocessors(result.Subprocessors),
}
}

View File

@@ -187,3 +187,17 @@ func Internal(ctx context.Context) *gqlerror.Error {
},
}
}
func Unavailable(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
Path: graphql.GetPath(ctx),
Extensions: map[string]any{
"code": "UNAVAILABLE",
},
}
}
func Unavailablef(ctx context.Context, format string, a ...any) *gqlerror.Error {
return Unavailable(ctx, fmt.Errorf(format, a...))
}