Skip to content

Commit 76444fa

Browse files
authored
feat(config): add system prompt sections and DeepSeek base_url (#391)
This change introduces `system_prompt_sections` to `AssistantConfig`, allowing users to define system prompts as a list of tagged and ordered sections. This improves the modularity and mergeability of system prompts across different configuration files. Additionally, the DeepSeek provider now supports a `base_url` setting to allow pointing to custom API endpoints or local proxies. The `AppConfig` has been updated to make `inherit` and `config_load_paths` optional fields, preventing unnecessary default values from appearing in serialized configurations. Extensive documentation comments were also added to most configuration structs and fields to improve schema generation. --------- Signed-off-by: Jean Mertz <git@jeanmertz.com>
1 parent 1e3aec6 commit 76444fa

File tree

110 files changed

+590
-129
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+590
-129
lines changed

crates/jp_config/src/assignment.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,26 @@ impl KvAssignment {
490490
self.try_f32().map(Some)
491491
}
492492

493+
/// Try to parse the value as a signed 32-bit integer.
494+
pub(crate) fn try_i32(self) -> Result<i32, KvAssignmentError> {
495+
let Self { key, value, .. } = self;
496+
497+
match value {
498+
#[expect(clippy::cast_possible_truncation)]
499+
KvValue::Json(Value::Number(v)) if v.is_i64() => Ok(v.as_i64().expect("is i64") as i32),
500+
KvValue::Json(_) => type_error(&key, &value, &["number", "string"]),
501+
KvValue::String(v) => Ok(v
502+
.parse()
503+
.map_err(|err| KvAssignmentError::new(key.full_path.clone(), err))?),
504+
}
505+
}
506+
507+
/// Convenience method for [`Self::try_i32`] that wraps the `Ok` value into
508+
/// `Some`.
509+
pub(crate) fn try_some_i32(self) -> Result<Option<i32>, KvAssignmentError> {
510+
self.try_i32().map(Some)
511+
}
512+
493513
/// Try to parse the value as a JSON array of partial configs, and set or
494514
/// merge the elements.
495515
pub(crate) fn try_vec<T>(

crates/jp_config/src/assistant.rs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//! improved performance.
77
88
pub mod instructions;
9+
pub mod sections;
910
pub mod tool_choice;
1011

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

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

47+
/// A list of system prompt sections for the assistant.
48+
#[setting(nested, default = default_sections, merge = vec_with_strategy)]
49+
pub system_prompt_sections: MergeableVec<SectionConfig>,
50+
4051
/// A list of instructions for the assistant.
52+
///
53+
/// Instructions are similar to system prompts but are organized into a list
54+
/// of titled sections. This allows for better organization and easier
55+
/// overriding or extending of specific instructions when merging multiple
56+
/// configurations.
4157
#[setting(nested, default = default_instructions, merge = vec_with_strategy)]
4258
pub instructions: MergeableVec<InstructionsConfig>,
4359

@@ -57,6 +73,9 @@ impl AssignKeyValue for PartialAssistantConfig {
5773
"name" => self.name = kv.try_some_string()?,
5874
"system_prompt" => self.system_prompt = kv.try_some_object_or_from_str()?,
5975
_ if kv.p("instructions") => kv.try_vec_of_nested(self.instructions.as_mut())?,
76+
_ if kv.p("system_prompt_sections") => {
77+
kv.try_vec_of_nested(self.system_prompt_sections.as_mut())?;
78+
}
6079
"tool_choice" => self.tool_choice = kv.try_some_from_str()?,
6180
_ if kv.p("model") => self.model.assign(kv)?,
6281
_ => return missing_key(&kv),
@@ -71,13 +90,17 @@ impl PartialConfigDelta for PartialAssistantConfig {
7190
Self {
7291
name: delta_opt(self.name.as_ref(), next.name),
7392
system_prompt: delta_opt_partial(self.system_prompt.as_ref(), next.system_prompt),
74-
instructions: {
75-
next.instructions
76-
.into_iter()
77-
.filter(|v| !self.instructions.contains(v))
78-
.collect::<Vec<_>>()
79-
.into()
80-
},
93+
instructions: next
94+
.instructions
95+
.into_iter()
96+
.filter(|v| !self.instructions.contains(v))
97+
.collect::<Vec<_>>()
98+
.into(),
99+
system_prompt_sections: next
100+
.system_prompt_sections
101+
.into_iter()
102+
.filter(|v| !self.system_prompt_sections.contains(v))
103+
.collect(),
81104
tool_choice: delta_opt(self.tool_choice.as_ref(), next.tool_choice),
82105
model: self.model.delta(next.model),
83106
}
@@ -92,6 +115,7 @@ impl ToPartial for AssistantConfig {
92115
name: partial_opts(self.name.as_ref(), defaults.name),
93116
system_prompt: partial_opt_config(self.system_prompt.as_ref(), defaults.system_prompt),
94117
instructions: self.instructions.to_partial(),
118+
system_prompt_sections: self.system_prompt_sections.to_partial(),
95119
tool_choice: partial_opt(&self.tool_choice, defaults.tool_choice),
96120
model: self.model.to_partial(),
97121
}
@@ -123,6 +147,12 @@ fn default_instructions(_: &()) -> TransformResult<MergeableVec<PartialInstructi
123147
}))
124148
}
125149

