1use agent_line::{Agent, Ctx, Outcome, Runner, StepResult, Workflow};
2
3#[derive(Clone, Debug)]
8struct TopicState {
9 query: String,
10 topics: Vec<String>,
11 selected: Vec<String>,
12}
13
14#[derive(Clone, Debug)]
15struct ArticleState {
16 topic: String,
17 draft: String,
18 revision: u32,
19}
20
21struct TopicSearcher;
26impl Agent<TopicState> for TopicSearcher {
27 fn name(&self) -> &'static str {
28 "topic_searcher"
29 }
30 fn run(&mut self, mut state: TopicState, ctx: &mut Ctx) -> StepResult<TopicState> {
31 ctx.log(format!("searching for: {}", state.query));
32
33 state.topics = vec![
35 "Rust in embedded systems".into(),
36 "Why plumbers love side projects".into(),
37 "Welding meets software: CNC pipelines".into(),
38 "HVAC technicians automating schedules".into(),
39 "Electricians using Raspberry Pi on the job".into(),
40 ];
41
42 ctx.log(format!("found {} topics", state.topics.len()));
43 Ok((state, Outcome::Continue))
44 }
45}
46
47struct TopicPicker;
48impl Agent<TopicState> for TopicPicker {
49 fn name(&self) -> &'static str {
50 "topic_picker"
51 }
52 fn run(&mut self, mut state: TopicState, ctx: &mut Ctx) -> StepResult<TopicState> {
53 let response = ctx.llm()
54 .system("You are a newsletter curator. Pick exactly 3 topics. Return one per line, nothing else.")
55 .user(format!("Choose from:\n{}", state.topics.join("\n")))
56 .send()?;
57
58 state.selected = response
60 .lines()
61 .map(|l| l.trim())
62 .map(|l| {
63 l.trim_start_matches(|c: char| c.is_numeric() || c == '.' || c == '-' || c == ' ')
64 })
65 .map(|l| l.trim().to_string())
66 .filter(|l| !l.is_empty())
67 .collect();
68
69 for topic in &state.selected {
70 ctx.log(format!("selected: {topic}"));
71 }
72
73 Ok((state, Outcome::Done))
74 }
75}
76
77struct ArticleWriter;
82impl Agent<ArticleState> for ArticleWriter {
83 fn name(&self) -> &'static str {
84 "article_writer"
85 }
86 fn run(&mut self, mut state: ArticleState, ctx: &mut Ctx) -> StepResult<ArticleState> {
87 state.revision += 1;
88 ctx.log(format!(
89 "writing draft {} for: {}",
90 state.revision, state.topic
91 ));
92
93 if state.revision == 1 {
95 state.draft = format!(
96 "# {}\n\nThis is a artcle about {}. It has lots of good infomation.",
97 state.topic, state.topic
98 );
99 } else {
100 state.draft = format!(
101 "# {}\n\nThis is an article about {}. It has lots of good information.",
102 state.topic, state.topic
103 );
104 }
105
106 Ok((state, Outcome::Continue))
107 }
108}
109
110struct ArticleValidator;
111impl Agent<ArticleState> for ArticleValidator {
112 fn name(&self) -> &'static str {
113 "article_validator"
114 }
115 fn run(&mut self, state: ArticleState, ctx: &mut Ctx) -> StepResult<ArticleState> {
116 let response = ctx
118 .llm()
119 .system("You are a strict editor. List any errors. Say PASS if none.")
120 .user(&state.draft)
121 .send()?;
122
123 if response.contains("PASS") {
125 Ok((state, Outcome::Done))
126 } else {
127 ctx.set("errors", &response);
128 Ok((state, Outcome::Next("article_fixer")))
129 }
130 }
131}
132
133struct ArticleFixer;
134impl Agent<ArticleState> for ArticleFixer {
135 fn name(&self) -> &'static str {
136 "article_fixer"
137 }
138 fn run(&mut self, mut state: ArticleState, ctx: &mut Ctx) -> StepResult<ArticleState> {
139 let errors = ctx.get("errors").unwrap_or("no errors found").to_string();
140 let response = ctx
141 .llm()
142 .system("You are a writer. Rewrite the article fixing only the listed errors.")
143 .user(format!("Errors:\n{errors}\n\nArticle:\n{}", state.draft))
144 .send()?;
145
146 state.draft = response;
147 Ok((state, Outcome::Next("article_validator")))
148 }
149}
150
151fn main() {
156 let mut ctx = Ctx::new();
157
158 let topic_wf = Workflow::builder("find-topics")
162 .register(TopicSearcher)
163 .register(TopicPicker)
164 .start_at("topic_searcher")
165 .then("topic_picker")
166 .build()
167 .unwrap();
168
169 let mut topic_runner = Runner::new(topic_wf);
170 let topics = topic_runner
171 .run(
172 TopicState {
173 query: "bluecollar engineering newsletter".into(),
174 topics: vec![],
175 selected: vec![],
176 },
177 &mut ctx,
178 )
179 .unwrap();
180
181 println!("=== Topics ===");
182 for entry in ctx.logs() {
183 println!(" {entry}");
184 }
185 ctx.clear_logs();
186 println!();
187
188 let article_wf = Workflow::builder("write-article")
190 .register(ArticleWriter)
191 .register(ArticleValidator)
192 .register(ArticleFixer)
193 .start_at("article_writer")
194 .then("article_validator")
195 .build()
196 .unwrap();
197
198 let mut article_runner = Runner::new(article_wf);
199 let mut finished_articles: Vec<String> = Vec::new();
200
201 for (i, topic) in topics.selected.iter().enumerate() {
202 println!("=== Article {} ===", i + 1);
203
204 let result = article_runner
205 .run(
206 ArticleState {
207 topic: topic.clone(),
208 draft: String::new(),
209 revision: 0,
210 },
211 &mut ctx,
212 )
213 .unwrap();
214
215 finished_articles.push(result.draft.clone());
216 println!(" Revisions: {}", result.revision);
217
218 for entry in ctx.logs() {
219 println!(" {entry}");
220 }
221 ctx.clear_logs();
222 println!();
223 }
224
225 println!("=== Stored ===");
227 for (i, article) in finished_articles.iter().enumerate() {
228 let preview: String = article.chars().take(60).collect();
229 println!(" article_{}.md: {preview}...", i + 1);
230 }
231}