blob: 5140eca920e76ebd4f29705fda2d10f65c4f8113 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use maka_code_runtime::{CellContext, CellDiagnostic, CellDiagnosticKind, CellOutput};
use serde_json::{Value, json};
pub(crate) fn invoke(
name: &str,
mut args: Vec<Value>,
mut kwargs: serde_json::Map<String, Value>,
context: &CellContext,
) -> Result<Value, CellDiagnostic> {
for value in args.iter().chain(kwargs.values()) {
crate::protocol::validate_json(value)?;
}
let fields: &[&str] = match name {
"text" | "notify" | "audio" | "generated_image" => &["value"],
"image" => &["value", "detail"],
"store" => &["key", "value"],
"load" => &["key"],
"exit" | "yield_control" => &[],
_ => return Err(error("unknown Python helper")),
};
if args.len() > fields.len() {
return Err(error("too many helper arguments"));
}
for field in fields.iter().take(args.len()) {
if kwargs.contains_key(*field) {
return Err(error("argument supplied twice"));
}
}
for field in fields.iter().skip(args.len()) {
match kwargs.remove(*field) {
Some(value) => args.push(value),
None if name == "image" && *field == "detail" => args.push(Value::Null),
None => return Err(error(format!("missing argument: {field}"))),
}
}
if !kwargs.is_empty() {
return Err(error("unknown helper keyword argument"));
}
match name {
"text" => context.emit(CellOutput::Text {
text: display(&args[0]),
})?,
"notify" => context.notify(display(&args[0]))?,
"store" => context.store(key(&args[0])?.into(), args[1].clone())?,
"load" => return Ok(context.load(key(&args[0])?).unwrap_or(Value::Null)),
"yield_control" => context.yield_output(),
"exit" => {}
"image" => {
let value = &args[0];
let detail = if !args[1].is_null() {
args[1].clone()
} else {
value
.get("detail")
.or_else(|| value.pointer("/_meta/codex~1imageDetail"))
.cloned()
.unwrap_or(Value::Null)
};
if value.get("ref").is_some() {
emit(
context,
json!({"kind":"image","image":{"mimeType":value.get("mimeType"),"ref":value.get("ref"),"detail":detail}}),
)?;
} else {
media(
context,
"image",
value.get("image_url").unwrap_or(value),
detail,
)?;
}
}
"audio" => media(
context,
"audio",
args[0].get("audio_url").unwrap_or(&args[0]),
Value::Null,
)?,
"generated_image" => {
media(
context,
"image",
args[0]
.get("image_url")
.ok_or_else(|| error("missing image_url"))?,
Value::Null,
)?;
if let Some(hint) = args[0].get("output_hint").filter(|value| !value.is_null()) {
context.emit(CellOutput::Text {
text: display(hint),
})?;
}
}
_ => unreachable!(),
}
Ok(Value::Null)
}
fn media(
context: &CellContext,
kind: &str,
value: &Value,
detail: Value,
) -> Result<(), CellDiagnostic> {
let content = if let Some(url) = value.as_str() {
let (mime, data) = url
.strip_prefix("data:")
.and_then(|value| value.split_once(";base64,"))
.ok_or_else(|| error("expected a base64 data URL"))?;
if !mime.starts_with(&format!("{kind}/"))
|| !data
.trim_end_matches('=')
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/'))
|| data.len() - data.trim_end_matches('=').len() > 2
{
return Err(error("expected a base64 data URL"));
}
json!({"type":kind,"mimeType":mime,"data":data})
} else {
if value.get("type").and_then(Value::as_str) != Some(kind) {
return Err(error(format!("expected a {kind} content block")));
}
json!({"type":kind,"mimeType":value.get("mimeType"),"data":value.get("data")})
};
emit(
context,
json!({"kind":"media","content":content,"detail":detail}),
)
}
fn emit(context: &CellContext, value: Value) -> Result<(), CellDiagnostic> {
context.emit(serde_json::from_value(value).map_err(error)?)
}
fn key(value: &Value) -> Result<&str, CellDiagnostic> {
value
.as_str()
.ok_or_else(|| error("store key must be a string"))
}
fn display(value: &Value) -> String {
value
.as_str()
.map(str::to_owned)
.unwrap_or_else(|| value.to_string())
}
pub(crate) fn error(message: impl std::fmt::Display) -> CellDiagnostic {
CellDiagnostic::new(CellDiagnosticKind::ExecutionError, message.to_string())
}