150+
/// The default instructions for the assistant.
151+
#[expect(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
152+
const fn default_sections(_: &()) -> TransformResult<MergeableVec<PartialSectionConfig>> {
153+
Ok(MergeableVec::Vec(vec![]))
154+
}
155+
126156
/// The default system prompt for the assistant.
127157
#[expect(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
128158
fn default_system_prompt(_: &()) -> TransformResult<Option<PartialMergeableString>> {

crates/jp_config/src/assistant/instructions.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@ use crate::{
1919
#[config(default, rename_all = "snake_case")]
2020
pub struct InstructionsConfig {
2121
/// The title of the instructions.
22+
///
23+
/// This is used to organize instructions into sections.
2224
#[serde(skip_serializing_if = "Option::is_none")]
2325
pub title: Option<String>,
2426

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

@@ -37,9 +41,13 @@ pub struct InstructionsConfig {
3741
pub position: isize,
3842

3943
/// The list of instructions.
44+
///
45+
/// Each item is a separate instruction.
4046
pub items: Vec<String>,
4147

4248
/// A list of examples to go with the instructions.
49+
///
50+
/// Examples are used to demonstrate how to follow the instructions.
4351
#[setting(nested)]
4452
pub examples: Vec<ExampleConfig>,
4553
}
@@ -253,12 +261,18 @@ impl FromStr for PartialExampleConfig {
253261
#[config(rename_all = "snake_case")]
254262
pub struct ContrastConfig {
255263
/// The good example.
264+
///
265+
/// This is an example of how to follow the instruction.
256266
pub good: String,
257267

258268
/// The bad example.
269+
///
270+
/// This is an example of how NOT to follow the instruction.
259271
pub bad: String,
260272

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

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//! System prompt sections.
2+
3+
use std::str::FromStr;
4+
5+
use schematic::Config;
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::{
9+
BoxedError,
10+
assignment::{AssignKeyValue, AssignResult, KvAssignment, missing_key},
11+
delta::{PartialConfigDelta, delta_opt},
12+
partial::{ToPartial, partial_opt, partial_opts},
13+
};
14+
15+
/// Command configuration, either as a string or a complete configuration.
16+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config)]
17+
#[config(rename_all = "snake_case", serde(untagged))]
18+
#[serde(untagged)]
19+
pub enum SectionConfigOrString {
20+
/// A single string, which is interpreted as the full content of the
21+
/// section.
22+
String(String),
23+
24+
/// A complete section configuration.
25+
#[setting(nested)]
26+
Config(SectionConfig),
27+
}
28+
29+
impl AssignKeyValue for PartialSectionConfigOrString {
30+
fn assign(&mut self, kv: KvAssignment) -> AssignResult {
31+
match kv.key_string().as_str() {
32+
"" => *self = kv.try_object_or_from_str()?,
33+
_ => match self {
34+
Self::String(_) => return missing_key(&kv),
35+
Self::Config(config) => config.assign(kv)?,
36+
},
37+
}
38+
39+
Ok(())
40+
}
41+
}
42+
43+
impl PartialConfigDelta for PartialSectionConfigOrString {
44+
fn delta(&self, next: Self) -> Self {
45+
match (self, next) {
46+
(Self::Config(prev), Self::Config(next)) => Self::Config(prev.delta(next)),
47+
(_, next) => next,
48+
}
49+
}
50+
}
51+
52+
impl ToPartial for SectionConfigOrString {
53+
fn to_partial(&self) -> Self::Partial {
54+
match self {
55+
Self::String(v) => Self::Partial::String(v.to_owned()),
56+
Self::Config(v) => Self::Partial::Config(v.to_partial()),
57+
}
58+
}
59+
}
60+
61+
impl FromStr for PartialSectionConfigOrString {
62+
type Err = BoxedError;
63+
64+
fn from_str(s: &str) -> Result<Self, Self::Err> {
65+
Ok(Self::String(s.to_owned()))
66+
}
67+
}
68+
69+
/// A list of sections for a system prompt.
70+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Config)]
71+
#[config(default, rename_all = "snake_case")]
72+
pub struct SectionConfig {
73+
/// The content of the section.
74+
pub content: String,
75+
76+
/// Optional tag surrounding the section.
77+
#[serde(skip_serializing_if = "Option::is_none")]
78+
pub tag: Option<String>,
79+
80+
/// The position of the section.
81+
///
82+
/// A lower position will be shown first. This is useful when merging
83+
/// multiple sections, and you want to make sure the most important
84+
/// sections are shown first.
85+
///
86+
/// Defaults to `0`.
87+
#[setting(default = 0)]
88+
pub position: i32,
89+
}
90+
91+
impl AssignKeyValue for PartialSectionConfig {
92+
fn assign(&mut self, kv: KvAssignment) -> AssignResult {
93+
match kv.key_string().as_str() {
94+
"" => *self = kv.try_object_or_from_str()?,
95+
"tag" => self.tag = kv.try_some_string()?,
96+
"content" => self.content = kv.try_some_string()?,
97+
"position" => self.position = kv.try_some_i32()?,
98+
_ => return missing_key(&kv),
99+
}
100+
101+
Ok(())
102+
}
103+
}
104+
105+
impl ToPartial for SectionConfig {
106+
fn to_partial(&self) -> Self::Partial {
107+
let defaults = Self::Partial::default();
108+
109+
Self::Partial {
110+
tag: partial_opts(self.tag.as_ref(), defaults.tag),
111+
content: partial_opt(&self.content, defaults.content),
112+
position: partial_opt(&self.position, defaults.position),
113+
}
114+
}
115+
}
116+
117+
impl PartialConfigDelta for PartialSectionConfig {
118+
fn delta(&self, next: Self) -> Self {
119+
Self {
120+
tag: delta_opt(self.tag.as_ref(), next.tag),
121+
content: delta_opt(self.content.as_ref(), next.content),
122+
position: delta_opt(self.position.as_ref(), next.position),
123+
}
124+
}
125+
}
126+
127+
impl SectionConfig {
128+
/// Add a tag to the section.
129+
#[must_use]
130+
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
131+
self.tag = Some(tag.into());
132+
self
133+
}
134+
135+
/// Add content to the section.
136+
#[must_use]
137+
pub fn with_content(mut self, content: impl Into<String>) -> Self {
138+
self.content = content.into();
139+
self
140+
}
141+
}
142+
143+
impl FromStr for PartialSectionConfig {
144+
type Err = BoxedError;
145+
146+
fn from_str(s: &str) -> Result<Self, Self::Err> {
147+
Ok(Self {
148+
content: Some(s.to_owned()),
149+
..Default::default()
150+
})
151+
}
152+
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::*;
157+
158+
#[test]
159+
fn test_instructions_assign() {
160+
let mut p = PartialSectionConfig::default();
161+
162+
let kv = KvAssignment::try_from_cli("tag", "foo").unwrap();
163+
p.assign(kv).unwrap();
164+
assert_eq!(p.tag, Some("foo".into()));
165+
166+
let kv = KvAssignment::try_from_cli("content", "bar").unwrap();
167+
p.assign(kv).unwrap();
168+
assert_eq!(p.content, Some("bar".into()));
169+
170+
let kv = KvAssignment::try_from_cli("position", "1").unwrap();
171+
p.assign(kv).unwrap();
172+
assert_eq!(p.position, Some(1));
173+
}
174+
}

