use std::sync::Arc;
use pamin_kit::schemars::JsonSchema;
use pamin_kit::{
Audience, Body, Completing, Content, Ctx, Feature, Hardware, Hints, Icon, Kit, Local, Looks,
Manifest, Message, Module, Ops, Role, Routing, Space,
};
use serde::{Deserialize, Serialize};
pub struct Echo;
#[derive(Deserialize, JsonSchema)]
pub struct Say {
text: String,
}
#[derive(Serialize, JsonSchema)]
pub struct Said {
text: String,
project: String,
}
#[derive(Deserialize, JsonSchema)]
pub struct SayUri {
text: String,
}
#[derive(Deserialize, JsonSchema)]
pub struct Unsay {}
#[derive(Deserialize, JsonSchema)]
pub struct Dot {}
const DOT: &[u8] = &[
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b,
0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60, 0x00, 0x00, 0x00,
0x02, 0x00, 0x01, 0x48, 0xaf, 0xa4, 0x71, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
0x42, 0x60, 0x82,
];
#[derive(Deserialize, JsonSchema)]
pub struct Visit {}
#[derive(Deserialize, JsonSchema)]
pub struct Confirm {
text: String,
}
#[derive(Deserialize, JsonSchema)]
pub struct Sure {
yes: bool,
}
#[derive(Deserialize, JsonSchema)]
pub struct Greet {
name: String,
#[serde(default)]
greeting: String,
}
#[derive(Deserialize, JsonSchema)]
pub struct Wait {
text: String,
millis: u64,
}
impl Echo {
async fn say(&self, space: &Space, say: Say) -> Result<Said, String> {
if say.text.is_empty() {
return Err("nothing to say".into());
}
Ok(Said {
text: say.text,
project: space.project.clone(),
})
}
async fn confirm(&self, ctx: &Ctx, confirm: Confirm) -> Result<Said, String> {
let question = format!("Say {:?}?", confirm.text);
match ctx.ask::<Sure>("sure", &question)? {
Some(Sure { yes: true }) => self.say(ctx.space(), Say { text: confirm.text }).await,
_ => Err("the user did not want it said".into()),
}
}
async fn wait(&self, ctx: &Ctx, wait: Wait) -> Result<Said, String> {
const STEPS: u64 = 10;
for step in 0..STEPS {
if ctx.is_cancelled() {
return Err("cancelled".into());
}
ctx.progress(step as f64, Some(STEPS as f64), Some("waiting"))
.await;
tokio::time::sleep(std::time::Duration::from_millis(wait.millis / STEPS)).await;
}
self.say(ctx.space(), Say { text: wait.text }).await
}
}
impl Module for Echo {
fn manifest(&self) -> Manifest {
Manifest {
name: "echo",
contract_version: "1.0.0",
routing: Routing::Stateless,
long_running: true,
scopes: vec![],
metering_unit: "call",
hardware: Hardware::default(),
audience: Audience::default(),
}
}
fn register(&self, ops: &mut Ops<Self>) {
ops.tool(
"echo_say",
Feature::Core,
|m: Arc<Self>, ctx: Ctx, a: Say| async move {
let said = m.say(ctx.space(), a).await?;
let uri = format!("echo://say/{}", said.text);
ctx.updated(uri.clone());
ctx.attach(Content::Link {
uri,
name: "Said".into(),
mime: Some("application/json".into()),
});
Ok::<_, String>(said)
},
)
.tool(
"echo_shout",
Feature::Pro("shout"),
|m: Arc<Self>, ctx: Ctx, a: Say| async move { m.say(ctx.space(), a).await },
)
.tool(
"echo_dot",
Feature::Core,
|_: Arc<Self>, ctx: Ctx, _: Dot| async move {
ctx.attach(Content::Image {
mime: "image/png".into(),
data: DOT.to_vec(),
});
Ok::<_, String>(())
},
)
.tool(
"echo_unsay",
Feature::Core,
|_: Arc<Self>, _: Ctx, _: Unsay| async { Ok::<_, String>(()) },
)
.app_only("echo_unsay")
.tool(
"echo_visit",
Feature::Core,
|_: Arc<Self>, ctx: Ctx, _: Visit| async move {
let went = ctx.ask_url(
"visit",
"Have a look at echo's page.",
"https://example.com/echo",
)?;
Ok::<_, String>(went)
},
)
.tool(
"echo_confirm",
Feature::Core,
|m: Arc<Self>, ctx: Ctx, a: Confirm| async move { m.confirm(&ctx, a).await },
)
.task(
"echo_wait",
Feature::Core,
|m: Arc<Self>, ctx: Ctx, a: Wait| async move { m.wait(&ctx, a).await },
)
.app("echo_say", "ui://echo/said", include_str!("said.html"))
.hints(
"echo_say",
Hints {
read_only: true,
idempotent: true,
open_world: false,
..Hints::default()
},
)
.resource(
"echo://about",
"About echo",
Feature::Core,
|_: Arc<Self>, _: Ctx, (): ()| async {
Ok::<_, String>(Body::text("text/plain", "Echo says text back."))
},
)
.resource(
"echo://say/{text}",
"Said",
Feature::Core,
|m: Arc<Self>, ctx: Ctx, uri: SayUri| async move {
let said = m.say(ctx.space(), Say { text: uri.text }).await?;
Body::json(&said)
},
)
.prompt(
"echo_greet",
Feature::Core,
|_: Arc<Self>, _: Ctx, greet: Greet| async move {
Ok::<_, String>(vec![Message::text(
Role::User,
match greet.greeting.as_str() {
"" => format!("Say hello to {} with echo_say.", greet.name),
greeting => format!("Say {greeting} to {} with echo_say.", greet.name),
},
)])
},
)
.complete(
"echo_greet",
"name",
|_: Arc<Self>, _: Ctx, completing: Completing| async move {
let names = ["Ada", "Alan", "Grace"];
Ok::<_, String>(
names
.iter()
.filter(|name| name.starts_with(&completing.typed))
.map(|name| name.to_string())
.collect(),
)
},
)
.complete(
"echo_greet",
"greeting",
|_: Arc<Self>, _: Ctx, completing: Completing| async move {
let formal = completing
.given
.get("name")
.is_some_and(|name| name == "Ada");
let greetings: &[&str] = if formal {
&["Good day"]
} else {
&["Hello", "Hi"]
};
Ok::<_, String>(
greetings
.iter()
.filter(|greeting| greeting.starts_with(&completing.typed))
.map(|greeting| greeting.to_string())
.collect(),
)
},
)
.looks(
"echo_say",
Looks {
title: Some("Say"),
..Looks::default()
},
)
.looks(
"echo://about",
Looks {
title: Some("About echo"),
mime: Some("text/plain"),
audience: &[Role::User],
priority: Some(0.2),
..Looks::default()
},
)
.server(Looks {
title: Some("Echo"),
icons: &[Icon {
src: "https://example.com/echo.svg",
mime: Some("image/svg+xml"),
sizes: &["any"],
}],
about: Some("Says back what it is given."),
website: Some("https://example.com/echo"),
..Looks::default()
})
.instructions("Use echo_say to hear text back; echo_wait for a slow reply.");
}
}
#[allow(dead_code)]
#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
Kit::new(Echo, Local::new("example")).serve_stdio().await
}