2023-11-15 22:46:37 +08:00
|
|
|
mod context;
|
|
|
|
mod document;
|
|
|
|
pub(crate) mod error;
|
|
|
|
mod fields;
|
|
|
|
mod template_checker;
|
|
|
|
|
2024-09-02 17:28:53 +08:00
|
|
|
use std::collections::BTreeMap;
|
2023-11-15 22:46:37 +08:00
|
|
|
use std::convert::TryFrom;
|
2024-09-02 17:28:53 +08:00
|
|
|
use std::ops::Deref;
|
2023-11-15 22:46:37 +08:00
|
|
|
|
|
|
|
use error::{NewPromptError, RenderPromptError};
|
|
|
|
|
|
|
|
use self::context::Context;
|
|
|
|
use self::document::Document;
|
|
|
|
use crate::update::del_add::DelAdd;
|
2024-09-02 17:28:53 +08:00
|
|
|
use crate::{FieldId, FieldsIdsMap};
|
2023-11-15 22:46:37 +08:00
|
|
|
|
|
|
|
pub struct Prompt {
|
|
|
|
template: liquid::Template,
|
|
|
|
template_text: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
|
|
pub struct PromptData {
|
|
|
|
pub template: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<Prompt> for PromptData {
|
|
|
|
fn from(value: Prompt) -> Self {
|
2023-12-14 05:06:39 +08:00
|
|
|
Self { template: value.template_text }
|
2023-11-15 22:46:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<PromptData> for Prompt {
|
|
|
|
type Error = NewPromptError;
|
|
|
|
|
|
|
|
fn try_from(value: PromptData) -> Result<Self, Self::Error> {
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new(value.template)
|
2023-11-15 22:46:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Clone for Prompt {
|
|
|
|
fn clone(&self) -> Self {
|
|
|
|
let template_text = self.template_text.clone();
|
2023-12-14 05:06:39 +08:00
|
|
|
Self { template: new_template(&template_text).unwrap(), template_text }
|
2023-11-15 22:46:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_template(text: &str) -> Result<liquid::Template, liquid::Error> {
|
|
|
|
liquid::ParserBuilder::with_stdlib().build().unwrap().parse(text)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_template() -> liquid::Template {
|
|
|
|
new_template(default_template_text()).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_template_text() -> &'static str {
|
|
|
|
"{% for field in fields %} \
|
|
|
|
{{ field.name }}: {{ field.value }}\n\
|
|
|
|
{% endfor %}"
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Prompt {
|
|
|
|
fn default() -> Self {
|
2023-12-14 05:06:39 +08:00
|
|
|
Self { template: default_template(), template_text: default_template_text().into() }
|
2023-11-15 22:46:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for PromptData {
|
|
|
|
fn default() -> Self {
|
2023-12-14 05:06:39 +08:00
|
|
|
Self { template: default_template_text().into() }
|
2023-11-15 22:46:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Prompt {
|
2023-12-14 05:06:39 +08:00
|
|
|
pub fn new(template: String) -> Result<Self, NewPromptError> {
|
2023-11-15 22:46:37 +08:00
|
|
|
let this = Self {
|
|
|
|
template: liquid::ParserBuilder::with_stdlib()
|
|
|
|
.build()
|
|
|
|
.unwrap()
|
|
|
|
.parse(&template)
|
|
|
|
.map_err(NewPromptError::cannot_parse_template)?,
|
|
|
|
template_text: template,
|
|
|
|
};
|
|
|
|
|
|
|
|
// render template with special object that's OK with `doc.*` and `fields.*`
|
|
|
|
this.template
|
|
|
|
.render(&template_checker::TemplateChecker)
|
|
|
|
.map_err(NewPromptError::invalid_fields_in_template)?;
|
|
|
|
|
|
|
|
Ok(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn render(
|
|
|
|
&self,
|
|
|
|
document: obkv::KvReaderU16<'_>,
|
|
|
|
side: DelAdd,
|
2024-09-02 17:28:53 +08:00
|
|
|
field_id_map: &FieldsIdsMapWithMetadata,
|
2023-11-15 22:46:37 +08:00
|
|
|
) -> Result<String, RenderPromptError> {
|
|
|
|
let document = Document::new(document, side, field_id_map);
|
|
|
|
let context = Context::new(&document, field_id_map);
|
|
|
|
|
|
|
|
self.template.render(&context).map_err(RenderPromptError::missing_context)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-02 17:28:53 +08:00
|
|
|
pub struct FieldsIdsMapWithMetadata<'a> {
|
|
|
|
fields_ids_map: &'a FieldsIdsMap,
|
|
|
|
metadata: BTreeMap<FieldId, FieldMetadata>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> FieldsIdsMapWithMetadata<'a> {
|
|
|
|
pub fn new(fields_ids_map: &'a FieldsIdsMap, searchable_fields_ids: &'_ [FieldId]) -> Self {
|
|
|
|
let mut metadata: BTreeMap<FieldId, FieldMetadata> =
|
|
|
|
fields_ids_map.ids().map(|id| (id, Default::default())).collect();
|
|
|
|
for searchable_field_id in searchable_fields_ids {
|
|
|
|
let Some(metadata) = metadata.get_mut(searchable_field_id) else { continue };
|
|
|
|
metadata.searchable = true;
|
|
|
|
}
|
|
|
|
Self { fields_ids_map, metadata }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn metadata(&self, field_id: FieldId) -> Option<FieldMetadata> {
|
|
|
|
self.metadata.get(&field_id).copied()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Deref for FieldsIdsMapWithMetadata<'a> {
|
|
|
|
type Target = FieldsIdsMap;
|
|
|
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
self.fields_ids_map
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Clone, Copy)]
|
|
|
|
pub struct FieldMetadata {
|
|
|
|
pub searchable: bool,
|
|
|
|
}
|
|
|
|
|
2023-12-13 04:19:48 +08:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::Prompt;
|
|
|
|
use crate::error::FaultSource;
|
|
|
|
use crate::prompt::error::{NewPromptError, NewPromptErrorKind};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn default_template() {
|
|
|
|
// does not panic
|
|
|
|
Prompt::default();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn empty_template() {
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("".into()).unwrap();
|
2023-12-13 04:19:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_ok() {
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{{doc.title}}: {{doc.overview}}".into()).unwrap();
|
2023-12-13 04:19:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_syntax() {
|
|
|
|
assert!(matches!(
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{{doc.title: {{doc.overview}}".into()),
|
2023-12-13 04:19:48 +08:00
|
|
|
Err(NewPromptError {
|
|
|
|
kind: NewPromptErrorKind::CannotParseTemplate(_),
|
|
|
|
fault: FaultSource::User
|
|
|
|
})
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_missing_doc() {
|
|
|
|
assert!(matches!(
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{{title}}: {{overview}}".into()),
|
2023-12-13 04:19:48 +08:00
|
|
|
Err(NewPromptError {
|
|
|
|
kind: NewPromptErrorKind::InvalidFieldsInTemplate(_),
|
|
|
|
fault: FaultSource::User
|
|
|
|
})
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_nested_doc() {
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{{doc.actor.firstName}}: {{doc.actor.lastName}}".into()).unwrap();
|
2023-12-13 04:19:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_fields() {
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{% for field in fields %}{{field}}{% endfor %}".into()).unwrap();
|
2023-12-13 04:19:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_fields_ok() {
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{% for field in fields %}{{field.name}}: {{field.value}}{% endfor %}".into())
|
|
|
|
.unwrap();
|
2023-12-13 04:19:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_fields_invalid() {
|
|
|
|
assert!(matches!(
|
|
|
|
// intentionally garbled field
|
2023-12-14 05:06:39 +08:00
|
|
|
Prompt::new("{% for field in fields %}{{field.vaelu}} {% endfor %}".into()),
|
2023-12-13 04:19:48 +08:00
|
|
|
Err(NewPromptError {
|
|
|
|
kind: NewPromptErrorKind::InvalidFieldsInTemplate(_),
|
|
|
|
fault: FaultSource::User
|
|
|
|
})
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|