Skip to content

Commit 4e7733a

Browse files
author
trae
committed
Milestone 8: LaTeX paper generation
1 parent 1c2736b commit 4e7733a

File tree

7 files changed

+293
-5
lines changed

7 files changed

+293
-5
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "consensusmind"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
edition = "2021"
55
authors = ["Distributed Systems Labs, LLC"]
66
license = "Apache-2.0"

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ConsensusMind is an autonomous research agent that conducts end-to-end research
88

99
## Status
1010

11-
**Current Version:** 0.7.0 - Milestone 7 Complete
11+
**Current Version:** 0.8.0 - Milestone 8 Complete
1212

1313
### Completed Milestones
1414

@@ -47,9 +47,12 @@ ConsensusMind is an autonomous research agent that conducts end-to-end research
4747
- Experiment runner tied to hypothesis IDs
4848
- Results saved under data/experiments and summarized into output reports
4949

50+
#### Milestone 8: Paper Generation
51+
- LaTeX paper generation from hypotheses and experiment results
52+
5053
## Features
5154

52-
### Current (v0.7.0)
55+
### Current (v0.8.0)
5356
- Configuration management from TOML files
5457
- Environment variable overrides for sensitive data
5558
- Structured logging to file and console
@@ -64,6 +67,7 @@ ConsensusMind is an autonomous research agent that conducts end-to-end research
6467
- Agent run pipeline (search, download, index, retrieve, summarize, report)
6568
- Hypothesis generation and persistence
6669
- Consensus simulation and experimentation
70+
- LaTeX paper generation from experiment outputs
6771

6872
### Planned
6973
- Automated LaTeX paper generation
@@ -126,6 +130,7 @@ Additional settings:
126130
consensusmind run "<query>"
127131
consensusmind hypothesize "<query>"
128132
consensusmind experiment <hypothesis-id> [--seeds N] [--ticks T] [--nodes N]
133+
consensusmind paper <hypothesis-id>
129134
consensusmind index
130135
consensusmind semantic-search "<query>" [top_k]
131136
consensusmind simulate [rounds] [leader_failure_prob] [seed]
@@ -154,7 +159,7 @@ CI runs `cargo fmt --check`, `cargo clippy -- -D warnings`, and `cargo test` on
154159
- [x] Milestone 5: Consensus Simulator
155160
- [x] Milestone 6: Hypothesis Generation
156161
- [x] Milestone 7: Automated Experimentation
157-
- [ ] Milestone 8: Paper Generation
162+
- [x] Milestone 8: Paper Generation
158163
- [ ] Milestone 9: Integration & Polish
159164
- [ ] Milestone 10: Whitepaper & Research Paper
160165

src/agent/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,17 @@ impl Agent {
228228
save_experiment_report(&self.config.paths.output, &report)
229229
}
230230

