Interfaces and Plugins
You're reading Part One of the Create Wraps tutorial, where we learn everything you need to know to productively develop Polywrap wraps.
Now that our Oracle Wrap can hide data with the obscure
method, we're ready to add the enlighten
method and illuminate new knowledge with generative AI. The enlighten
method will allow users to ask a question, and it will return an answer generated by an AI model. We're going to import the HTTP interface and use it to send HTTP requests to the Perplexity API for open-source large language models.
Let's learn a bit about interface wraps and plugins to see how they can help us.
Interfaces
An interface wrap defines a schema without implementing a module. Other wraps and plugins can implement the interface instead. An interface doesn't have knowledge of its implementations. Instead, users of the Polywrap Client can choose which wraps or plugins to register as known implementations.
We're going to use the HTTP interface, imported using the Wrap URI "wrapscan.io/polywrap/http@1.0"
.
It is considered a best practice for users to register a URI Redirect for each interface in their Polywrap Client configuration that redirects from the interface URI to the URI of their primary implementation. This allows wrap users to directly invoke the interface URI--as if it were a Wasm wrap--without needing to know the URI of the primary implementation. In Polywrap's default Polywrap Client configuration, each interface URI is redirected to the URI of its default implementation.
What if it doesn't make sense to redirect to any one default implementation? What if the Polywrap Client user did not do so? Wraps can get a list of registered implementations at runtime and invoke one or more of them. You can learn how to do this in Interface Instances.
Polywrap's URI Resolution system uses the URI Resolver Extension wrap interface to resolve URIs.
Let's talk about two cases where interfaces are useful.
First, interfaces are useful when you want to allow for multiple, fungible implementations of an interface. For example, you might want to allow users to register multiple implementations of an interface that resolves URIs. This is the case with Polywrap's URI Resolution system. Users can register multiple implementations of the URI Resolver Extension interface, and the Polywrap Client will invoke all of them when trying to resolve a URI.
Another case is when you want your wrap to play nicely with plugins. Plugins are ordinary, language-specific packages that cannot necessarily be resolved by immutable Wrap URIs. Users can install a plugin using a package manager (e.g. JavaScript's NPM or Python's PyPi) and register it in the Polywrap Client with an arbitrary URI.
If a plugin implements an interface, users are expected to either redirect the interface URI to point to the registered plugin URI, or to register the plugin as an interface implementation that can be obtained at runtime. For example, the default Polywrap Client configuration uses the URI "plugin/http@1.1.0"
to register the HTTP plugin and registers a redirect from the HTTP interface URI to the plugin URI.
Plugins
Plugins extend the Polywrap Client to give wraps access to host capabilities while keeping users in control. Plugins are a more powerful and secure alternative to the Wasi standard used in many WebAssembly modules.
Standard WebAssembly modules are securely sandboxed and cannot access host capabilities like a user's filesystem. This is a feature, not a bug, because it prevents malicious behaviors. However, many useful modules need to access host capabilities. For example, a database module needs to read and write to the filesystem. A module that implements a web server needs to listen on a port. These capabilities can be implemented in plugins.
Users register plugins in the Polywrap Client, choosing which plugins they want to allow their wraps to use. Plugins are implemented as language-specific packages. For example, a plugin for the JavaScript Polywrap Client is just a JavaScript package.
From the perspective of a wrap developer, a plugin behaves like a wrap. Plugins are imported into schemas from URIs--typically by importing an interface--and are subinvoked just like a Wasm wrap module.
Implementation
Enough talk! Let's implement the enlighten
method.
Update the schema
Let's add the enlighten
method and an import statement for the HTTP interface to the schema. The method will take a question and a Perplexity API key as arguments, and will return a response. Remember to run codegen after updating the schema.
You can view the HTTP interface wrap's schema on GitHub.;
#import { Module } into Sha3 from "wrapscan.io/polywrap/sha3@1.0.0"
#import { Module } into Http from "wrapscan.io/polywrap/http@1.0"
type Module {
obscure(data: [String!]!, chaosLevel: Int): String!
enlighten(question: String!, apiKey: String!): String!
}
Implement the method
Now we can import the generated Http Module class to send HTTP requests.
To call the HTTP post
method, we need to use some custom types defined in the HTTP interface schema. Polywrap's codegen is smart enough to generate these types for us, even though we didn't list them in an import statement, because the Http Module class depends on them.
In order to focus on learning Polywrap, we aren't going to parse the HTTP response. We'll just return the raw response body as a string.
- Rust
- Go
- TypeScript
- AssemblyScript
pub mod wrap;
pub use wrap::prelude::*;
use crate::wrap::imported::{ArgsKeccak256, ArgsPost};
use polywrap_wasm_rs::Map;
impl ModuleTrait for Module {
fn obscure(args: ArgsObscure) -> Result<String, String> {
// handle default values
let chaos_level = args.chaos_level.unwrap_or(1).max(1);
let mut obscured = String::new();
for data in &args.data {
let mut message = data.clone();
for _ in 0..chaos_level {
message = Sha3Module::keccak_256(&ArgsKeccak256 { message })?;
}
obscured += &message;
}
Ok(obscured)
}
fn enlighten(args: ArgsEnlighten) -> Result<String, String> {
let mut headers = Map::new();
headers.insert("accept".to_string(), "application/json".to_string());
headers.insert("content-type".to_string(), "application/json".to_string());
headers.insert("Authorization".to_string(), format!("Bearer {}", args.api_key));
let body = format!(r#"{{
"model": "mistral-7b-instruct",
"messages": [
{{"role": "system", "content": "Be precise and concise."}},
{{"role": "user", "content": "{}"}}
]
}}"#, args.question);
let response = HttpModule::post(&ArgsPost {
url: "https://api.perplexity.ai/chat/completions".to_string(),
request: Some(HttpRequest {
headers: Some(headers),
response_type: HttpResponseType::TEXT,
body: Some(body),
url_params: None,
form_data: None,
timeout: None,
}),
})?.ok_or("request failed with null body".to_string())?;
if response.status != 200 {
return Err(format!("request failed with status {}", response.status));
}
response.body.ok_or("request failed with null body".to_string())
}
}
package module
import (
"example.com/template-wasm-go/module/wrap/types"
"example.com/template-wasm-go/module/wrap/imported/sha3"
"example.com/template-wasm-go/module/wrap/imported/http"
"fmt"
)
func Obscure(args *types.ArgsObscure) string {
// Handle default values
chaosLevel := int32(1)
if args.ChaosLevel != nil && *args.ChaosLevel >= 1 {
chaosLevel = *args.ChaosLevel
}
var obscured string
for _, data := range args.Data {
tempData := data
for i := int32(0); i < chaosLevel; i++ {
hashArgs := &sha3.Sha3_ArgsKeccak_256 { Message: tempData }
hashed, err := sha3.Sha3_Keccak_256(hashArgs)
if err != nil {
return ""
}
tempData = hashed
}
obscured += tempData
}
return obscured
}
func Enlighten(args *types.ArgsEnlighten) string {
headers := map[string]string{
"accept": "application/json",
"content-type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", args.ApiKey),
}
body := fmt.Sprintf(`{
"model": "mistral-7b-instruct",
"messages": [
{"role": "system", "content": "Be precise and concise."},
{"role": "user", "content": "%s"}
]
}`, args.Question)
httpArgs := &http.Http_ArgsPost{
Url: "https://api.perplexity.ai/chat/completions",
Request: &http.Http_Request{
Headers: headers,
ResponseType: http.Http_ResponseTypeTEXT,
Body: &body,
},
}
response, err := http.Http_Post(httpArgs)
if err != nil {
return ""
}
if response.Status != 200 {
return fmt.Sprintf("request failed with status %d", response.Status)
}
if response.Body == nil {
return "request failed with null body"
}
return *response.Body
}
import {
Args_enlighten,
Args_obscure,
Http_Module,
Http_Request,
Http_Response,
Http_ResponseType,
ModuleBase,
Sha3_Module
} from './wrap';
export class Module extends ModuleBase {
obscure(args: Args_obscure): string {
// handle default values
const chaosLevel = args.chaosLevel || 1;
// obscure the data with chaos
let obscured: string = "";
for (let i = 0; i < args.data.length; ++i) {
let data = args.data[i];
for (let j = 0; j < chaosLevel; ++j) {
const result = Sha3_Module.keccak_256({ message: data })
if (!result.ok) throw Error("hash failed");
data = result.value!!;
}
obscured += data;
}
return obscured;
}
enlighten(args: Args_enlighten): string {
const request: Http_Request = {
headers: new Map<string, string>()
.set('accept', 'application/json')
.set('content-type', 'application/json')
.set("Authorization", `Bearer ${args.apiKey}`),
responseType: Http_ResponseType.TEXT,
body: JSON.stringify({
model: 'mistral-7b-instruct',
messages: [
{role: 'system', content: 'Be precise and concise.'},
{role: 'user', content: args.question}
]
}),
urlParams: null,
formData: null,
timeout: null,
};
const result: Result<Http_Response | null> = Http_Module.post({
url: "https://api.perplexity.ai/chat/completions",
request,
});
if (!result.ok) throw result.error;
const response = result.value;
if (response == null) {
throw new Error("request failed with null response");
}
if (response.status != 200) {
throw new Error(`request failed with status ${response.status}`);
}
if (response.body == null) {
throw new Error("request failed with null body");
}
return response.body;
}
}
import {
Args_enlighten,
Args_obscure,
Http_Module,
Http_Request,
Http_Response,
Http_ResponseType,
ModuleBase,
Sha3_Module
} from './wrap';
export class Module extends ModuleBase {
obscure(args: Args_obscure): string {
// handle default values
const chaosLevel: i32 = (args.chaosLevel == null || args.chaosLevel!!.unwrap() < 1)
? 1
: args.chaosLevel!!.unwrap();
let obscured: string = "";
for (let i = 0; i < args.data.length; ++i) {
let data = args.data[i];
for (let j = 0; j < chaosLevel; ++j) {
data = Sha3_Module.keccak_256({ message: data }).expect("hash failed");
}
obscured += data;
}
return obscured;
}
enlighten(args: Args_enlighten): string {
const request: Http_Request = {
headers: new Map<string, string>()
.set('accept', 'application/json')
.set('content-type', 'application/json')
.set("Authorization", `Bearer ${args.apiKey}`),
responseType: Http_ResponseType.TEXT,
body: `{
"model": "mistral-7b-instruct",
"messages": [
{
"role": "system",
"content": "Be precise and concise."
},
{
"role": "user",
"content": "${args.question}"
}
]
}`
};
const response: Http_Response | null = Http_Module.post({
url: "https://api.perplexity.ai/chat/completions",
request,
}).unwrap();
if (response == null) {
throw new Error("request failed with null response");
}
if (response.status != 200) {
throw new Error(`request failed with status ${response.status}`);
}
if (response.body == null) {
throw new Error("request failed with null body");
}
return response.body!!;
}
}
Next Steps
Our Oracle Wrap is now complete! But we're not quite done yet. We still need to test our wrap and publish it so others can use it. We'll test our wrap in the next section. Make sure to build your wrap and fix any compilation errors before moving on.