Add a custom RAG
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Greentic ships a built-in knowledge tier for agentic workers:
attach documents, and the worker retrieves from them automatically. Sometimes that isn’t
enough — you already run a retrieval service, or your ranking logic doesn’t fit the
attach-and-go model. This page walks through wiring your own retrieval-augmented
generation (RAG) stack into Greentic as a WASM component, backed by a real, published reference
component: greentic-example-rag at tag
v0.1.0. Its Rust source,
component.manifest.json, pack.yaml, and check-exports.sh are quoted here verbatim; the flow
below is an illustrative example authored for this guide, not a file in that repo.
Choose your path
Section titled “Choose your path”| You have | Path |
|---|---|
| An HTTP retrieval service (a search API, a vector DB behind an endpoint, an internal RAG microservice) | Path 1 — call it from a component |
| Retrieval logic in code, no separate service to call (an in-memory or embedded corpus you score yourself) | Path 2 — embed the logic in the component |
| Only documents, no code and no service | Path 3 — use Knowledge Base collections, no component needed |
The component contract
Section titled “The component contract”Every component the Greentic runner loads is a WASI Preview 2 component built against
ABI greentic:component@0.6.0, world greentic:component/component@0.6.0. Two exports
are non-negotiable — miss either and the component fails silently at runtime, not at build time:
| Export | Purpose |
|---|---|
greentic:component/node@0.6.0 | The invoke entry point the runner calls to run an operation. |
greentic:component/component-descriptor@0.6.0 | The describe entry point the runner calls to introspect operations, schemas, and capability requirements. |
describe() is the single source of truth for what the runner and the designer see. Three
fields matter more than the rest:
operation.idbecomes the tool name a flow node key or an agentic-worker tool call addresses. Get this wrong and nothing routes to your component.display_name.fallback— if you only supply an i18n key with no fallback text, the designer synthesizes a generic description instead of showing your actual intent. Always give a fallback string.input.schema— a trivial or empty schema means the model the agentic worker runs on receives{}for this tool’s parameters. It won’t know to send aquery. Write a real schema.
Path 1: call an HTTP retrieval service
Section titled “Path 1: call an HTTP retrieval service”Scaffold
Section titled “Scaffold”Start from the component generator. This is the exact command that produced
greentic-example-rag:
greentic-component new --name greentic-example-rag --org ai.greentic \ --operation retrieve --default-operation retrieve \ --http-client --secret-key RAG_API_KEY --secret-format text \ --non-interactive --no-git--template defaults to rust-wasi-p2-min and --org defaults to ai.greentic, so this is
the full generator invocation — no flags are implied that aren’t shown. --http-client turns on
the capabilities.host.http.client capability; --secret-key RAG_API_KEY --secret-format text
declares a text-format secret requirement in the manifest.
// `component-descriptor@0.6.0` is not part of the canonical WIT package// vendored inside `greentic-interfaces-guest` (that package only declares a// `component-descriptor` *record* nested in the `node` interface, not an// exported interface of that name). The runner's introspection path reads// this separate interface, so we declare + export it locally: this inline// `generate!` targets the *same* `greentic:component@0.6.0` package, and the// componentizer unions it with the guest crate's own `node` /// `component-qa` / `component-i18n` exports into one final component.#[cfg(target_arch = "wasm32")]wit_bindgen::generate!({ inline: r#" package greentic:component@0.6.0;
interface component-descriptor { // CBOR-encoded JSON read by runner-host for contract introspection. describe: func() -> list<u8>; }
world component-descriptor-only { export component-descriptor; } "#, world: "component-descriptor-only", generate_all,});and the export itself, wired to a describe() payload that mirrors the operation’s real JSON
schemas:
#[cfg(target_arch = "wasm32")]struct ComponentDescriptorExports;
#[cfg(target_arch = "wasm32")]impl exports::greentic::component::component_descriptor::Guest for ComponentDescriptorExports { fn describe() -> Vec<u8> { encode_cbor(&component_descriptor_payload_json()) }}
#[cfg(target_arch = "wasm32")]export!(ComponentDescriptorExports);
#[cfg(target_arch = "wasm32")]greentic_interfaces_guest::export_component_v060!(Component);Enable the HTTP client feature — exactly one HTTP feature
Section titled “Enable the HTTP client feature — exactly one HTTP feature”In Cargo.toml:
greentic-interfaces-guest = { version = ">=1.1.0-dev, <1.2.0-0", default-features = false, features = ["component-v0-6", "secrets", "http-client-v1-1"] }The domain logic — src/retrieve.rs
Section titled “The domain logic — src/retrieve.rs”This is the file a partner actually edits; lib.rs is ABI plumbing. Two pure functions, quoted
verbatim:
//! Retrieval against an external HTTP service.//!//! This is the file a partner edits. `lib.rs` is ABI plumbing; the domain logic lives here.
use serde_json::{Value, json};
/// Shapes the request body sent to the partner's retrieval endpoint.pub fn build_request_body(payload: &Value) -> Result<Value, String> { let query = payload .get("query") .and_then(Value::as_str) .filter(|q| !q.trim().is_empty()) .ok_or_else(|| "missing required field 'query'".to_string())?; let top_k = payload.get("top_k").and_then(Value::as_u64).unwrap_or(5); Ok(json!({ "query": query, "top_k": top_k }))}
/// Normalises the service response into the component's output contract.pub fn shape_response(body: &Value) -> Value { let chunks = body .get("chunks") .and_then(Value::as_array) .cloned() .unwrap_or_default(); let context = body .get("context") .and_then(Value::as_str) .map(str::to_owned) .unwrap_or_else(|| { chunks .iter() .filter_map(|c| c.get("text").and_then(Value::as_str)) .collect::<Vec<_>>() .join("\n\n") }); json!({ "context": context, "chunks": chunks })}build_request_body requires a non-blank query and defaults top_k to 5.
shape_response prefers an explicit context string from the service; if the service only
returns chunks, it joins their text fields into one context blob. This is the part you
rewrite for your own service’s response shape.
Calling the service, and where config and secrets come from
Section titled “Calling the service, and where config and secrets come from”/// Calls the partner's retrieval endpoint.////// `payload` is the full invocation payload the runner delivers:/// `{ "config": { ... }, "input": { ... } }`. The runner merges the flow node's/// `config:` block into the payload before `invoke` (`pack.rs:2390-2422`);/// components never read host environment variables.#[cfg(target_arch = "wasm32")]fn call_retrieval_service(payload: &serde_json::Value) -> Result<serde_json::Value, String> { use greentic_interfaces_guest::http_client_v1_1 as client; use greentic_interfaces_guest::secrets_store;
let endpoint = payload .get("config") .and_then(|c| c.get("rag_endpoint")) .and_then(serde_json::Value::as_str) .ok_or_else(|| "missing config.rag_endpoint".to_string())?;
// Secrets come from the secrets store only — never from config or the payload. let api_key_bytes = secrets_store::get(RAG_API_KEY_SECRET) .map_err(|e| format!("secrets get failed: {e:?}"))? .ok_or_else(|| format!("missing secret {RAG_API_KEY_SECRET}"))?; let api_key = String::from_utf8(api_key_bytes).map_err(|_| "secret is not utf8".to_string())?;
let body = retrieve::build_request_body(payload.get("input").unwrap_or(payload))?;
let req = client::Request { method: "POST".to_string(), url: endpoint.to_string(), headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ("Authorization".to_string(), format!("Bearer {api_key}")), ], body: serde_json::to_vec(&body).ok(), }; let opts = client::RequestOptions { timeout_ms: Some(10_000), allow_insecure: Some(false), follow_redirects: Some(true), };
let resp = client::send(&req, Some(opts), None) .map_err(|e| format!("http error: {} ({})", e.message, e.code))?; if !(200..300).contains(&resp.status) { return Err(format!("retrieval service returned status {}", resp.status)); } let body_bytes = resp.body.ok_or_else(|| "empty response body".to_string())?; serde_json::from_slice(&body_bytes).map_err(|e| format!("invalid JSON response: {e}"))}
#[cfg(target_arch = "wasm32")]const RAG_API_KEY_SECRET: &str = "RAG_API_KEY";Two things worth calling out explicitly, both visible in that snippet:
- Config, not environment variables. The retrieval endpoint comes from
payload.config.rag_endpoint. The invocation payload the runner hands toinvokeis always{ "config": { ... }, "input": { ... } }— the flow node’sconfig:block merged in. A component never reads a host environment variable for this. - Secrets come from the secrets store, never from config, payload, or environment. The API
key is fetched with
secrets_store::get("RAG_API_KEY")— a call to thegreentic:secrets-storehost interface, resolved per-tenant at runtime. It is never read out ofconfigor the invocation payload.
The dispatcher branch
Section titled “The dispatcher branch”The single ABI entry point (invoke) routes by operation name. The retrieve branch:
"retrieve" => match call_retrieval_service(&value) { Ok(body) => retrieve::shape_response(&body), Err(err) => serde_json::json!({ "error": err }),},An HTTP or secrets failure becomes a structured {"error": "..."} output rather than a trap —
the component stays well-behaved even when the retrieval service is down or misconfigured.
Path 2: embed retrieval logic in the component
Section titled “Path 2: embed retrieval logic in the component”Same skeleton as Path 1 — same scaffold command, same two required exports, same manifest and
packaging steps below — but retrieve never leaves the component. It embeds the query and
scores it directly against a corpus carried in config, instead of calling out over HTTP. Drop
--http-client from the scaffold command and skip the http-client-v1-1 feature entirely; there
is no outbound call to make.
component-rag in this workspace is a useful logic reference for this path — its scoring
approach is a valid pattern to read. It is not a copy-paste starting point: its call sites
and module layout were generated against a different bindings shape than the scaffold above
produces, so treat it as a shape to learn from, not code to lift verbatim.
Path 3: no code — Knowledge Base collections
Section titled “Path 3: no code — Knowledge Base collections”If you only have documents and no retrieval logic to write, you don’t need a component at all. Use the Designer’s Knowledge Base collections: upload documents to the tenant knowledge library, group them into a named collection, and attach the collection to an agentic worker’s Knowledge tile. The built-in knowledge tier (see Agent Knowledge (RAG)) handles chunking, embedding, and per-turn retrieval for you.
Build, test, package
Section titled “Build, test, package”-
Run the unit tests — the part of the component that is verifiable locally, no runtime needed:
Terminal window cargo test --lib retrieve -
Build the release wasm:
Terminal window cargo build --target wasm32-wasip2 --release -
Check the required exports — this is the property that actually matters, since a missing export fails silently at runtime rather than at build time. The repo commits a small script for this,
check-exports.sh:#!/usr/bin/env bash# Both exports are required by the runner: node@0.6.0 for invoke (pack.rs:381),# component-descriptor@0.6.0 for describe (pack.rs:3186).set -euo pipefailWASM="${1:-target/wasm32-wasip2/release/greentic_example_rag.wasm}"WIT="$(wasm-tools component wit "$WASM")"fail=0for iface in "greentic:component/node@0.6.0" "greentic:component/component-descriptor@0.6.0"; doif grep -q "$iface" <<<"$WIT"; thenecho "ok: exports $iface"elseecho "MISSING: $iface"; fail=1fidoneexit $failRun it:
Terminal window ./check-exports.shExpect two
ok:lines. -
Refresh the manifest through the CLI, with
--no-flow:Terminal window greentic-component build --manifest ./component.manifest.json --no-flow--no-flowis required here, not optional: a plaingreentic-component build(no flags) tries to generate a default dev-flow from the operation’s input schema, and fails outright becausequeryis a required field with no default —greentic-componentwon’t invent a value for a required-no-default field.--no-flowskips dev-flow generation and just refreshes the wasm hashes in the manifest. (Because of the same harness gap above, this command also prints a warning and skips emittingdist/*.describe.cbor/.json— the wasm build and hash refresh still succeed.)
Package as a pack
Section titled “Package as a pack”The pack CLI binary is greentic-pack — packc is only the crate name, not an installable
command.
pack_id: greentic.example.ragversion: 0.1.0kind: applicationpublisher: Greenticcapabilities: []components: - id: ai.greentic.greentic-example-rag version: 0.1.0 world: greentic:component/component@0.6.0 supports: - componentconfig profiles: default: default supported: - default capabilities: wasi: random: true clocks: true host: secrets: required: - key: RAG_API_KEY required: true description: API key for the retrieval service scope: env: dev tenant: default format: text http: client: true server: false telemetry: scope: node wasm: components/greentic_example_rag.wasmdependencies: []flows: []assets: []profiles and capabilities are required per component entry — a minimal pack.yaml with only
id/version/world/wasm/supports fails greentic-pack build with missing field profiles. The capabilities block here mirrors what’s already declared in
component.manifest.json.
mkdir -p pack/componentscp target/wasm32-wasip2/release/greentic_example_rag.wasm pack/components/cp component.manifest.json pack/components/greentic-pack build --in ./packThat produces pack/dist/manifest.cbor and pack/dist/pack.gtpack.
Sign and verify
Section titled “Sign and verify”openssl genpkey -algorithm ed25519 -out signing.pemgreentic-pack sign --pack ./pack --key signing.pemopenssl pkey -in signing.pem -pubout -out signing.pub.pemgreentic-pack verify --pack ./pack --key signing.pub.pemverify takes the public key, not the private signing key you passed to sign — passing
the private key fails with a PEM-type mismatch.
Use it: as a flow node
Section titled “Use it: as a flow node”Flow node keys are <component-id>.<operation>, and the flow itself is schema_version: 2:
# Example flow authored for this guide (not a file in the reference repo).id: answer_with_contexttitle: Answer with custom RAG contexttype: messagingschema_version: 2start: retrieve_context
nodes: retrieve_context: ai.greentic.greentic-example-rag.retrieve: # component=ai.greentic.greentic-example-rag, operation=retrieve config: rag_endpoint: "https://rag.example.com/v1/retrieve" input: mapping: '{"query": "{{in.text}}", "top_k": 3}' routing: - to: answer
answer: dw.agent.support-answerer: input: mapping: '{"user_text": "{{in.text}}", "context": "{{retrieve_context.context}}"}' routing: replyUse it: as an agentic-worker tool
Section titled “Use it: as an agentic-worker tool”Register the component in the admin console’s Component tools tab
(see Component Tools & Registry Credentials), with role
agentic_worker, and list retrieve in allowed_operations. If the component lives in a
private OCI registry, add a registry credential for that host in the same tab so introspection
and pulls can authenticate.
Limits you need to know
Section titled “Limits you need to know”Secrets
Section titled “Secrets”Secret values never come from config, the invocation payload, or a host environment variable —
only from the secrets store, via greentic:secrets-store#get (as shown in
call_retrieval_service above). A component declares what it needs in
component.manifest.json:
"secret_requirements": [ { "key": "RAG_API_KEY", "required": true, "description": "API key for the retrieval service", "format": "text", "scope": { "env": "dev", "tenant": "default" } }]To collect and apply the secrets a pack declares in one pass, use:
greentic-secrets init --pack <PATH>See Secrets for the full model — scoping, rotation, and how the runtime enforces that a component can only reach the secrets it declared.
Dependencies
Section titled “Dependencies”greentic-example-rag pins:
greentic-interfaces-guest1.1.1greentic-types1.1.2
See also
Section titled “See also”- Agent Knowledge (RAG) — the built-in knowledge tier this page’s Path 3 points at
- Components — the general component model
- Secrets — the full secrets model
- Component Tools & Registry Credentials — registering a component as an agentic-worker tool
greentic-example-ragatv0.1.0— the full source every code block above is quoted from