Files
WeKnora/pluginsdk/parser.go
wizardchen b0767453e4 feat(plugin): document parsers from code plugins
Plugins can contribute parser engines (contributes.parsers, with the
fileTypes they handle). Each registers under its qualified ID
(acme.ocr/ocr) next to the builtin engines and the docreader's, kept in a
separate store so the builtin plugin catalog never describes it as a
builtin. A knowledge base picks it in its parser rules like any engine;
uploads of its file types are accepted.

The engine calls POST /v1/parsers/{id}/parse with the file as base64 and
takes back Markdown plus the images it references, which the pipeline
stores and chunks as usual. A retryable plugin error (unavailable, rate
limited) fails the call so the task retries; any other error fails the
document for good. Other engines' tenant credentials are never passed to
a plugin. A plugin engine that is gone no longer falls through to the
Python docreader; the parse fails saying the plugin is not running. The
engine list honours the workspace's plugin switches and shows plugin
engines by their localized names.

SDK: p.Parser(id, ...), openapi.yaml and a conformance check.
examples/plugins/subtitles parses SRT and WebVTT into a transcript and
counts parsed files per workspace through the Host API key-value store.
2026-09-25 23:50:08 +08:00

56 lines
1.7 KiB
Go

package pluginsdk
import (
"context"
"encoding/json"
"net/http"
"github.com/Tencent/WeKnora/pluginsdk/pluginapi"
)
// Parser turns a document into Markdown. Return a non-retryable error (such
// as pluginapi.CodeInvalidConfig or CodeNotFound) for a document that will
// never parse; retryable ones (CodeUnavailable, CodeRateLimited) make
// WeKnora try again later.
type Parser interface {
Parse(ctx context.Context, call *Call, in pluginapi.ParseInput) (*pluginapi.ParseOutput, error)
}
// ParserFunc adapts a function to Parser.
type ParserFunc func(ctx context.Context, call *Call, in pluginapi.ParseInput) (*pluginapi.ParseOutput, error)
// Parse implements Parser.
func (f ParserFunc) Parse(ctx context.Context, call *Call, in pluginapi.ParseInput) (*pluginapi.ParseOutput, error) {
return f(ctx, call, in)
}
// Parser registers parser id (contributes.parsers[].id).
func (p *Plugin) Parser(id string, parser Parser) { p.parsers[id] = parser }
func (p *Plugin) routeParsers(mux *http.ServeMux) {
mux.HandleFunc("POST /v1/parsers/{id}/parse", func(w http.ResponseWriter, r *http.Request) {
parser, ok := p.parsers[r.PathValue("id")]
if !ok {
writeError(w, pluginapi.Errorf(pluginapi.CodeNotFound, "no parser %q", r.PathValue("id")))
return
}
p.unary(func(ctx context.Context, call *Call, raw json.RawMessage) (any, error) {
in, err := decodeInput[pluginapi.ParseInput](raw)
if err != nil {
return nil, err
}
if len(in.Content) == 0 && in.URL == "" {
return nil, pluginapi.Errorf(pluginapi.CodeBadRequest, "content or url is required")
}
out, err := parser.Parse(ctx, call, in)
if err != nil {
return nil, err
}
if out == nil {
out = &pluginapi.ParseOutput{}
}
return out, nil
})(w, r)
})
}