1use crate::context::{Context, ContextKey, Fact};
24use std::collections::HashSet;
25use std::fmt::Write;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum PromptFormat {
30 Plain,
32 Edn,
34}
35
36impl Default for PromptFormat {
37 fn default() -> Self {
38 Self::Edn
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum AgentRole {
45 Proposer,
47 Validator,
49 Synthesizer,
51 Analyzer,
53}
54
55impl AgentRole {
56 fn to_keyword(self) -> &'static str {
58 match self {
59 Self::Proposer => ":proposer",
60 Self::Validator => ":validator",
61 Self::Synthesizer => ":synthesizer",
62 Self::Analyzer => ":analyzer",
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
69pub enum Constraint {
70 NoInvent,
72 NoContradict,
74 NoHallucinate,
76 CiteSources,
78}
79
80impl Constraint {
81 fn to_keyword(self) -> &'static str {
83 match self {
84 Self::NoInvent => ":no-invent",
85 Self::NoContradict => ":no-contradict",
86 Self::NoHallucinate => ":no-hallucinate",
87 Self::CiteSources => ":cite-sources",
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct OutputContract {
95 pub emit: String,
97 pub key: ContextKey,
99 pub format: Option<String>,
101}
102
103impl OutputContract {
104 #[must_use]
106 pub fn new(emit: impl Into<String>, key: ContextKey) -> Self {
107 Self {
108 emit: emit.into(),
109 key,
110 format: None,
111 }
112 }
113
114 #[must_use]
116 pub fn with_format(mut self, format: impl Into<String>) -> Self {
117 self.format = Some(format.into());
118 self
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct AgentPrompt {
128 pub role: AgentRole,
130 pub objective: String,
132 pub context: PromptContext,
134 pub constraints: HashSet<Constraint>,
136 pub output_contract: OutputContract,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Default)]
142pub struct PromptContext {
143 pub facts: Vec<(ContextKey, Vec<Fact>)>,
145}
146
147impl PromptContext {
148 #[must_use]
150 pub fn new() -> Self {
151 Self::default()
152 }
153
154 pub fn add_facts(&mut self, key: ContextKey, facts: Vec<Fact>) {
156 if !facts.is_empty() {
157 self.facts.push((key, facts));
158 }
159 }
160
161 #[must_use]
163 pub fn from_context(ctx: &Context, dependencies: &[ContextKey]) -> Self {
164 let mut prompt_ctx = Self::new();
165 for &key in dependencies {
166 let facts = ctx.get(key).to_vec();
167 prompt_ctx.add_facts(key, facts);
168 }
169 prompt_ctx
170 }
171}
172
173fn context_key_to_keyword(key: ContextKey) -> &'static str {
175 match key {
176 ContextKey::Seeds => ":seeds",
177 ContextKey::Hypotheses => ":hypotheses",
178 ContextKey::Strategies => ":strategies",
179 ContextKey::Constraints => ":constraints",
180 ContextKey::Signals => ":signals",
181 ContextKey::Competitors => ":competitors",
182 ContextKey::Evaluations => ":evaluations",
183 ContextKey::Proposals => ":proposals",
184 ContextKey::Diagnostic => ":diagnostic",
185 }
186}
187
188impl AgentPrompt {
189 #[must_use]
191 pub fn new(
192 role: AgentRole,
193 objective: impl Into<String>,
194 context: PromptContext,
195 output_contract: OutputContract,
196 ) -> Self {
197 Self {
198 role,
199 objective: objective.into(),
200 context,
201 constraints: HashSet::new(),
202 output_contract,
203 }
204 }
205
206 #[must_use]
208 pub fn with_constraint(mut self, constraint: Constraint) -> Self {
209 self.constraints.insert(constraint);
210 self
211 }
212
213 #[must_use]
215 pub fn with_constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
216 self.constraints.extend(constraints);
217 self
218 }
219
220 pub fn to_edn(&self) -> String {
231 let mut s = String::new();
232 s.push_str("{:r ");
233 s.push_str(self.role.to_keyword());
234 s.push_str("\n :o :");
235 s.push_str(&self.objective.replace(' ', "-"));
237 s.push_str("\n :c {");
238
239 let mut first_key = true;
241 for (key, facts) in &self.context.facts {
242 if !first_key {
243 s.push(' ');
244 }
245 first_key = false;
246 s.push_str(context_key_to_keyword(*key));
247 s.push_str(" [{");
248 for (i, fact) in facts.iter().enumerate() {
249 if i > 0 {
250 s.push_str("} {");
251 }
252 s.push_str(":id \"");
253 s.push_str(&escape_string(&fact.id));
254 s.push_str("\" :c \"");
255 s.push_str(&escape_string(&fact.content));
256 s.push_str("\"");
257 }
258 s.push_str("}]");
259 }
260
261 s.push_str("}\n :k #{");
262
263 let mut constraints: Vec<_> = self.constraints.iter().collect();
265 constraints.sort(); for (i, constraint) in constraints.iter().enumerate() {
267 if i > 0 {
268 s.push(' ');
269 }
270 s.push_str(constraint.to_keyword());
271 }
272
273 s.push_str("}\n :out {:emit :");
274 s.push_str(&self.output_contract.emit);
275 s.push_str(" :key ");
276 s.push_str(context_key_to_keyword(self.output_contract.key));
277 if let Some(ref format) = self.output_contract.format {
278 s.push_str(" :format :");
279 s.push_str(format);
280 }
281 s.push_str("}}");
282
283 s
284 }
285
286 pub fn to_plain(&self) -> String {
288 let mut s = String::new();
289 writeln!(s, "Role: {:?}", self.role).unwrap();
290 writeln!(s, "Objective: {}", self.objective).unwrap();
291 writeln!(s, "\nContext:").unwrap();
292
293 for (key, facts) in &self.context.facts {
294 writeln!(s, "\n## {:?}", key).unwrap();
295 for fact in facts {
296 writeln!(s, "- {}: {}", fact.id, fact.content).unwrap();
297 }
298 }
299
300 if !self.constraints.is_empty() {
301 writeln!(s, "\nConstraints:").unwrap();
302 for constraint in &self.constraints {
303 writeln!(s, "- {:?}", constraint).unwrap();
304 }
305 }
306
307 writeln!(s, "\nOutput: {:?} -> {:?}", self.output_contract.emit, self.output_contract.key).unwrap();
308
309 s
310 }
311
312 pub fn serialize(&self, format: PromptFormat) -> String {
314 match format {
315 PromptFormat::Edn => self.to_edn(),
316 PromptFormat::Plain => self.to_plain(),
317 }
318 }
319}
320
321fn escape_string(s: &str) -> String {
323 s.replace('\\', "\\\\")
324 .replace('"', "\\\"")
325 .replace('\n', "\\n")
326 .replace('\r', "\\r")
327 .replace('\t', "\\t")
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::context::{Context, Fact};
334
335 #[test]
336 fn test_edn_serialization() {
337 let mut ctx = PromptContext::new();
338 ctx.add_facts(
339 ContextKey::Signals,
340 vec![
341 Fact {
342 key: ContextKey::Signals,
343 id: "s1".to_string(),
344 content: "Revenue +15% Q3".to_string(),
345 },
346 Fact {
347 key: ContextKey::Signals,
348 id: "s2".to_string(),
349 content: "Market $2.3B".to_string(),
350 },
351 ],
352 );
353
354 let prompt = AgentPrompt::new(
355 AgentRole::Proposer,
356 "extract-competitors",
357 ctx,
358 OutputContract::new("proposed-fact", ContextKey::Competitors),
359 )
360 .with_constraint(Constraint::NoInvent)
361 .with_constraint(Constraint::NoContradict);
362
363 let edn = prompt.to_edn();
364 assert!(edn.contains(":r :proposer"));
365 assert!(edn.contains(":o :extract-competitors"));
366 assert!(edn.contains(":signals"));
367 assert!(edn.contains(":no-invent"));
368 assert!(edn.contains(":no-contradict"));
369 assert!(edn.contains(":competitors"));
370 }
371
372 #[test]
373 fn test_context_building() {
374 let mut context = Context::new();
375 context
376 .add_fact(Fact {
377 key: ContextKey::Seeds,
378 id: "seed1".to_string(),
379 content: "Test seed".to_string(),
380 })
381 .unwrap();
382
383 let prompt_ctx = PromptContext::from_context(&context, &[ContextKey::Seeds]);
384 assert_eq!(prompt_ctx.facts.len(), 1);
385 assert_eq!(prompt_ctx.facts[0].0, ContextKey::Seeds);
386 assert_eq!(prompt_ctx.facts[0].1.len(), 1);
387 }
388
389 #[test]
390 fn test_escape_string() {
391 assert_eq!(escape_string("hello"), "hello");
392 assert_eq!(escape_string("hello\"world"), "hello\\\"world");
393 assert_eq!(escape_string("hello\nworld"), "hello\\nworld");
394 }
395
396 #[test]
397 fn test_token_efficiency() {
398 let mut ctx = PromptContext::new();
399 ctx.add_facts(
400 ContextKey::Signals,
401 vec![Fact {
402 key: ContextKey::Signals,
403 id: "s1".to_string(),
404 content: "Revenue +15% Q3".to_string(),
405 }],
406 );
407
408 let prompt = AgentPrompt::new(
409 AgentRole::Proposer,
410 "analyze",
411 ctx,
412 OutputContract::new("proposed-fact", ContextKey::Strategies),
413 );
414
415 let edn = prompt.to_edn();
416 let plain = prompt.to_plain();
417
418 println!("EDN length: {}", edn.len());
419 println!("Plain length: {}", plain.len());
420 println!("EDN:\n{}", edn);
421 println!("Plain:\n{}", plain);
422
423 assert!(edn.len() > 0);
428 assert!(plain.len() > 0);
429 }
430}
431