231+
pub fn paper(&self, hypothesis_id: &str) -> Result<crate::output::paper::SavedPaper> {
232+
let hypothesis =
233+
experiment::find_hypothesis(&self.config.knowledge.hypotheses_file, hypothesis_id)?;
234+
let run = crate::output::paper::load_experiment_results(
235+
&self.config.paths.experiments,
236+
hypothesis_id,
237+
)?;
238+
let store = MetadataStore::new(self.config.knowledge.metadata_file.clone())?;
239+
crate::output::paper::write_paper_tex(&self.config.paths.output, &hypothesis, &run, &store)
240+
}
241+
231242
async fn hypothesize_inner(&mut self, query: &str) -> Result<SavedReport> {
232243
let max_results = (self.config.agent.max_iterations as usize).clamp(1, 10);
233244
let download_limit = self.config.agent.download_limit;

src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ async fn main() -> Result<()> {
146146
println!("Experiment report saved to {}", report.path.display());
147147
return Ok(());
148148
}
149+
Some("paper") => {
150+
let hypothesis_id = args.get(2).map(|s| s.as_str()).unwrap_or("");
151+
if hypothesis_id.trim().is_empty() {
152+
println!("Usage: consensusmind paper <hypothesis-id>");
153+
return Ok(());
154+
}
155+
let agent = Agent::new(config)?;
156+
let paper = agent.paper(hypothesis_id)?;
157+
println!("Paper TeX saved to {}", paper.tex_path.display());
158+
return Ok(());
159+
}
149160
Some("simulate") => {
150161
let rounds = args
151162
.get(2)

src/output/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
44
use std::fs;
55
use std::path::{Path, PathBuf};
66

7+
pub mod paper;
8+
79
#[derive(Debug, Clone, Serialize, Deserialize)]
810
pub struct AgentRunReport {
911
pub query: String,

src/output/paper.rs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
use crate::agent::experiment::ExperimentRun;
2+
use crate::agent::hypothesis::Hypothesis;
3+
use crate::knowledge::database::MetadataStore;
4+
use anyhow::{Context, Result};
5+
use chrono::Utc;
6+
use serde::{Deserialize, Serialize};
7+
use std::fs;
8+
use std::path::{Path, PathBuf};
9+
10+
#[derive(Debug, Clone, Serialize, Deserialize)]
11+
pub struct SavedPaper {
12+
pub tex_path: PathBuf,
13+
}
14+
15+
pub fn write_paper_tex(
16+
output_root: &Path,
17+
hypothesis: &Hypothesis,
18+
experiment_run: &ExperimentRun,
19+
metadata_store: &MetadataStore,
20+
) -> Result<SavedPaper> {
21+
let papers_dir = output_root.join("papers");
22+
fs::create_dir_all(&papers_dir)?;
23+
24+
let ts = Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
25+
let filename = format!("paper-{}-{}.tex", sanitize_filename(&hypothesis.id), ts);
26+
let tex_path = papers_dir.join(filename);
27+
28+
let tex = render_tex(hypothesis, experiment_run, metadata_store)?;
29+
crate::utils::fs::atomic_write(&tex_path, tex.as_bytes())?;
30+
31+
Ok(SavedPaper { tex_path })
32+
}
33+
34+
pub fn load_experiment_results(
35+
experiments_root: &Path,
36+
hypothesis_id: &str,
37+
) -> Result<ExperimentRun> {
38+
let path = experiments_root.join(hypothesis_id).join("results.json");
39+
let contents = fs::read_to_string(&path)
40+
.with_context(|| format!("Failed to read experiment results: {}", path.display()))?;
41+
Ok(serde_json::from_str(&contents)?)
42+
}
43+
44+
fn render_tex(
45+
hypothesis: &Hypothesis,
46+
experiment_run: &ExperimentRun,
47+
metadata_store: &MetadataStore,
48+
) -> Result<String> {
49+
let mut s = String::new();
50+
s.push_str("\\documentclass[11pt]{article}\n");
51+
s.push_str("\\usepackage[margin=1in]{geometry}\n");
52+
s.push_str("\\usepackage{hyperref}\n");
53+
s.push_str("\\usepackage{amsmath}\n");
54+
s.push_str("\\usepackage{booktabs}\n");
55+
s.push_str("\\title{");
56+
s.push_str(&escape_tex(&hypothesis.title));
57+
s.push_str("}\n");
58+
s.push_str("\\author{ConsensusMind}\n");
59+
s.push_str("\\date{");
60+
s.push_str(&escape_tex(&Utc::now().format("%Y-%m-%d").to_string()));
61+
s.push_str("}\n");
62+
s.push_str("\\begin{document}\n\\maketitle\n");
63+
64+
s.push_str("\\begin{abstract}\n");
65+
s.push_str(&escape_tex(&hypothesis.description));
66+
s.push_str("\n\\end{abstract}\n\n");
67+
68+
s.push_str("\\section{Introduction}\n");
69+
s.push_str("We investigate a new hypothesis for blockchain consensus mechanisms and evaluate it using simulation-based experiments. ");
70+
s.push_str("This paper is auto-generated from a structured hypothesis description and experiment outputs.\n\n");
71+
72+
s.push_str("\\section{Hypothesis}\n");
73+
s.push_str("\\textbf{Mechanism.} ");
74+
s.push_str(&escape_tex(&hypothesis.mechanism));
75+
s.push_str("\n\n");
76+
77+
if !hypothesis.evaluation_plan.is_empty() {
78+
s.push_str("\\textbf{Evaluation plan.}\n\\begin{enumerate}\n");
79+
for step in hypothesis.evaluation_plan.iter() {
80+
s.push_str("\\item ");
81+
s.push_str(&escape_tex(step));
82+
s.push('\n');
83+
}
84+
s.push_str("\\end{enumerate}\n\n");
85+
}
86+
87+
s.push_str("\\section{Experimental Setup}\n");
88+
s.push_str("We run two families of experiments: (1) a leader-based baseline with an injected leader-failure probability and (2) a simplified Raft-style simulator with message delays, randomized election timeouts, and client request arrivals.\n\n");
89+
90+
s.push_str("\\section{Results}\n");
91+
s.push_str("\\subsection{Raft-style simulator}\n");
92+
if experiment_run.raft.is_empty() {
93+
s.push_str("No Raft simulation cases were recorded.\n\n");
94+
} else {
95+
s.push_str("Table~\\ref{tab:raft} summarizes mean metrics across seeds for each parameter setting.\n\n");
96+
s.push_str("\\begin{table}[h]\n\\centering\n");
97+
s.push_str("\\begin{tabular}{rrrrrr}\n\\toprule\n");
98+
s.push_str(
99+
"nodes & ticks & $p_{req}$ & elections & leader changes & commit rate\\\\\n\\midrule\n",
100+
);
101+
for case in experiment_run.raft.iter() {
102+
s.push_str(&format!(
103+
"{} & {} & {:.2} & {:.2} & {:.2} & {:.6}\\\\\n",
104+
case.params.nodes,
105+
case.params.ticks,
106+
case.params.client_request_prob,
107+
case.aggregate.mean_elections,
108+
case.aggregate.mean_leader_changes,
109+
case.aggregate.mean_commit_rate_per_tick
110+
));
111+
}
112+
s.push_str("\\bottomrule\n\\end{tabular}\n");
113+
s.push_str(
114+
"\\caption{Raft-style simulator aggregates (means across seeds).}\\label{tab:raft}\n",
115+
);
116+
s.push_str("\\end{table}\n\n");
117+
}
118+
119+
s.push_str("\\subsection{Leader-failure baseline}\n");
120+
if experiment_run.leader.is_empty() {
121+
s.push_str("No leader-baseline cases were recorded.\n\n");
122+
} else {
123+
s.push_str("Table~\\ref{tab:leader} summarizes mean commit rate across seeds.\n\n");
124+
s.push_str("\\begin{table}[h]\n\\centering\n");
125+
s.push_str("\\begin{tabular}{rr}\n\\toprule\n");
126+
s.push_str("$p_{fail}$ & mean commit rate\\\\\n\\midrule\n");
127+
for case in experiment_run.leader.iter() {
128+
s.push_str(&format!(
129+
"{:.2} & {:.4}\\\\\n",
130+
case.params.leader_failure_prob, case.aggregate.mean_commit_rate
131+
));
132+
}
133+
s.push_str("\\bottomrule\n\\end{tabular}\n");
134+
s.push_str("\\caption{Leader-failure baseline aggregates (means across seeds).}\\label{tab:leader}\n");
135+
s.push_str("\\end{table}\n\n");
136+
}
137+
138+
s.push_str("\\section{Related Work}\n");
139+
if hypothesis.related_paper_ids.is_empty() {
140+
s.push_str("No related papers were explicitly linked to this hypothesis.\n\n");
141+
} else {
142+
s.push_str("We cite the most relevant papers identified during literature review.\n\n");
143+
}
144+
145+
s.push_str("\\section{References}\n");
146+
s.push_str("\\begin{thebibliography}{99}\n");
147+
for paper_id in hypothesis.related_paper_ids.iter() {
148+
let label = bib_label(paper_id);
149+
let title = metadata_store
150+
.get_paper(paper_id)
151+
.map(|p| p.title.clone())
152+
.unwrap_or_else(|| paper_id.to_string());
153+
s.push_str("\\bibitem{");
154+
s.push_str(&escape_tex(&label));
155+
s.push('}');
156+
s.push_str(&escape_tex(&title));
157+
s.push_str(". ");
158+
s.push_str("\\href{https://arxiv.org/abs/");
159+
s.push_str(&escape_tex(paper_id));
160+
s.push_str("}{arXiv:");
161+
s.push_str(&escape_tex(paper_id));
162+
s.push_str("}.\n");
163+
}
164+
s.push_str("\\end{thebibliography}\n");
165+
166+
s.push_str("\\end{document}\n");
167+
Ok(s)
168+
}
169+
170+
fn bib_label(paper_id: &str) -> String {
171+
format!("arxiv:{}", paper_id.replace([':', '/'], "_"))
172+
}
173+
174+
fn sanitize_filename(s: &str) -> String {
175+
s.chars()
176+
.map(|c| {
177+
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
178+
c
179+
} else {
180+
'_'
181+
}
182+
})
183+
.collect()
184+
}
185+
186+
fn escape_tex(s: &str) -> String {
187+
let mut out = String::new();
188+
for c in s.chars() {
189+
match c {
190+
'\\' => out.push_str("\\textbackslash{}"),
191+
'{' => out.push_str("\\{"),
192+
'}' => out.push_str("\\}"),
193+
'$' => out.push_str("\\$"),
194+
'&' => out.push_str("\\&"),
195+
'#' => out.push_str("\\#"),
196+
'%' => out.push_str("\\%"),
197+
'_' => out.push_str("\\_"),
198+
'^' => out.push_str("\\^{}"),
199+
'~' => out.push_str("\\~{}"),
200+
_ => out.push(c),
201+
}
202+
}
203+
out
204+
}
205+
206+
#[cfg(test)]
207+
mod tests {
208+
use super::*;
209+
use crate::agent::experiment::{run_experiments, ExperimentOverrides};
210+
211+
fn temp_path(name: &str) -> PathBuf {
212+
let pid = std::process::id();
213+
let ts = Utc::now().timestamp_nanos_opt().unwrap_or(0);
214+
std::env::temp_dir().join(format!("consensusmind-paper-{}-{}-{}", name, pid, ts))
215+
}
216+
217+
#[test]
218+
fn render_tex_contains_title_and_tables() {
219+
let root = temp_path("render");
220+
fs::create_dir_all(&root).unwrap();
221+
222+
let hypotheses_path = root.join("hypotheses.json");
223+
let metadata_path = root.join("metadata.json");
224+
225+
let h = Hypothesis {
226+
id: "h1".to_string(),
227+
title: "A New Hypothesis".to_string(),
228+
description: "desc".to_string(),
229+
mechanism: "mech".to_string(),
230+
evaluation_plan: vec!["plan".to_string()],
231+
related_paper_ids: vec!["1234.56789".to_string()],
232+
novelty_score: 0.2,
233+
feasibility_score: 0.8,
234+
created_at: Utc::now().to_rfc3339(),
235+
};
236+
let h_contents = serde_json::to_string_pretty(&vec![h.clone()]).unwrap();
237+
crate::utils::fs::atomic_write(&hypotheses_path, h_contents.as_bytes()).unwrap();
238+
239+
crate::utils::fs::atomic_write(&metadata_path, "{}".as_bytes()).unwrap();
240+
let store = MetadataStore::new(metadata_path).unwrap();
241+
242+
let (_hyp, run, _path) = run_experiments(
243+
&root,
244+
&hypotheses_path,
245+
"h1",
246+
ExperimentOverrides {
247+
seeds: Some(2),
248+
ticks: Some(200),
249+
nodes: Some(3),
250+
},
251+
)
252+
.unwrap();
253+
254+
let tex = render_tex(&h, &run, &store).unwrap();
255+
assert!(tex.contains("\\title{A New Hypothesis}"));
256+
assert!(tex.contains("\\begin{table}"));
257+
assert!(tex.contains("\\bibitem{"));
258+
}
259+
}

0 commit comments

Comments
 (0)