Skip to content
Open
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
17 changes: 15 additions & 2 deletions crates/contrib/schematic_macros/src/common/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ fn generate_enum_schema(
.iter()
.all(|v| matches!(v.value.fields, Fields::Unit));
let mut default_index = None;
let mut expanded_index = None;

let variants_types = variants
.iter()
Expand All @@ -192,6 +193,10 @@ fn generate_enum_schema(
default_index = Some(i);
}

if v.args.expanded {
expanded_index = Some(i);
}

if v.is_excluded() {
None
} else {
Expand All @@ -200,6 +205,10 @@ fn generate_enum_schema(
})
.collect::<Vec<_>>();

let expanded = expanded_index.map_or_else(
|| quote! {},
|index| quote! { union = union.with_expanded_index(#index); },
);
let default_index = map_option_argument_quote(default_index);

if is_all_unit_enum {
Expand All @@ -217,12 +226,16 @@ fn generate_enum_schema(
quote! {
#deprecated
#description
schema.union(UnionType::from_schemas(

let mut union = UnionType::from_schemas(
[
#(#variants_types),*
],
#default_index,
))
);
#expanded

schema.union(union)
}
}
}
9 changes: 9 additions & 0 deletions crates/contrib/schematic_macros/src/common/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ pub struct MacroArgs {
pub serde: SerdeMeta,
pub no_deserialize_derive: bool,

// Declare input shapes the derive cannot infer, for a type whose
// `Deserialize` accepts more than its fields describe (a struct that also
// deserializes from a bool, say).
//
// Names a `fn(&mut SchemaBuilder) -> Vec<Schema>`. The derived struct schema
// is unioned with the returned variants, so field names and doc comments
// are still described alongside the extra shapes.
pub schema_union_with: Option<ExprPath>,

// Quick hack to avoid `is_untagged` to generate a custom Deserialize impl,
// which ignores any custom serde tags on an enum.
pub skip_custom_untagged_enum_deserialize_impl: bool,
Expand Down
7 changes: 7 additions & 0 deletions crates/contrib/schematic_macros/src/common/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ pub struct VariantArgs {
pub nested: bool,
pub required: bool,
pub empty: bool,

/// Mark this variant as the form the enum's other variants abbreviate.
///
/// Populates `UnionType::expanded_index`, so a schema consumer resolving
/// nested keys knows to follow this variant's fields.
pub expanded: bool,

#[darling(with = preserve_str_literal, map = "Some")]
pub is_empty: Option<Expr>,

Expand Down
25 changes: 24 additions & 1 deletion crates/contrib/schematic_macros/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,29 @@ fn emit_schematic_impls(args: &SchematicImplArgs<'_>) -> TokenStream {
let partial_schema_name = partial_name.to_string();
let partial_schema_impl = crate::common::Container::generate_partial_schema(name, cfg.generics);

// `schema_union_with` unions the derived schema with caller-supplied
// variants, for a type that deserializes from more shapes than its fields
// describe. Using it asserts the extra shapes are shorthands for the fields,
// which is what lets the union name an expanded form below. The partial's
// impl delegates here, so it sees the union too.
let build_body = match cfg.args.schema_union_with.as_ref() {
None => quote! {
#schema_impl
},
Some(path) => quote! {
let described = { #schema_impl };
let mut variants = #path(&mut schema);

// The derived schema goes last and is marked as the expanded form:
// the caller-supplied variants are shorthand spellings of it, so a
// consumer resolving keys should follow the derived fields.
let expanded = variants.len();
variants.push(described);

schema.union(UnionType::new_any(variants).with_expanded_index(expanded))
},
};

quote! {
#[automatically_derived]
impl #impl_generics schematic::Schematic for #name #ty_generics #schematic_where {
Expand All @@ -164,7 +187,7 @@ fn emit_schematic_impls(args: &SchematicImplArgs<'_>) -> TokenStream {
fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema {
use schematic::schema::*;

#schema_impl
#build_body
}
}

Expand Down
34 changes: 34 additions & 0 deletions crates/contrib/schematic_types/src/unions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ pub struct UnionType {
)]
pub default_index: Option<usize>,

/// Index of the variant the other variants are shorthand spellings of.
///
/// Set when a union describes one value written several ways, so a consumer
/// can find the form that names the value's parts: a tool's `enable`
/// accepts `true` or `{ state, allow_toggle }`, and the table is the
/// expanded form of the bool.
///
/// Left unset when the variants are genuinely different values rather than
/// spellings of one.
/// A model id is either an id or an alias resolved through a lookup, and
/// neither expands into the other.
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub expanded_index: Option<usize>,

pub partial: bool,

pub operator: UnionOperator,
Expand Down Expand Up @@ -58,6 +75,23 @@ impl UnionType {
}
}

/// Mark which variant the others are shorthand spellings of.
///
/// See [`Self::expanded_index`].
#[must_use]
pub fn with_expanded_index(mut self, index: usize) -> Self {
self.expanded_index = Some(index);
self
}

/// The variant the others are shorthand spellings of, if any.
#[must_use]
pub fn expanded_variant(&self) -> Option<&Schema> {
self.expanded_index
.and_then(|index| self.variants_types.get(index))
.map(AsRef::as_ref)
}

#[must_use]
pub fn has_null(&self) -> bool {
self.variants_types.iter().any(|schema| schema.ty.is_null())
Expand Down
4 changes: 2 additions & 2 deletions crates/jp_cli/src/cmd/conversation/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ async fn generate_titles(
tool_choice: jp_config::assistant::tool_choice::ToolChoice::default(),
};

let retry_config =
RetryConfig::default().with_max_response_bytes(config.assistant.request.max_response_bytes);
let retry_config = RetryConfig::default()
.with_max_response_bytes(config.assistant.request.max_response_bytes.bytes());
let llm_events =
collect_with_retry(provider.as_ref(), &model_details, query, &retry_config).await?;

Expand Down
4 changes: 2 additions & 2 deletions crates/jp_cli/src/cmd/conversation/summarize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub async fn generate_summary(
stream,
instructions,
&user_message,
app_cfg.assistant.request.max_response_bytes,
app_cfg.assistant.request.max_response_bytes.bytes(),
)
.await
}
Expand All @@ -121,7 +121,7 @@ async fn summarize_stream(
mut stream: ConversationStream,
instructions: &str,
user_message: &str,
max_response_bytes: u32,
max_response_bytes: Option<u64>,
) -> Result<String> {
let retry_config = RetryConfig::default().with_max_response_bytes(max_response_bytes);

Expand Down
6 changes: 3 additions & 3 deletions crates/jp_cli/src/cmd/conversation/summarize_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ async fn summarize_with(
batches: Vec<Vec<Event>>,
stream: ConversationStream,
) -> super::Result<String> {
summarize_with_ceiling(batches, stream, 1_048_576).await
summarize_with_ceiling(batches, stream, Some(1_048_576)).await
}

async fn summarize_with_ceiling(
batches: Vec<Vec<Event>>,
stream: ConversationStream,
max_response_bytes: u32,
max_response_bytes: Option<u64>,
) -> super::Result<String> {
let provider = MockProvider::with_batches(batches);
let model_id = test_model_id();
Expand Down Expand Up @@ -90,7 +90,7 @@ async fn summarize_applies_the_configured_output_ceiling() {
FinishReason::Completed,
)];

let error = summarize_with_ceiling(batches, range_stream(&["sig"]), 25)
let error = summarize_with_ceiling(batches, range_stream(&["sig"]), Some(25))
.await
.expect_err("the summary must stop at the configured ceiling");

Expand Down
4 changes: 2 additions & 2 deletions crates/jp_cli/src/cmd/query/interrupt/signals_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync::Arc;
use assert_matches::assert_matches;
use jp_config::{
AppConfig,
assistant::request::{CachePolicy, RequestConfig},
assistant::request::{CachePolicy, MaxResponseBytes, RequestConfig},
};
use jp_conversation::{
ConversationEvent, ConversationStream,
Expand Down Expand Up @@ -46,7 +46,7 @@ fn make_retry_state(max_retries: u32) -> StreamRetryState {
base_backoff_ms: 1,
max_backoff_secs: 1,
stream_idle_timeout_secs: 120,
max_response_bytes: 1_048_576,
max_response_bytes: MaxResponseBytes::default(),
cache: CachePolicy::default(),
};
StreamRetryState::new(config, false)
Expand Down
8 changes: 4 additions & 4 deletions crates/jp_cli/src/cmd/query/stream/retry_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};

use jp_config::{
AppConfig,
assistant::request::{CachePolicy, RequestConfig},
assistant::request::{CachePolicy, MaxResponseBytes, RequestConfig},
};
use jp_conversation::{
Conversation,
Expand All @@ -21,7 +21,7 @@ fn make_retry_state(max_retries: u32) -> StreamRetryState {
base_backoff_ms: 1, // 1ms for fast tests
max_backoff_secs: 1,
stream_idle_timeout_secs: 120,
max_response_bytes: 1_048_576,
max_response_bytes: MaxResponseBytes::default(),
cache: CachePolicy::default(),
};
StreamRetryState::new(config, false)
Expand Down Expand Up @@ -86,7 +86,7 @@ fn backoff_uses_retry_after_when_present() {
base_backoff_ms: 1,
max_backoff_secs: 120,
stream_idle_timeout_secs: 120,
max_response_bytes: 1_048_576,
max_response_bytes: MaxResponseBytes::default(),
cache: CachePolicy::default(),
};
let state = StreamRetryState::new(config, false);
Expand Down Expand Up @@ -343,7 +343,7 @@ async fn interrupt_during_backoff_cuts_wait_short() {
base_backoff_ms: 1,
max_backoff_secs: 120,
stream_idle_timeout_secs: 120,
max_response_bytes: 1_048_576,
max_response_bytes: MaxResponseBytes::default(),
cache: CachePolicy::default(),
};
let mut retry_state = StreamRetryState::new(config, false);
Expand Down
5 changes: 3 additions & 2 deletions crates/jp_cli/src/cmd/query/tool/inquiry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ pub struct InquiryConfig {
pub sections: Vec<SectionConfig>,

/// Output ceiling for the inquiry request, from
/// `assistant.request.max_response_bytes`.
pub max_response_bytes: u32,
/// `conversation.inquiry.assistant.request.max_response_bytes`.
/// `None` leaves the response unbounded.
pub max_response_bytes: Option<u64>,
}

/// Resolves inquiries by making structured output calls to an LLM provider.
Expand Down
6 changes: 3 additions & 3 deletions crates/jp_cli/src/cmd/query/tool/inquiry_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn test_inquiry_config(provider: MockProvider) -> InquiryConfig {
model: test_model(),
system_prompt: None,
sections: vec![],
max_response_bytes: 1_048_576,
max_response_bytes: Some(1_048_576),
}
}

Expand Down Expand Up @@ -324,7 +324,7 @@ async fn llm_backend_uses_per_question_override() {
model: test_model(),
system_prompt: Some("Override prompt.".into()),
sections: vec![],
max_response_bytes: 1_048_576,
max_response_bytes: Some(1_048_576),
};

let overrides = IndexMap::from([(("test_tool".into(), "confirm".into()), override_config)]);
Expand Down Expand Up @@ -559,7 +559,7 @@ async fn dedicated_model_backend_returns_answer() {
}),
system_prompt: Some("Answer concisely.".to_string()),
sections: vec![],
max_response_bytes: 1_048_576,
max_response_bytes: Some(1_048_576),
};

let backend = LlmInquiryBackend::new(config, IndexMap::new(), vec![], vec![]);
Expand Down
Loading
Loading