Skip to content

Latest commit

 

History

History
212 lines (175 loc) · 11.7 KB

README.md

File metadata and controls

212 lines (175 loc) · 11.7 KB

Quivr Plugin SDK for Go

Package quivrplugin implements all five Contributions of Plugin Protocol v0. The core keeps schedules, checkpoints, credentials and health; your source plugin fetches pages. It uses YAML, JSON Schema and the OpenTelemetry API, never engine packages. Requires Go 1.26 or later. Both kits serve normalizers, alert rules, connectors, ingestion and retrieval.

Discovery serves the highest version the manifest admits, through Plugin API 0.19.0. See the feature history.

The HTTP server automatically verifies engine tokens for API 0.14.0. Provision QUIVR_PLUGIN_SIGNING_KEYS as described in the authentication contract. Older declared API ranges remain unsigned; plugin dev and plugin test supply local keys.

go get github.com/The-Vibe-Company/quivr/sdks/go

The sample examples/static-source is a complete plugin: a manifest, a connector, fixtures and a test.

Trace context

The HTTP adapter continues incoming W3C trace context in each handler’s context.Context. Configure an OpenTelemetry SDK provider and exporter in your plugin process to export internal spans. For provider requests, call quivrplugin.InjectTrace(ctx, request.Header); SDK Blob reads already do this. See the protocol’s propagation and privacy rules.

A connector

Declare each kind in quivr-plugin.yaml with the JSON Schemas of its configuration and credential:

id: example.feeds
version: 0.1.0
compatibility:
  engine: ">=0.1.0 <0.3.0"
  plugin_api: ">=0.3.0 <0.4.0"
contributions:
  connector:
    kinds:
      feed:
        config_schema: {type: object, required: [url], properties: {url: {type: string}}}
        credential_schema: {type: object, required: [token], properties: {token: {type: string}}}
        default_interval_seconds: 900
run:
  command: [go, run, .]

Implement Fetch and CheckCredential, register with plugin.Connector(kind, impl), then call plugin.Serve(). The static-source example shows configuration, credential decoding and checkpoint pagination.

An ingestion plugin

