Skip to content

Commit c88413b

Browse files
committed
JSON schema for OpenAI and fault tolerant in parsing datetime.
1 parent 7a54759 commit c88413b

File tree

4 files changed

+57
-10
lines changed

4 files changed

+57
-10
lines changed

src/base/json_schema.rs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ pub struct ToJsonSchemaOptions {
88
/// Use union type (with `null`) for optional fields instead.
99
/// Models like OpenAI will reject the schema if a field is not required.
1010
pub fields_always_required: bool,
11+
12+
/// If true, the JSON schema supports the `format` keyword.
13+
pub supports_format: bool,
1114
}
1215

1316
pub trait ToJsonSchema {
@@ -54,27 +57,46 @@ impl ToJsonSchema for schema::BasicValueType {
5457
}
5558
schema::BasicValueType::Uuid => {
5659
schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
57-
schema.format = Some("uuid".to_string());
60+
if options.supports_format {
61+
schema.format = Some("uuid".to_string());
62+
} else {
63+
schema.metadata.get_or_insert_default().description =
64+
Some("A UUID, e.g. 123e4567-e89b-12d3-a456-426614174000".to_string());
65+
}
5866
}
5967
schema::BasicValueType::Date => {
6068
schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
61-
schema.format = Some("date".to_string());
69+
if options.supports_format {
70+
schema.format = Some("date".to_string());
71+
} else {
72+
schema.metadata.get_or_insert_default().description =
73+
Some("A date, e.g. 2025-03-27".to_string());
74+
}
6275
}
6376
schema::BasicValueType::Time => {
6477
schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
65-
schema.format = Some("time".to_string());
78+
if options.supports_format {
79+
schema.format = Some("time".to_string());
80+
} else {
81+
schema.metadata.get_or_insert_default().description =
82+
Some("A time, e.g. 13:32:12".to_string());
83+
}
6684
}
6785
schema::BasicValueType::LocalDateTime => {
6886
schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
69-
schema.format = Some("date-time".to_string());
87+
if options.supports_format {
88+
schema.format = Some("date-time".to_string());
89+
}
7090
schema.metadata.get_or_insert_default().description =
71-
Some("Date time without timezone offset, YYYY-MM-DDThh:mm:ss".to_string());
91+
Some("Date time without timezone offset, e.g. 2025-03-27T13:32:12".to_string());
7292
}
7393
schema::BasicValueType::OffsetDateTime => {
7494
schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
75-
schema.format = Some("date-time".to_string());
95+
if options.supports_format {
96+
schema.format = Some("date-time".to_string());
97+
}
7698
schema.metadata.get_or_insert_default().description =
77-
Some("Date time with timezone offset in RFC3339".to_string());
99+
Some("Date time with timezone offset in RFC3339, e.g. 2025-03-27T13:32:12Z, 2025-03-27T07:32:12.313-06:00".to_string());
78100
}
79101
schema::BasicValueType::Json => {
80102
// Can be any value. No type constraint.

src/base/value.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use crate::{api_bail, api_error};
22

33
use super::schema::*;
4-
use anyhow::Result;
4+
use anyhow::{Context, Result};
55
use base64::prelude::*;
6+
use chrono::Offset;
7+
use log::warn;
68
use serde::{
79
de::{SeqAccess, Visitor},
810
ser::{SerializeMap, SerializeSeq, SerializeTuple},
911
Deserialize, Serialize,
1012
};
11-
use std::{collections::BTreeMap, ops::Deref, sync::Arc};
13+
use std::{collections::BTreeMap, ops::Deref, str::FromStr, sync::Arc};
1214

1315
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1416
pub struct RangeValue {
@@ -866,7 +868,20 @@ impl BasicValue {
866868
BasicValue::LocalDateTime(v.parse()?)
867869
}
868870
(serde_json::Value::String(v), BasicValueType::OffsetDateTime) => {
869-
BasicValue::OffsetDateTime(chrono::DateTime::parse_from_rfc3339(&v)?)
871+
match chrono::DateTime::parse_from_rfc3339(&v) {
872+
Ok(dt) => BasicValue::OffsetDateTime(dt),
873+
Err(e) => {
874+
if let Ok(dt) = v.parse::<chrono::NaiveDateTime>() {
875+
warn!("Datetime without timezone offset, assuming UTC");
876+
BasicValue::OffsetDateTime(chrono::DateTime::from_naive_utc_and_offset(
877+
dt,
878+
chrono::Utc.fix(),
879+
))
880+
} else {
881+
Err(e)?
882+
}
883+
}
884+
}
870885
}
871886
(v, BasicValueType::Json) => BasicValue::Json(Arc::from(v)),
872887
(

src/llm/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,15 @@ pub trait LlmGenerationClient: Send + Sync {
5454
false
5555
}
5656

57+
/// If true, the LLM supports the `format` keyword in the JSON schema.
58+
fn json_schema_supports_format(&self) -> bool {
59+
true
60+
}
61+
5762
fn to_json_schema_options(&self) -> ToJsonSchemaOptions {
5863
ToJsonSchemaOptions {
5964
fields_always_required: self.json_schema_fields_always_required(),
65+
supports_format: self.json_schema_supports_format(),
6066
}
6167
}
6268
}

src/llm/openai.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,8 @@ impl LlmGenerationClient for Client {
101101
fn json_schema_fields_always_required(&self) -> bool {
102102
true
103103
}
104+
105+
fn json_schema_supports_format(&self) -> bool {
106+
false
107+
}
104108
}

0 commit comments

Comments
 (0)