crates/jp_config/src/conversation.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@ use crate::{
2222
#[config(rename_all = "snake_case")]
2323
pub struct ConversationConfig {
2424
/// Title configuration.
25+
///
26+
/// This section configures how conversation titles are generated.
2527
#[setting(nested)]
2628
pub title: TitleConfig,
2729

2830
/// Tool configuration.
31+
///
32+
/// This section configures tool usage within conversations.
2933
#[setting(nested)]
3034
pub tools: ToolsConfig,
3135

3236
/// Attachment configuration.
37+
///
38+
/// This section defines attachments (files, resources) that are added to
39+
/// conversations.
3340
#[setting(nested, merge = schematic::merge::append_vec)]
3441
pub attachments: Vec<AttachmentConfig>,
3542
}

crates/jp_config/src/conversation/attachment.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
partial::{ToPartial, partial_opt},
1515
};
1616

17-
/// Reasoning configuration.
17+
/// Attachment configuration.
1818
#[derive(Debug, Clone, PartialEq, Config)]
1919
#[config(serde(untagged))]
2020
pub enum AttachmentConfig {
@@ -108,10 +108,14 @@ impl From<Url> for PartialAttachmentConfig {
108108
#[derive(Debug, Clone, PartialEq, Config)]
109109
pub struct AttachmentObjectConfig {
110110
/// The type of the attachment.
111+
///
112+
/// e.g. `file`, `http`, etc.
111113
#[setting(required, rename = "type")]
112114
pub kind: String,
113115

114116
/// The url path of the attachment.
117+
///
118+
/// The path part of the URL.
115119
#[setting(required)]
116120
pub path: String,
117121

0 commit comments

Comments
 (0)