Declare contributions.ingestion with the vector spaces the plugin owns and register a type with two methods, SegmentAndEmbed (segments of the Version's text Parts, each with a vector per requested space) and EmbedQuery, through plugin.Ingestion(impl). The SDK refuses undeclared spaces and checks offsets, dimensions and max_segments before answering; a RetryableIngestError delays the Version, a TerminalIngestError quarantines it for later reprocessing. examples/hash-embedder is certified in CI, and Write an ingestion plugin covers pinning it.

For bounded continuation, declare API 0.18.0 and contributions.ingestion.paging: true. Implement PagedIngester.SegmentAndEmbedPage with contiguous coverage, honoring request.Page.Start and MaxSegments; return NextStart until complete. Legacy ingesters keep using SegmentAndEmbed without a page request.

A retrieval plugin

Declare contributions.retrieval profiles and register plugin.Retrieval(impl). The deployment must map default; local profiles may be deep-only. Each round, Search answers quivrplugin.Ask(requests...) for candidates or quivrplugin.Rank(hits...) for the final ranking; req.Served holds every candidate served so far. Before answering, the SDK refuses undeclared profiles and checks what the core checks: no requests in the last round, request and k limits, and a ranking of served candidates only. A TerminalSearchError refuses the query. examples/fusion-retriever is certified in CI, and Write a retrieval plugin covers pinning it.

What the SDK does

Concern Behavior
Routes GET /v0/discovery (identity, declared Contributions, the highest admitted Plugin API version, the sha256: digest of the exact manifest bytes), GET /v0/health, POST /v0/contributions/connector/fetch and /check_credential; /receive for push kinds and attachment routes when declared; normalizer, subscription, ingestion and retrieval routes when declared
Request checks Request schema → 400 invalid_request; undeclared kind → 400 unknown_kind; installer configuration, instance config and credential against the declared schemas → 400 invalid_configuration, invalid_config or invalid_credential. All terminal; a credential never appears in the message
Errors AccessError → 403, SourceError → 422 (terminal), TransientError(...).WithRetryAfter(d) → 503 with retry_after_seconds, all with error_class. ErrNotDue → a skipped run with the checkpoint unchanged. Any other error → 503 transient unexpected_error; a panic → 500 source internal_error. Details go to the log, never to the envelope
Response checks Before sending: the response schema, max_items, max_response_bytes, a checkpoint of at most limits.max_checkpoint_bytes (64 KiB by default, at most 1 MiB) and diagnostics of at most 16 KiB (500 invalid_response). The engine still applies its own item validation
Credentials Credential prints [redacted] in fmt, JSON and slog. Its string values are scrubbed from error messages and from req.Logger()
Deadline The request context ends at the manifest's timeout_ms

Checkpoints, items and errors

  • The checkpoint is yours. Any JSON value that resumes after the returned items: a cursor, a timestamp, a set of seen ids. The core stores it and sends it back unchanged, and moves it forward only after your items are accepted, so a crash never loses items. Keep it under 64 KiB, or declare limits.max_checkpoint_bytes (at most 1 MiB).
  • Pages. Return at most max_items items and More: true to be called again at once in the same run, with the checkpoint moved. Use req.Now, not time.Now(), and req.PageInRun and req.ReadsToday to respect source quotas. req.Connector.CorpusID and req.Connector.SourceNamespace (Plugin API 0.3.1) name where the instance writes, to bind Relation targets.
  • Items carry a stable RecordKey, a Revision when the source has one (the core skips an unchanged revision), text or a Manifest of text Parts (quivrplugin.NewManifest), extensions in namespaces your manifest declares, or Withdraw: true for an item deleted at the source.
  • Attachments (Plugin API 0.4): declare contributions.connector.attachments and implement OpenAttachment, which streams req.Attachment.Ref. The SDK reads the bytes once into a temporary file, answers their size and SHA-256 or a skip (optional SkippedAttachment updates the item's extensions), then uploads them to the core's grant, which never prints. Set SizeBytes and SHA256 when you already hold the exact bytes.
  • Errors tell the core what to show operators: AccessError for a refused credential (health access_error), TransientError for an outage or a rate limit (retried), SourceError for data you cannot use.
  • Push (Plugin API 0.5): declare modes: [pull, push] and implement Receiver. The core relays each request the source sends to the instance's public WebhookURL (in fetch requests; register it with the source). Receive verifies it with the credential over req.Request.Body(), then answers Accept(items...), Respond(200, type, body) for a challenge, or Refuse(401, reason), which changes nothing. Map a delivered item exactly as Fetch maps it, so both paths converge on one Receipt. Page.Push (PushIsActive, PushIsPending, PushHasFailed) reports your setup at the source; an active one may relax polling.

For a kind with declared secure api.routes, register a typed helper for each route:

plugin.Route("alerts", "event", func(ctx context.Context, req *quivrplugin.ReceiveRequest) (*quivrplugin.Delivery, error) {
	if req.Body == nil {
		return quivrplugin.Refuse(http.StatusBadRequest, "missing event"), nil
	}
	return quivrplugin.Accept(), nil
})

The engine authenticates the request. The SDK matches the declared method and path, validates the parsed JSON body against request_schema, and then calls the helper. Register every declared route when the connector has no Receiver; a Receiver can continue handling routes without helpers.

Test it

plugintest replays a fixture in process, through the same handler the core talks to:

f, _ := plugintest.Load("fixtures/pages.json")
if err := plugintest.Verify(plugin, f); err != nil { // pages, errors, credential check
	t.Fatal(err)
}

A fixture (schema) names the kind, config, credential and starting checkpoint, and what to expect: the Record Keys of each page, an error class, the credential check, and for a push kind the receive cases with their expected verdicts. Use test credentials of 8 characters or more, so the leak check can see them.

Then certify the plugin with the Contract Runner:

quivr plugin inspect .
quivr plugin test --startup-timeout 120s .   # go run compiles on the first start

It runs every fixture page by page, feeding each checkpoint back, resumes from the final checkpoint, checks credentials, error classes and invalid requests, and fails when a credential value appears in an answer or in your plugin's output. CI certifies the sample and publishes its report as the go-connector-contract-report artifact. To run it in Quivr, pin it as in Run a connector plugin.

Normalizers and alert rules

Register plugin.Normalizer(impl) with Normalize(ctx, *NormalizerRequest) returning *NormalizerResponse, or plugin.Subscription(impl) with Evaluate(ctx, *SubscriptionRequest) returning *SubscriptionResponse. request.ReadInput(ctx) verifies file or signed URL bytes against size and SHA-256. Use Match(id, explanation, partKeys...), NoMatch(id) and NotReady(id) for decisions. The adapter checks declared expression/configuration schemas, batch size, decision coverage, evidence and response bounds. Both handlers return TerminalError or RetryableError to classify failures.

Scaffold and version ownership

quivr plugin init generates Python normalizer, subscription and connector starters, including a push source with --kind connector --push. Go starters and Python ingestion/retrieval starters are omitted: these need a chosen vector model or search strategy, or Go module setup. Start from the SDK examples or register the operations above; both kits can serve every Contribution even when no starter exists.

The author of a Plugin API change owns both kits in the same PR: update the feature table, regenerate models/schema copies/version constants, adapt both servers, bump the Python package version and prepare the Go module release tag, and run SDK tests plus Contract Runner certification. Additive SDK features bump the minor package version; compatible fixes bump patch; breaking SDK interfaces bump major. Plugin API versions remain independent of kit releases.

Rules

  • Import only this SDK and public packages: make plugin-boundary fails when code under plugins/ or sdks/go/ imports Quivr's internal/ packages or requires the engine module.
  • The schema copies under quivrplugin/schemas come from contracts/; run python3 sdks/go/scripts/sync_schemas.py after a contract change (make test fails when they are stale).