Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/jp_config/src/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,26 @@ impl KvAssignment {
self.try_f32().map(Some)
}

/// Try to parse the value as a signed 32-bit integer.
pub(crate) fn try_i32(self) -> Result<i32, KvAssignmentError> {
let Self { key, value, .. } = self;

match value {
#[expect(clippy::cast_possible_truncation)]
KvValue::Json(Value::Number(v)) if v.is_i64() => Ok(v.as_i64().expect("is i64") as i32),
KvValue::Json(_) => type_error(&key, &value, &["number", "string"]),
KvValue::String(v) => Ok(v
.parse()
.map_err(|err| KvAssignmentError::new(key.full_path.clone(), err))?),
}
}

/// Convenience method for [`Self::try_i32`] that wraps the `Ok` value into
/// `Some`.
pub(crate) fn try_some_i32(self) -> Result<Option<i32>, KvAssignmentError> {
self.try_i32().map(Some)
}

/// Try to parse the value as a JSON array of partial configs, and set or
/// merge the elements.
pub(crate) fn try_vec<T>(
Expand Down
46 changes: 38 additions & 8 deletions crates/jp_config/src/assistant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! improved performance.

pub mod instructions;
pub mod sections;
pub mod tool_choice;

use schematic::{Config, TransformResult};
Expand All @@ -14,6 +15,7 @@ use crate::{
assignment::{AssignKeyValue, AssignResult, KvAssignment, missing_key},
assistant::{
instructions::{InstructionsConfig, PartialInstructionsConfig},
sections::{PartialSectionConfig, SectionConfig},
tool_choice::ToolChoice,
},
delta::{PartialConfigDelta, delta_opt, delta_opt_partial},
Expand All @@ -30,14 +32,28 @@ use crate::{
#[derive(Debug, Clone, PartialEq, Config)]
#[config(rename_all = "snake_case")]
pub struct AssistantConfig {
/// Optional name of the assistant.
/// The name of the assistant.
///
/// This is purely cosmetic and currently not used in the UI.
pub name: Option<String>,

/// The system prompt to use for the assistant.
///
/// The system prompt is the initial instruction given to the assistant to
/// define its behavior, tone, and role.
#[setting(nested, default = default_system_prompt, merge = string_with_strategy)]
pub system_prompt: Option<MergeableString>,

/// A list of system prompt sections for the assistant.
#[setting(nested, default = default_sections, merge = vec_with_strategy)]
pub system_prompt_sections: MergeableVec<SectionConfig>,

/// A list of instructions for the assistant.
///
/// Instructions are similar to system prompts but are organized into a list
/// of titled sections. This allows for better organization and easier
/// overriding or extending of specific instructions when merging multiple
/// configurations.
#[setting(nested, default = default_instructions, merge = vec_with_strategy)]
pub instructions: MergeableVec<InstructionsConfig>,

Expand All @@ -57,6 +73,9 @@ impl AssignKeyValue for PartialAssistantConfig {
"name" => self.name = kv.try_some_string()?,
"system_prompt" => self.system_prompt = kv.try_some_object_or_from_str()?,
_ if kv.p("instructions") => kv.try_vec_of_nested(self.instructions.as_mut())?,
_ if kv.p("system_prompt_sections") => {
kv.try_vec_of_nested(self.system_prompt_sections.as_mut())?;
}
"tool_choice" => self.tool_choice = kv.try_some_from_str()?,
_ if kv.p("model") => self.model.assign(kv)?,
_ => return missing_key(&kv),
Expand All @@ -71,13 +90,17 @@ impl PartialConfigDelta for PartialAssistantConfig {
Self {
name: delta_opt(self.name.as_ref(), next.name),
system_prompt: delta_opt_partial(self.system_prompt.as_ref(), next.system_prompt),
instructions: {
next.instructions
.into_iter()
.filter(|v| !self.instructions.contains(v))
.collect::<Vec<_>>()
.into()
},
instructions: next
.instructions
.into_iter()
.filter(|v| !self.instructions.contains(v))
.collect::<Vec<_>>()
.into(),
system_prompt_sections: next
.system_prompt_sections
.into_iter()
.filter(|v| !self.system_prompt_sections.contains(v))
.collect(),
tool_choice: delta_opt(self.tool_choice.as_ref(), next.tool_choice),
model: self.model.delta(next.model),
}
Expand All @@ -92,6 +115,7 @@ impl ToPartial for AssistantConfig {
name: partial_opts(self.name.as_ref(), defaults.name),
system_prompt: partial_opt_config(self.system_prompt.as_ref(), defaults.system_prompt),
instructions: self.instructions.to_partial(),
system_prompt_sections: self.system_prompt_sections.to_partial(),
tool_choice: partial_opt(&self.tool_choice, defaults.tool_choice),
model: self.model.to_partial(),
}
Expand Down Expand Up @@ -123,6 +147,12 @@ fn default_instructions(_: &()) -> TransformResult<MergeableVec<PartialInstructi
}))
}

/// The default instructions for the assistant.
#[expect(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
const fn default_sections(_: &()) -> TransformResult<MergeableVec<PartialSectionConfig>> {
Ok(MergeableVec::Vec(vec![]))
}

/// The default system prompt for the assistant.
#[expect(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
fn default_system_prompt(_: &()) -> TransformResult<Option<PartialMergeableString>> {
Expand Down
14 changes: 14 additions & 0 deletions crates/jp_config/src/assistant/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ use crate::{
#[config(default, rename_all = "snake_case")]
pub struct InstructionsConfig {
/// The title of the instructions.
///
/// This is used to organize instructions into sections.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,

/// An optional description of the instructions.
///
/// This is used to provide more context about the instructions.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,

Expand All @@ -37,9 +41,13 @@ pub struct InstructionsConfig {
pub position: isize,

/// The list of instructions.
///
/// Each item is a separate instruction.
pub items: Vec<String>,

/// A list of examples to go with the instructions.
///
/// Examples are used to demonstrate how to follow the instructions.
#[setting(nested)]
pub examples: Vec<ExampleConfig>,
}
Expand Down Expand Up @@ -253,12 +261,18 @@ impl FromStr for PartialExampleConfig {
#[config(rename_all = "snake_case")]
pub struct ContrastConfig {
/// The good example.
///
/// This is an example of how to follow the instruction.
pub good: String,

/// The bad example.
///
/// This is an example of how NOT to follow the instruction.
pub bad: String,

/// Why is the good example better than the bad example?
///
/// This is optional, but recommended to provide more context.
pub reason: Option<String>,
}

Expand Down
174 changes: 174 additions & 0 deletions crates/jp_config/src/assistant/sections.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! System prompt sections.

use std::str::FromStr;

use schematic::Config;
use serde::{Deserialize, Serialize};

use crate::{
BoxedError,
assignment::{AssignKeyValue, AssignResult, KvAssignment, missing_key},
delta::{PartialConfigDelta, delta_opt},
partial::{ToPartial, partial_opt, partial_opts},
};

/// Command configuration, either as a string or a complete configuration.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config)]
#[config(rename_all = "snake_case", serde(untagged))]
#[serde(untagged)]
pub enum SectionConfigOrString {
/// A single string, which is interpreted as the full content of the
/// section.
String(String),

/// A complete section configuration.
#[setting(nested)]
Config(SectionConfig),
}

impl AssignKeyValue for PartialSectionConfigOrString {
fn assign(&mut self, kv: KvAssignment) -> AssignResult {
match kv.key_string().as_str() {
"" => *self = kv.try_object_or_from_str()?,
_ => match self {
Self::String(_) => return missing_key(&kv),
Self::Config(config) => config.assign(kv)?,
},
}

Ok(())
}
}

impl PartialConfigDelta for PartialSectionConfigOrString {
fn delta(&self, next: Self) -> Self {
match (self, next) {
(Self::Config(prev), Self::Config(next)) => Self::Config(prev.delta(next)),
(_, next) => next,
}
}
}

impl ToPartial for SectionConfigOrString {
fn to_partial(&self) -> Self::Partial {
match self {
Self::String(v) => Self::Partial::String(v.to_owned()),
Self::Config(v) => Self::Partial::Config(v.to_partial()),
}
}
}

impl FromStr for PartialSectionConfigOrString {
type Err = BoxedError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::String(s.to_owned()))
}
}

/// A list of sections for a system prompt.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config)]
#[config(default, rename_all = "snake_case")]
pub struct SectionConfig {
/// The content of the section.
pub content: String,

/// Optional tag surrounding the section.
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,

/// The position of the section.
///
/// A lower position will be shown first. This is useful when merging
/// multiple sections, and you want to make sure the most important
/// sections are shown first.
///
/// Defaults to `0`.
#[setting(default = 0)]
pub position: i32,
}

impl AssignKeyValue for PartialSectionConfig {
fn assign(&mut self, kv: KvAssignment) -> AssignResult {
match kv.key_string().as_str() {
"" => *self = kv.try_object_or_from_str()?,
"tag" => self.tag = kv.try_some_string()?,
"content" => self.content = kv.try_some_string()?,
"position" => self.position = kv.try_some_i32()?,
_ => return missing_key(&kv),
}

Ok(())
}
}

impl ToPartial for SectionConfig {
fn to_partial(&self) -> Self::Partial {
let defaults = Self::Partial::default();

Self::Partial {
tag: partial_opts(self.tag.as_ref(), defaults.tag),
content: partial_opt(&self.content, defaults.content),
position: partial_opt(&self.position, defaults.position),
}
}
}

impl PartialConfigDelta for PartialSectionConfig {
fn delta(&self, next: Self) -> Self {
Self {
tag: delta_opt(self.tag.as_ref(), next.tag),
content: delta_opt(self.content.as_ref(), next.content),
position: delta_opt(self.position.as_ref(), next.position),
}
}
}

impl SectionConfig {
/// Add a tag to the section.
#[must_use]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}

/// Add content to the section.
#[must_use]
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
}

impl FromStr for PartialSectionConfig {
type Err = BoxedError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
content: Some(s.to_owned()),
..Default::default()
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_instructions_assign() {
let mut p = PartialSectionConfig::default();

let kv = KvAssignment::try_from_cli("tag", "foo").unwrap();
p.assign(kv).unwrap();
assert_eq!(p.tag, Some("foo".into()));

let kv = KvAssignment::try_from_cli("content", "bar").unwrap();
p.assign(kv).unwrap();
assert_eq!(p.content, Some("bar".into()));

let kv = KvAssignment::try_from_cli("position", "1").unwrap();
p.assign(kv).unwrap();
assert_eq!(p.position, Some(1));
}
}
7 changes: 7 additions & 0 deletions crates/jp_config/src/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ use crate::{
#[config(rename_all = "snake_case")]
pub struct ConversationConfig {
/// Title configuration.
///
/// This section configures how conversation titles are generated.
#[setting(nested)]
pub title: TitleConfig,

/// Tool configuration.
///
/// This section configures tool usage within conversations.
#[setting(nested)]
pub tools: ToolsConfig,

/// Attachment configuration.
///
/// This section defines attachments (files, resources) that are added to
/// conversations.
#[setting(nested, merge = schematic::merge::append_vec)]
pub attachments: Vec<AttachmentConfig>,
}
Expand Down
6 changes: 5 additions & 1 deletion crates/jp_config/src/conversation/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
partial::{ToPartial, partial_opt},
};

/// Reasoning configuration.
/// Attachment configuration.
#[derive(Debug, Clone, PartialEq, Config)]
#[config(serde(untagged))]
pub enum AttachmentConfig {
Expand Down Expand Up @@ -108,10 +108,14 @@ impl From<Url> for PartialAttachmentConfig {
#[derive(Debug, Clone, PartialEq, Config)]
pub struct AttachmentObjectConfig {
/// The type of the attachment.
///
/// e.g. `file`, `http`, etc.
#[setting(required, rename = "type")]
pub kind: String,

/// The url path of the attachment.
///
/// The path part of the URL.
#[setting(required)]
pub path: String,

Expand Down
Loading
Loading