use serde::Serialize;
use serde_json::{json, Map, Value};
use crate::client::Client;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
Native,
Json,
#[default]
Toon,
Markdown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LlmOptions {
pub max_rows: Option<usize>,
pub redact_sensitive: bool,
pub include_schema: bool,
}
impl Default for LlmOptions {
fn default() -> Self {
Self {
max_rows: None,
redact_sensitive: true,
include_schema: false,
}
}
}
impl LlmOptions {
pub fn max_rows(mut self, rows: usize) -> Self {
self.max_rows = Some(rows);
self
}
pub fn redact_sensitive(mut self, redact: bool) -> Self {
self.redact_sensitive = redact;
self
}
pub fn include_schema(mut self, include: bool) -> Self {
self.include_schema = include;
self
}
fn to_wire(self) -> Value {
json!({
"max_rows": self.max_rows.filter(|r| *r > 0),
"redact_sensitive": self.redact_sensitive,
"include_schema": self.include_schema,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(transparent)]
pub struct LlmSource(Value);
impl LlmSource {
pub fn sql(query: &str) -> Self {
LlmSource(json!({"Sql": {"query": query}}))
}
pub fn document_find(
collection: &str,
filter: &crate::DocumentFilter,
limit: Option<usize>,
) -> Self {
LlmSource(json!({"DocumentFind": {
"collection": collection,
"filter": filter,
"limit": limit.filter(|l| *l > 0),
}}))
}
}
impl Client {
pub fn llm_context(
&mut self,
sources: &[LlmSource],
format: OutputFormat,
options: Option<LlmOptions>,
) -> Result<String> {
if sources.is_empty() {
return Err(Error::invalid("a context bundle needs at least one source"));
}
let response = self.send(json!({"Llm": {"Context": {
"sources": sources,
"format": format,
"options": options.unwrap_or_default().to_wire(),
}}}))?;
rendered(&response)
}
pub fn llm_schema(
&mut self,
format: OutputFormat,
options: Option<LlmOptions>,
) -> Result<String> {
let response = self.send(json!({"Llm": {"Schema": {
"format": format,
"options": options.unwrap_or_default().to_wire(),
}}}))?;
rendered(&response)
}
pub fn admin_ping(&mut self) -> Result<()> {
self.send(json!({"Admin": "Ping"})).map(|_| ())
}
pub fn admin_status(&mut self) -> Result<Map<String, Value>> {
let response = self.send(json!({"Admin": "Status"}))?;
if response.kind == "Message" {
let text = response.data.as_str().unwrap_or_default().to_string();
let mut out = Map::new();
out.insert("message".into(), Value::String(text));
return Ok(out);
}
response.decode("admin status")
}
}
fn rendered(response: &crate::Response) -> Result<String> {
match response.kind.as_str() {
"Toon" | "Message" => Ok(response
.data
.as_str()
.map(str::to_string)
.unwrap_or_else(|| response.data.to_string())),
"Json" => Ok(response.data.to_string()),
other => Err(Error::protocol(format!(
"expected a rendered export, got {}",
if other.is_empty() { "no data" } else { other }
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DocumentFilter;
#[test]
fn formats_use_the_servers_spelling() {
for (format, text) in [
(OutputFormat::Native, "native"),
(OutputFormat::Json, "json"),
(OutputFormat::Toon, "toon"),
(OutputFormat::Markdown, "markdown"),
] {
assert_eq!(serde_json::to_value(format).unwrap(), text);
}
}
#[test]
fn the_default_options_redact() {
let options = LlmOptions::default();
assert!(options.redact_sensitive);
assert_eq!(
options.to_wire(),
json!({"max_rows": null, "redact_sensitive": true, "include_schema": false})
);
assert_eq!(
LlmOptions::default().max_rows(10).to_wire()["max_rows"],
json!(10)
);
}
#[test]
fn sources_are_externally_tagged() {
assert_eq!(
serde_json::to_value(LlmSource::sql("SELECT 1")).unwrap(),
json!({"Sql": {"query": "SELECT 1"}})
);
let source = LlmSource::document_find("products", &DocumentFilter::all(), None);
let value = serde_json::to_value(source).unwrap();
assert_eq!(value["DocumentFind"]["collection"], "products");
assert_eq!(value["DocumentFind"]["filter"], json!("All"));
assert_eq!(value["DocumentFind"]["limit"], Value::Null);
}
#[test]
fn a_rendered_export_reads_text_or_json() {
let mut response = crate::Response {
request_id: "r".into(),
status: "ok".into(),
kind: "Toon".into(),
data: json!("users:\n 1 ada"),
route: None,
elapsed_ms: None,
warnings: vec![],
error_code: None,
leader_hint: None,
};
assert_eq!(rendered(&response).unwrap(), "users:\n 1 ada");
response.kind = "Json".into();
response.data = json!({"users": []});
assert_eq!(rendered(&response).unwrap(), r#"{"users":[]}"#);
response.kind = "Rows".into();
assert!(rendered(&response).is_err());
}
}