#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
)
)]
pub mod http;
mod package;
mod stdio;
pub use package::{ABI, Function, MANIFEST_FLAG, Manifest, Package};
pub use stdio::ServerInfo;
pub use subako_function_sdk_macros::{function, package};
use std::io::{BufRead as _, Write as _};
use std::process::ExitCode;
use schemars::JsonSchema;
use serde::Serialize;
use serde::de::DeserializeOwned;
pub struct Input(serde_json::Value);
impl Input {
pub fn parse<T: DeserializeOwned>(self) -> Result<T, ToolError> {
serde_json::from_value(self.0).map_err(|e| ToolError::new(format!("invalid input: {e}")))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Output {
content: Vec<Content>,
structured: Option<serde_json::Value>,
}
impl Output {
pub fn json<T: Serialize>(value: &T) -> Result<Output, ToolError> {
let value = to_json(value)?;
Ok(Output {
content: vec![Content::Text(value.to_string())],
structured: Some(value),
})
}
pub fn content(content: Vec<Content>) -> Output {
Output {
content,
structured: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Content {
Text(String),
ImageFile(String),
Image {
mime_type: String,
data: String,
},
}
impl Content {
pub fn text(text: impl Into<String>) -> Self {
Content::Text(text.into())
}
pub fn image_file(path: impl Into<String>) -> Self {
Content::ImageFile(path.into())
}
fn to_json(&self) -> serde_json::Value {
match self {
Content::Text(text) => serde_json::json!({"type": "text", "text": text}),
Content::ImageFile(path) => {
let name = path.rsplit('/').next().unwrap_or(path);
serde_json::json!({"type": "resource_link", "uri": volume_uri(path), "name": name})
}
Content::Image { mime_type, data } => {
serde_json::json!({"type": "image", "mimeType": mime_type, "data": data})
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Answer<T> {
value: T,
content: Vec<Content>,
}
impl<T> Answer<T> {
pub fn new(value: T) -> Self {
Answer {
value,
content: Vec::new(),
}
}
pub fn with(mut self, content: Content) -> Self {
self.content.push(content);
self
}
pub fn with_image_file(self, path: impl Into<String>) -> Self {
self.with(Content::image_file(path))
}
}
pub trait Reply {
fn schema(contract: schemars::generate::Contract) -> serde_json::Value;
fn into_output(self) -> Result<Output, ToolError>;
}
impl<T: Serialize + JsonSchema> Reply for T {
fn schema(contract: schemars::generate::Contract) -> serde_json::Value {
package::schema::<T>(contract)
}
fn into_output(self) -> Result<Output, ToolError> {
Output::json(&self)
}
}
impl<T: Serialize + JsonSchema> Reply for Answer<T> {
fn schema(contract: schemars::generate::Contract) -> serde_json::Value {
package::schema::<T>(contract)
}
fn into_output(self) -> Result<Output, ToolError> {
Output::json(&self.value).map(|output| Output {
content: output.content.into_iter().chain(self.content).collect(),
..output
})
}
}
const SEGMENT: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'-')
.remove(b'.')
.remove(b'_')
.remove(b'~');
fn volume_uri(path: &str) -> String {
let encode = |path: &str| {
path.split('/')
.map(|segment| percent_encoding::utf8_percent_encode(segment, SEGMENT).to_string())
.collect::<Vec<_>>()
.join("/")
};
match path.strip_prefix("/volume/") {
Some(inside) => format!("volume:///{}", encode(inside)),
None => format!("file://{}", encode(path)),
}
}
fn to_json<T: Serialize>(value: &T) -> Result<serde_json::Value, ToolError> {
serde_json::to_value(value).map_err(|e| ToolError::new(format!("output is not JSON: {e}")))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolError(String);
impl ToolError {
pub fn new(message: impl Into<String>) -> Self {
ToolError(message.into())
}
pub fn unknown_tool(tool: &str) -> Self {
ToolError(format!("unknown tool: {tool}"))
}
pub fn message(&self) -> &str {
&self.0
}
}
impl From<String> for ToolError {
fn from(message: String) -> Self {
ToolError(message)
}
}
impl From<&str> for ToolError {
fn from(message: &str) -> Self {
ToolError(message.to_string())
}
}
impl From<std::io::Error> for ToolError {
fn from(error: std::io::Error) -> Self {
ToolError(error.to_string())
}
}
pub trait Server {
fn info(&self) -> ServerInfo<'_>;
fn tools(&self) -> Vec<serde_json::Value>;
fn call(&self, tool: &str, input: Input) -> Result<Output, ToolError>;
}
pub fn execute(stdin: &[u8], server: &impl Server) -> (Vec<u8>, u8) {
let stdout = stdin
.split_inclusive(|byte| *byte == b'\n')
.flat_map(|line| serve(line, server))
.collect();
(stdout, 0)
}
fn serve(line: &[u8], server: &impl Server) -> Vec<u8> {
match stdio::read(line) {
stdio::Incoming::Discover { id } => stdio::discover_line(&id, &server.info()),
stdio::Incoming::ListTools { id } => stdio::tools_line(&id, &server.info(), server.tools()),
stdio::Incoming::Call(call) => {
let result = server.call(&call.name, Input(call.arguments));
stdio::result_line(&call.id, &server.info(), result_fields(result).into_iter())
}
stdio::Incoming::Ignored => Vec::new(),
stdio::Incoming::Refused(refusal) => refusal.line(),
}
}
fn result_fields(result: Result<Output, ToolError>) -> Vec<(&'static str, serde_json::Value)> {
match result {
Ok(Output {
content,
structured,
}) => {
let content: Vec<_> = content.iter().map(Content::to_json).collect();
let content = ("content", serde_json::json!(content));
match structured {
Some(structured) => vec![content, ("structuredContent", structured)],
None => vec![content],
}
}
Err(ToolError(message)) => vec![
(
"content",
serde_json::json!([{"type": "text", "text": message}]),
),
("isError", serde_json::json!(true)),
],
}
}
pub fn run(server: &impl Server) -> ExitCode {
loop {
let mut line = Vec::new();
let read = std::io::stdin().lock().read_until(b'\n', &mut line);
let (answer, more) = match read {
Ok(0) => return ExitCode::SUCCESS,
Ok(_) => (serve(&line, server), true),
Err(e) => (stdio::Refusal::unreadable(&e).line(), false),
};
let mut stdout = std::io::stdout().lock();
match stdout.write_all(&answer).and_then(|()| stdout.flush()) {
Ok(()) if more => {}
Ok(()) => return ExitCode::SUCCESS,
Err(_) => return ExitCode::from(2),
}
}
}