Refactor extractor to extraction provider and update related configurations

- Renamed extractor provider to extraction provider across the codebase for consistency and clarity.
- Updated references in configuration files, provider factories, and asset management to reflect the new terminology.
- Removed the extractor provider implementation and associated test files, streamlining the provider structure.
- Adjusted test cases and documentation to align with the new extraction provider framework.
This commit is contained in:
Max 2025-08-13 16:32:30 +08:00
parent 82788e82a9
commit 6acea5b004
18 changed files with 272 additions and 269 deletions

File diff suppressed because one or more lines are too long

View file

@ -128,7 +128,7 @@ func TestProviderConfigGetProviders(t *testing.T) {
}
// Test getting providers for different types and languages
testCases := []string{"chunking", "embedding", "converter", "extractor", "fetcher"}
testCases := []string{"chunking", "embedding", "converter", "extraction", "fetcher"}
for _, providerType := range testCases {
// Test with "en"

View file

@ -8,7 +8,7 @@ This directory contains all the providers for the Knowledge Base (KB) system. Pr
- [Provider Types](#provider-types)
- [Chunking Providers](#chunking-providers)
- [Embedding Providers](#embedding-providers)
- [Extractor Providers](#extractor-providers)
- [Extraction Providers](#extraction-providers)
- [Fetcher Providers](#fetcher-providers)
- [Converter Providers](#converter-providers)
- [Configuration Format](#configuration-format)
@ -152,9 +152,9 @@ Uses local FastEmbed models for embedding generation without API calls.
}
```
### Extractor Providers
### Extraction Providers
#### OpenAI Extractor (`__yao.openai`)
#### OpenAI Extraction (`__yao.openai`)
Extracts entities and relationships from documents using OpenAI models for knowledge graph construction.
@ -535,7 +535,7 @@ All providers provide sensible default values for optional fields. Required fiel
"concurrent": 15
}
},
"extractor": {
"extraction": {
"id": "__yao.openai",
"properties": {
"connector": "openai.gpt-4o-mini",

View file

@ -9,16 +9,16 @@ import (
kbtypes "github.com/yaoapp/yao/kb/types"
)
// ExtractorOpenAI is an OpenAI extractor provider
type ExtractorOpenAI struct{}
// ExtractionOpenAI is an OpenAI extraction provider
type ExtractionOpenAI struct{}
// AutoRegister registers the extractor providers
// AutoRegister registers the extraction providers
func init() {
factory.Extractors["__yao.openai"] = &ExtractorOpenAI{}
factory.Extractions["__yao.openai"] = &ExtractionOpenAI{}
}
// Make creates a new OpenAI extractor
func (e *ExtractorOpenAI) Make(option *kbtypes.ProviderOption) (types.Extraction, error) {
// Make creates a new OpenAI extraction
func (e *ExtractionOpenAI) Make(option *kbtypes.ProviderOption) (types.Extraction, error) {
// Start with default values
options := openai.Options{
ConnectorName: "", // Will be set from option
@ -127,7 +127,7 @@ func (e *ExtractorOpenAI) Make(option *kbtypes.ProviderOption) (types.Extraction
return openai.NewOpenai(options)
}
// Schema returns the schema for the OpenAI extractor provider
func (e *ExtractorOpenAI) Schema(provider *kbtypes.Provider, locale string) (*kbtypes.ProviderSchema, error) {
return factory.GetSchemaFromBindata(factory.ProviderTypeExtractor, "openai", locale)
// Schema returns the schema for the OpenAI extraction provider
func (e *ExtractionOpenAI) Schema(provider *kbtypes.Provider, locale string) (*kbtypes.ProviderSchema, error) {
return factory.GetSchemaFromBindata(factory.ProviderTypeExtraction, "openai", locale)
}

View file

@ -6,14 +6,14 @@ import (
kbtypes "github.com/yaoapp/yao/kb/types"
)
func TestExtractorOpenAI_Make(t *testing.T) {
extractor := &ExtractorOpenAI{}
func TestExtractionOpenAI_Make(t *testing.T) {
extraction := &ExtractionOpenAI{}
// Note: OpenAI extractor requires connectors to be loaded
// Note: OpenAI extraction requires connectors to be loaded
// All tests will fail in test environment due to missing connectors
t.Run("nil option should return error due to missing connector", func(t *testing.T) {
_, err := extractor.Make(nil)
_, err := extraction.Make(nil)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -22,7 +22,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
t.Run("empty option should return error due to missing connector", func(t *testing.T) {
option := &kbtypes.ProviderOption{}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -35,7 +35,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"connector": "openai.gpt-4o-mini",
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -49,7 +49,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"toolcall": true,
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -63,7 +63,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"toolcall": false,
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -84,7 +84,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"retry_delay": 2,
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -100,7 +100,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"concurrent": 3.0, // float64 -> int
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -115,7 +115,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"retry_attempts": 2.0, // float64 -> int
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -137,7 +137,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
},
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -159,7 +159,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"tools": "invalid", // invalid type
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -175,7 +175,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
// Other properties should use defaults
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -191,7 +191,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
// No connector specified
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -205,7 +205,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"toolcall": true,
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -219,7 +219,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"toolcall": false,
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -237,7 +237,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
},
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -251,7 +251,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"temperature": 2.5, // Above normal range, will be validated by openai.NewOpenai
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -269,7 +269,7 @@ func TestExtractorOpenAI_Make(t *testing.T) {
"retry_delay": 0, // Will be set to default by openai.NewOpenai
},
}
_, err := extractor.Make(option)
_, err := extraction.Make(option)
if err == nil {
t.Error("Expected error due to missing connector")
}
@ -277,9 +277,9 @@ func TestExtractorOpenAI_Make(t *testing.T) {
})
}
func TestExtractorOpenAI_Schema(t *testing.T) {
extractor := &ExtractorOpenAI{}
schema, err := extractor.Schema(nil, "en")
func TestExtractionOpenAI_Schema(t *testing.T) {
extraction := &ExtractionOpenAI{}
schema, err := extraction.Schema(nil, "en")
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

View file

@ -17,8 +17,8 @@ const (
ProviderTypeConverter ProviderType = "converter"
// ProviderTypeEmbedding is a type for embedding providers
ProviderTypeEmbedding ProviderType = "embedding"
// ProviderTypeExtractor is a type for extractor providers
ProviderTypeExtractor ProviderType = "extractor"
// ProviderTypeExtraction is a type for extraction providers
ProviderTypeExtraction ProviderType = "extraction"
// ProviderTypeFetcher is a type for fetcher providers
ProviderTypeFetcher ProviderType = "fetcher"
)
@ -38,8 +38,8 @@ var Converters = map[string]Converter{}
// Embeddings is a map of embedding providers
var Embeddings = map[string]Embedding{}
// Extractors is a map of extractor providers
var Extractors = map[string]Extractor{}
// Extractions is a map of extraction providers
var Extractions = map[string]Extraction{}
// Fetchers is a map of fetcher providers
var Fetchers = map[string]Fetcher{}
@ -104,15 +104,15 @@ func MakeEmbedding(id string, option *kbtypes.ProviderOption) (types.Embedding,
return embedding.Make(option)
}
// === Extractor API ===
// === Extraction API ===
// MakeExtractor creates a new extractor provider
func MakeExtractor(id string, option *kbtypes.ProviderOption) (types.Extraction, error) {
extractor, ok := Extractors[id]
// MakeExtraction creates a new extraction provider
func MakeExtraction(id string, option *kbtypes.ProviderOption) (types.Extraction, error) {
extraction, ok := Extractions[id]
if !ok {
return nil, fmt.Errorf("extractor provider %s not found", id)
return nil, fmt.Errorf("extraction provider %s not found", id)
}
return extractor.Make(option)
return extraction.Make(option)
}
// === Fetcher API ===
@ -139,8 +139,8 @@ func GetSchema(typ ProviderType, provider *kbtypes.Provider, locale string) (*kb
schema, exists = Converters[provider.ID]
case ProviderTypeEmbedding:
schema, exists = Embeddings[provider.ID]
case ProviderTypeExtractor:
schema, exists = Extractors[provider.ID]
case ProviderTypeExtraction:
schema, exists = Extractions[provider.ID]
case ProviderTypeFetcher:
schema, exists = Fetchers[provider.ID]
}

View file

@ -25,8 +25,8 @@ type Embedding interface {
Schema
}
// Extractor is a factory for extractor providers
type Extractor interface {
// Extraction is a factory for extraction providers
type Extraction interface {
Make(option *kbtypes.ProviderOption) (types.Extraction, error)
Schema
}

View file

@ -278,7 +278,7 @@ func (c *Config) resolveProviderEnvVars() error {
// Resolve env vars for all provider types and languages
providerMaps := []map[string][]*Provider{
c.Providers.Chunkings, c.Providers.Embeddings, c.Providers.Converters, c.Providers.Extractors,
c.Providers.Chunkings, c.Providers.Embeddings, c.Providers.Converters, c.Providers.Extractions,
c.Providers.Fetchers, c.Providers.Searchers, c.Providers.Rerankers, c.Providers.Votes,
c.Providers.Weights, c.Providers.Scores,
}
@ -372,7 +372,7 @@ func (c *Config) ComputeFeatures() Features {
// Advanced features
if c.Providers != nil {
features.EntityExtraction = c.hasProvidersInAnyLanguage(c.Providers.Extractors)
features.EntityExtraction = c.hasProvidersInAnyLanguage(c.Providers.Extractions)
features.WebFetching = c.hasProvidersInAnyLanguage(c.Providers.Fetchers)
features.CustomSearch = c.hasProvidersInAnyLanguage(c.Providers.Searchers)
features.ResultReranking = c.hasProvidersInAnyLanguage(c.Providers.Rerankers)

View file

@ -236,7 +236,7 @@ func TestConfig_ComputeFeatures(t *testing.T) {
{ID: "__yao.vision"},
},
},
Extractors: map[string][]*Provider{
Extractions: map[string][]*Provider{
"en": {{ID: "test"}},
},
Fetchers: map[string][]*Provider{

View file

@ -52,16 +52,16 @@ func (p *ProviderOption) Parse(v interface{}) error {
// LoadProviders loads providers from directories with language support
func LoadProviders(basePath string) (*ProviderConfig, error) {
config := &ProviderConfig{
Chunkings: make(map[string][]*Provider),
Embeddings: make(map[string][]*Provider),
Converters: make(map[string][]*Provider),
Extractors: make(map[string][]*Provider),
Fetchers: make(map[string][]*Provider),
Searchers: make(map[string][]*Provider),
Rerankers: make(map[string][]*Provider),
Votes: make(map[string][]*Provider),
Weights: make(map[string][]*Provider),
Scores: make(map[string][]*Provider),
Chunkings: make(map[string][]*Provider),
Embeddings: make(map[string][]*Provider),
Converters: make(map[string][]*Provider),
Extractions: make(map[string][]*Provider),
Fetchers: make(map[string][]*Provider),
Searchers: make(map[string][]*Provider),
Rerankers: make(map[string][]*Provider),
Votes: make(map[string][]*Provider),
Weights: make(map[string][]*Provider),
Scores: make(map[string][]*Provider),
}
// Provider type directories to load
@ -125,7 +125,7 @@ func loadProviderType(basePath, providerType string, config *ProviderConfig) err
case "converters":
config.Converters[language] = providers
case "extractions":
config.Extractors[language] = providers
config.Extractions[language] = providers
case "fetchers":
config.Fetchers[language] = providers
case "searchers":
@ -184,8 +184,8 @@ func (pc *ProviderConfig) GetProviders(providerType, language string) []*Provide
providerMap = pc.Embeddings
case "converter":
providerMap = pc.Converters
case "extractor":
providerMap = pc.Extractors
case "extraction":
providerMap = pc.Extractions
case "fetcher":
providerMap = pc.Fetchers
case "searcher":

View file

@ -91,16 +91,16 @@ type Config struct {
// ProviderConfig holds providers organized by language
type ProviderConfig struct {
// Provider configurations by language (e.g., "en", "zh-cn")
Chunkings map[string][]*Provider `json:"-"` // Text splitting providers by language
Embeddings map[string][]*Provider `json:"-"` // Text vectorization providers by language
Converters map[string][]*Provider `json:"-"` // File processing converters by language
Extractors map[string][]*Provider `json:"-"` // Entity and relationship extractors by language
Fetchers map[string][]*Provider `json:"-"` // File fetchers by language
Searchers map[string][]*Provider `json:"-"` // Search providers by language
Rerankers map[string][]*Provider `json:"-"` // Reranking providers by language
Votes map[string][]*Provider `json:"-"` // Voting providers by language
Weights map[string][]*Provider `json:"-"` // Weighting providers by language
Scores map[string][]*Provider `json:"-"` // Scoring providers by language
Chunkings map[string][]*Provider `json:"-"` // Text splitting providers by language
Embeddings map[string][]*Provider `json:"-"` // Text vectorization providers by language
Converters map[string][]*Provider `json:"-"` // File processing converters by language
Extractions map[string][]*Provider `json:"-"` // Entity and relationship extractions by language
Fetchers map[string][]*Provider `json:"-"` // File fetchers by language
Searchers map[string][]*Provider `json:"-"` // Search providers by language
Rerankers map[string][]*Provider `json:"-"` // Reranking providers by language
Votes map[string][]*Provider `json:"-"` // Voting providers by language
Weights map[string][]*Provider `json:"-"` // Weighting providers by language
Scores map[string][]*Provider `json:"-"` // Scoring providers by language
}
// VectorConfig represents vector database configuration
@ -134,17 +134,17 @@ type FFmpegConfig struct {
// LimitsConfig represents concurrency limits configuration
type LimitsConfig struct {
Job *QueueLimit `json:"job,omitempty" yaml:"job,omitempty"` // Job queue limits
Chunking *QueueLimit `json:"chunking,omitempty" yaml:"chunking,omitempty"` // Chunking limits
Embedding *QueueLimit `json:"embedding,omitempty" yaml:"embedding,omitempty"` // Embedding limits
Converter *QueueLimit `json:"converter,omitempty" yaml:"converter,omitempty"` // Converter limits
Extractor *QueueLimit `json:"extractor,omitempty" yaml:"extractor,omitempty"` // Extractor limits
Fetcher *QueueLimit `json:"fetcher,omitempty" yaml:"fetcher,omitempty"` // Fetcher limits
Searcher *QueueLimit `json:"searcher,omitempty" yaml:"searcher,omitempty"` // Searcher limits
Reranker *QueueLimit `json:"reranker,omitempty" yaml:"reranker,omitempty"` // Reranker limits
Vote *QueueLimit `json:"vote,omitempty" yaml:"vote,omitempty"` // Vote limits
Weight *QueueLimit `json:"weight,omitempty" yaml:"weight,omitempty"` // Weight limits
Score *QueueLimit `json:"score,omitempty" yaml:"score,omitempty"` // Score limits
Job *QueueLimit `json:"job,omitempty" yaml:"job,omitempty"` // Job queue limits
Chunking *QueueLimit `json:"chunking,omitempty" yaml:"chunking,omitempty"` // Chunking limits
Embedding *QueueLimit `json:"embedding,omitempty" yaml:"embedding,omitempty"` // Embedding limits
Converter *QueueLimit `json:"converter,omitempty" yaml:"converter,omitempty"` // Converter limits
Extraction *QueueLimit `json:"extraction,omitempty" yaml:"extraction,omitempty"` // Extraction limits
Fetcher *QueueLimit `json:"fetcher,omitempty" yaml:"fetcher,omitempty"` // Fetcher limits
Searcher *QueueLimit `json:"searcher,omitempty" yaml:"searcher,omitempty"` // Searcher limits
Reranker *QueueLimit `json:"reranker,omitempty" yaml:"reranker,omitempty"` // Reranker limits
Vote *QueueLimit `json:"vote,omitempty" yaml:"vote,omitempty"` // Vote limits
Weight *QueueLimit `json:"weight,omitempty" yaml:"weight,omitempty"` // Weight limits
Score *QueueLimit `json:"score,omitempty" yaml:"score,omitempty"` // Score limits
}
// QueueLimit represents queue and concurrency limits
@ -153,7 +153,7 @@ type QueueLimit struct {
QueueSize int `json:"queue_size,omitempty" yaml:"queue_size,omitempty"` // Queue size (0 means unlimited)
}
// Provider represents a service provider configuration (chunking, embedding, converter, extractor, fetcher, searcher, etc.)
// Provider represents a service provider configuration (chunking, embedding, converter, extraction, fetcher, searcher, etc.)
type Provider struct {
ID string `json:"id" yaml:"id"` // Required, unique id for the provider
Label string `json:"label" yaml:"label"` // Required, label for the provider, for display

View file

@ -61,7 +61,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) {
group.POST("/collections/:collectionID/backup", Backup)
group.POST("/collections/:collectionID/restore", Restore)
// Provider Management (Chunking, Converter, Embedding, Extractor, Fetcher ...)
// Provider Management (Chunking, Converter, Embedding, Extraction, Fetcher ...)
group.GET("/providers/:providerType", GetProviders)
group.GET("/providers/:providerType/:providerID/schema", GetProviderSchema)
}

View file

@ -261,12 +261,12 @@ func (r *BaseUpsertRequest) ToUpsertOptions(fileInfo ...string) (*types.UpsertOp
// Optional providers
if r.Extraction != nil {
extractionOption, err := resolveProviderOption(r.Extraction, "extractor", locale)
extractionOption, err := resolveProviderOption(r.Extraction, "extraction", locale)
if err != nil {
return nil, fmt.Errorf("failed to resolve extraction provider: %w", err)
}
extraction, err := factory.MakeExtractor(r.Extraction.ProviderID, extractionOption)
extraction, err := factory.MakeExtraction(r.Extraction.ProviderID, extractionOption)
if err != nil {
return nil, fmt.Errorf("failed to create extraction provider: %w", err)
}
@ -623,9 +623,9 @@ func addBaseRequestFields(data map[string]interface{}, req *BaseUpsertRequest) {
}
}
if req.Extraction != nil {
data["extractor_provider_id"] = req.Extraction.ProviderID
data["extraction_provider_id"] = req.Extraction.ProviderID
if req.Extraction.Option != nil {
data["extractor_properties"] = req.Extraction.Option.Properties
data["extraction_properties"] = req.Extraction.Option.Properties
}
}
}

View file

@ -41,6 +41,7 @@ func TestCreateCollection(t *testing.T) {
createData := map[string]interface{}{
"id": testCollectionID,
"metadata": map[string]interface{}{
"name": "Test Collection " + testCollectionID, // Required: collection display name
"category": "test",
"created_by": "test_user",
},
@ -166,6 +167,7 @@ func TestRemoveCollection(t *testing.T) {
createData := map[string]interface{}{
"id": testCollectionID,
"metadata": map[string]interface{}{
"name": "Test Remove Collection " + testCollectionID, // Required: collection display name
"category": "test_remove",
},
"config": map[string]interface{}{
@ -497,6 +499,7 @@ func TestCollectionIntegration(t *testing.T) {
createData := map[string]interface{}{
"id": testCollectionID,
"metadata": map[string]interface{}{
"name": "Integration Test Collection " + testCollectionID, // Required: collection display name
"category": "integration_test",
"purpose": "full_lifecycle_test",
},

View file

@ -642,14 +642,14 @@ func processXgen(process *process.Process) interface{} {
return ids
}
var chunkings, embeddings, converters, extractors, fetchers []string
var chunkings, embeddings, converters, extractions, fetchers []string
var searchers, rerankers, votes, weights, scores []string
if knowledgebase.Providers != nil {
chunkings = extractProviderIDs(knowledgebase.Providers.Chunkings)
embeddings = extractProviderIDs(knowledgebase.Providers.Embeddings)
converters = extractProviderIDs(knowledgebase.Providers.Converters)
extractors = extractProviderIDs(knowledgebase.Providers.Extractors)
extractions = extractProviderIDs(knowledgebase.Providers.Extractions)
fetchers = extractProviderIDs(knowledgebase.Providers.Fetchers)
searchers = extractProviderIDs(knowledgebase.Providers.Searchers)
rerankers = extractProviderIDs(knowledgebase.Providers.Rerankers)
@ -659,18 +659,18 @@ func processXgen(process *process.Process) interface{} {
}
kbConfig = map[string]interface{}{
"features": knowledgebase.Config.Features,
"chunkings": chunkings,
"embeddings": embeddings,
"converters": converters,
"extractors": extractors,
"fetchers": fetchers,
"searchers": searchers,
"rerankers": rerankers,
"votes": votes,
"weights": weights,
"scores": scores,
"uploader": knowledgebase.Config.Uploader, // Default: "__yao.attachment"
"features": knowledgebase.Config.Features,
"chunkings": chunkings,
"embeddings": embeddings,
"converters": converters,
"extractions": extractions,
"fetchers": fetchers,
"searchers": searchers,
"rerankers": rerankers,
"votes": votes,
"weights": weights,
"scores": scores,
"uploader": knowledgebase.Config.Uploader, // Default: "__yao.attachment"
}
}
}

View file

@ -1,6 +1,6 @@
{
"id": "__yao.openai",
"title": "OpenAI Entity Extractor",
"title": "OpenAI Entity Extraction",
"description": "AI-powered entity and relationship extraction using OpenAI's language models. Extracts structured information from unstructured text with configurable prompts and tool calling capabilities.",
"required": ["connector"],
"properties": {

View file

@ -241,18 +241,18 @@
"nullable": true
},
{
"name": "extractor_provider_id",
"name": "extraction_provider_id",
"type": "string",
"label": "Extractor Provider ID",
"comment": "Knowledge extractor provider ID (optional)",
"label": "Extraction Provider ID",
"comment": "Knowledge extraction provider ID (optional)",
"length": 128,
"nullable": true
},
{
"name": "extractor_properties",
"name": "extraction_properties",
"type": "json",
"label": "Extractor Properties",
"comment": "Extractor provider configuration properties",
"label": "Extraction Properties",
"comment": "Extraction provider configuration properties",
"nullable": true
},
{