Skip to content

Commit ebb654a

Browse files
committed
new plugin: serper web search
Signed-off-by: Tuan Anh Tran <[email protected]>
1 parent 27bda41 commit ebb654a

File tree

7 files changed

+514
-0
lines changed

7 files changed

+514
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasip1"

examples/plugins/serper/.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
debug/
4+
target/
5+
6+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8+
Cargo.lock
9+
10+
# These are backup files generated by rustfmt
11+
**/*.rs.bk
12+
13+
# MSVC Windows builds of rustc generate these, which store debugging information
14+
*.pdb
15+
16+
# RustRover
17+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19+
# and can be added to the global gitignore or merged into this file. For a more nuclear
20+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
21+
#.idea/

examples/plugins/serper/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "serper"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[lib]
7+
name = "plugin"
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
extism-pdk = "=1.4.0"
12+
serde = { version = "1.0.219", features = ["derive"] }
13+
serde_json = "1.0.140"
14+
base64-serde = "0.8.0"
15+
base64 = "0.22.1"

examples/plugins/serper/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM rust:1.86-slim AS builder
2+
3+
RUN rustup target add wasm32-wasip1 && \
4+
rustup component add rust-std --target wasm32-wasip1 && \
5+
cargo install cargo-auditable
6+
7+
WORKDIR /workspace
8+
COPY . .
9+
RUN cargo fetch
10+
RUN cargo auditable build --release --target wasm32-wasip1
11+
12+
FROM scratch
13+
WORKDIR /
14+
COPY --from=builder /workspace/target/wasm32-wasip1/release/plugin.wasm /plugin.wasm

examples/plugins/serper/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# serper
2+
3+
A plugin that performs Google web search using the [Serper](https://serper.dev) API and returns the raw JSON response for the given query string.
4+
5+
## Requirements
6+
7+
- Set `SERPER_API_KEY` in your config to your Serper API key.
8+
9+
## Usage
10+
11+
Call with:
12+
```json
13+
{
14+
"plugins": [
15+
{
16+
"name": "serper",
17+
"path": "oci://ghcr.io/tuananh/serper-plugin:latest",
18+
"runtime_config": {
19+
"env_var": {
20+
"SERPER_API_KEY": "<your-serper-api-key>"
21+
},
22+
"allowed_hosts": ["google.serper.dev"]
23+
}
24+
}
25+
]
26+
}
27+
```

examples/plugins/serper/src/lib.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
mod pdk;
2+
3+
use std::collections::BTreeMap;
4+
5+
use extism_pdk::*;
6+
use json::Value;
7+
use pdk::types::{
8+
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
9+
};
10+
use serde_json::json;
11+
12+
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
13+
match input.params.name.as_str() {
14+
"serper_web_search" => serper_web_search(input),
15+
_ => Ok(CallToolResult {
16+
is_error: Some(true),
17+
content: vec![Content {
18+
annotations: None,
19+
text: Some(format!("Unknown tool: {}", input.params.name)),
20+
mime_type: None,
21+
r#type: ContentType::Text,
22+
data: None,
23+
}],
24+
}),
25+
}
26+
}
27+
28+
fn serper_web_search(input: CallToolRequest) -> Result<CallToolResult, Error> {
29+
let args = input.params.arguments.unwrap_or_default();
30+
let query = match args.get("q") {
31+
Some(Value::String(q)) => q.clone(),
32+
_ => {
33+
return Ok(CallToolResult {
34+
is_error: Some(true),
35+
content: vec![Content {
36+
annotations: None,
37+
text: Some("Please provide a 'q' argument for the search query".into()),
38+
mime_type: None,
39+
r#type: ContentType::Text,
40+
data: None,
41+
}],
42+
});
43+
}
44+
};
45+
46+
let api_key = config::get("SERPER_API_KEY")?
47+
.ok_or_else(|| Error::msg("SERPER_API_KEY configuration is required but not set"))?;
48+
49+
let mut headers = BTreeMap::new();
50+
headers.insert("X-API-KEY".to_string(), api_key);
51+
headers.insert("Content-Type".to_string(), "application/json".to_string());
52+
53+
let req = HttpRequest {
54+
url: "https://google.serper.dev/search".to_string(),
55+
headers,
56+
method: Some("POST".to_string()),
57+
};
58+
59+
let body = json!({ "q": query });
60+
let res = http::request(&req, Some(&body.to_string()))?;
61+
let response_body = res.body();
62+
let response_text = String::from_utf8_lossy(response_body.as_slice()).to_string();
63+
64+
Ok(CallToolResult {
65+
is_error: None,
66+
content: vec![Content {
67+
annotations: None,
68+
text: Some(response_text),
69+
mime_type: Some("application/json".to_string()),
70+
r#type: ContentType::Text,
71+
data: None,
72+
}],
73+
})
74+
}
75+
76+
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
77+
Ok(ListToolsResult{
78+
tools: vec![
79+
ToolDescription {
80+
name: "serper_web_search".into(),
81+
description: "Performs a Google web search using the Serper API and returns the raw JSON response for the given query string.".into(),
82+
input_schema: json!({
83+
"type": "object",
84+
"properties": {
85+
"q": {
86+
"type": "string",
87+
"description": "The search query string",
88+
},
89+
},
90+
"required": ["q"],
91+
})
92+
.as_object()
93+
.unwrap()
94+
.clone(),
95+
},
96+
],
97+
})
98+
}

0 commit comments

Comments
 (0)