1#![allow(clippy::all)]
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11
12pub use specado_core::*;
13pub use specado_core::{
14 Error, Message, MessageRole, PromptSpec, ResponseConfig, SamplingConfig, StrictMode, Tool,
15 ToolChoice,
16};
17
18pub fn load_prompt_from_path(
23 path: impl AsRef<Path>,
24) -> specado_core::Result<specado_core::PromptSpec> {
25 let path = path.as_ref();
26 let contents = std::fs::read_to_string(path).map_err(|err| {
27 specado_core::Error::Config(format!("Failed to read prompt {}: {}", path.display(), err))
28 })?;
29
30 let ext = path
31 .extension()
32 .and_then(|ext| ext.to_str())
33 .map(|ext| ext.to_ascii_lowercase())
34 .unwrap_or_default();
35
36 if ext == "json" {
37 serde_json::from_str(&contents).map_err(|err| {
38 specado_core::Error::Config(format!("Invalid JSON prompt {}: {}", path.display(), err))
39 })
40 } else {
41 match serde_yaml::from_str(&contents) {
42 Ok(spec) => Ok(spec),
43 Err(yaml_err) if ext == "json" => Err(specado_core::Error::Config(format!(
44 "Invalid JSON prompt {}: {}",
45 path.display(),
46 yaml_err
47 ))),
48 Err(yaml_err) => serde_json::from_str(&contents).map_err(|json_err| {
49 specado_core::Error::Config(format!(
50 "Failed to parse prompt {} as YAML ({}) or JSON ({}).",
51 path.display(),
52 yaml_err,
53 json_err
54 ))
55 }),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct PromptBuilder {
64 pub messages: Vec<Message>,
65 #[serde(default)]
66 pub sampling: Option<SamplingConfig>,
67 #[serde(default, alias = "strict_mode")]
68 pub strict_mode: Option<StrictMode>,
69 #[serde(default)]
70 pub response: Option<ResponseConfig>,
71 #[serde(default)]
72 pub tools: Option<Vec<Tool>>,
73 #[serde(default, alias = "tool_choice")]
74 pub tool_choice: Option<ToolChoice>,
75 pub metadata: Option<HashMap<String, serde_json::Value>>,
76}
77
78pub fn create_prompt(builder: PromptBuilder) -> PromptSpec {
80 PromptSpec {
81 version: "1".to_string(),
82 messages: builder.messages,
83 sampling: builder.sampling.unwrap_or_default(),
84 strict_mode: builder.strict_mode.unwrap_or_default(),
85 response: builder.response.unwrap_or_default(),
86 tools: builder.tools.unwrap_or_default(),
87 tool_choice: builder.tool_choice,
88 metadata: builder.metadata.unwrap_or_default(),
89 }
90}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct SimplePromptOptions {
96 pub message: Option<String>,
97 pub user: Option<String>,
98 pub system: Option<String>,
99 pub temperature: Option<f64>,
100 #[serde(default)]
101 pub sampling: Option<SamplingConfig>,
102 #[serde(alias = "strict_mode")]
103 pub strict_mode: Option<StrictMode>,
104}
105
106pub fn simple_prompt(mut options: SimplePromptOptions) -> specado_core::Result<PromptSpec> {
108 let primary = options
109 .user
110 .take()
111 .or_else(|| options.message.take())
112 .unwrap_or_default();
113
114 if primary.trim().is_empty() {
115 return Err(Error::Config(
116 "simple prompt requires a non-empty user message".to_string(),
117 ));
118 }
119
120 let mut messages = Vec::new();
121 if let Some(system) = options.system.take() {
122 if system.trim().len() > 0 {
123 messages.push(Message {
124 role: MessageRole::System,
125 content: system,
126 });
127 }
128 }
129 messages.push(Message {
130 role: MessageRole::User,
131 content: primary,
132 });
133
134 let mut sampling = options.sampling.unwrap_or_default();
135 if let Some(temperature) = options.temperature {
136 sampling.temperature = Some(temperature);
137 }
138
139 Ok(create_prompt(PromptBuilder {
140 messages,
141 sampling: Some(sampling),
142 strict_mode: options.strict_mode,
143 ..PromptBuilder::default()
144 }))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn simple_prompt_adds_user_and_system_messages() {
153 let prompt = simple_prompt(SimplePromptOptions {
154 message: Some("Hello assistant".into()),
155 system: Some("Behave helpfully".into()),
156 temperature: Some(0.3),
157 ..SimplePromptOptions::default()
158 })
159 .expect("prompt should construct");
160
161 assert_eq!(prompt.messages.len(), 2);
162 assert_eq!(prompt.messages[0].role, MessageRole::System);
163 assert_eq!(prompt.messages[1].role, MessageRole::User);
164 assert_eq!(prompt.sampling.temperature, Some(0.3));
165 }
166
167 #[test]
168 fn simple_prompt_requires_user_content() {
169 let err = simple_prompt(SimplePromptOptions::default()).unwrap_err();
170 assert!(matches!(err, Error::Config(_)));
171 }
172}