use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToulminArgument {
pub claim: Claim,
pub grounds: Vec<Ground>,
pub warrant: Option<Warrant>,
pub backing: Vec<Backing>,
pub qualifier: Qualifier,
pub rebuttals: Vec<Rebuttal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claim {
pub statement: String,
pub claim_type: ClaimType,
pub scope: Scope,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClaimType {
Fact,
Value,
Policy,
Prediction,
Causal,
Definition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Scope {
Universal,
General,
Particular,
Singular,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ground {
pub evidence: String,
pub evidence_type: EvidenceType,
pub source: Option<String>,
pub credibility: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvidenceType {
Statistical,
Testimonial,
Example,
Documentary,
Empirical,
CommonGround,
Analogical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Warrant {
pub principle: String,
pub warrant_type: WarrantType,
pub is_explicit: bool,
pub strength: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WarrantType {
Authority,
Causal,
Classification,
Sign,
Comparison,
Generalization,
Principle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Backing {
pub support: String,
pub backing_type: BackingType,
pub source: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackingType {
Legal,
Scientific,
Historical,
Cultural,
Consensus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Qualifier {
Certainly,
Presumably,
Probably,
Possibly,
Unlikely,
Conditionally,
}
impl Qualifier {
pub fn confidence(&self) -> f32 {
match self {
Qualifier::Certainly => 0.99,
Qualifier::Presumably => 0.90,
Qualifier::Probably => 0.75,
Qualifier::Possibly => 0.50,
Qualifier::Unlikely => 0.25,
Qualifier::Conditionally => 0.60,
}
}
pub fn label(&self) -> &'static str {
match self {
Qualifier::Certainly => "certainly",
Qualifier::Presumably => "presumably",
Qualifier::Probably => "probably",
Qualifier::Possibly => "possibly",
Qualifier::Unlikely => "unlikely",
Qualifier::Conditionally => "if conditions hold",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rebuttal {
pub exception: String,
pub likelihood: f32,
pub severity: RebuttalSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RebuttalSeverity {
Minor,
Moderate,
Major,
Fatal,
}
#[derive(Debug, Default)]
pub struct ArgumentBuilder {
claim: Option<Claim>,
grounds: Vec<Ground>,
warrant: Option<Warrant>,
backing: Vec<Backing>,
qualifier: Option<Qualifier>,
rebuttals: Vec<Rebuttal>,
}
impl ArgumentBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn claim(mut self, statement: impl Into<String>) -> Self {
self.claim = Some(Claim {
statement: statement.into(),
claim_type: ClaimType::Fact,
scope: Scope::General,
});
self
}
pub fn claim_full(
mut self,
statement: impl Into<String>,
claim_type: ClaimType,
scope: Scope,
) -> Self {
self.claim = Some(Claim {
statement: statement.into(),
claim_type,
scope,
});
self
}
pub fn grounds(mut self, evidence: impl Into<String>) -> Self {
self.grounds.push(Ground {
evidence: evidence.into(),
evidence_type: EvidenceType::Empirical,
source: None,
credibility: 0.7,
});
self
}
pub fn grounds_full(
mut self,
evidence: impl Into<String>,
evidence_type: EvidenceType,
source: Option<String>,
credibility: f32,
) -> Self {
self.grounds.push(Ground {
evidence: evidence.into(),
evidence_type,
source,
credibility,
});
self
}
pub fn warrant(mut self, principle: impl Into<String>) -> Self {
self.warrant = Some(Warrant {
principle: principle.into(),
warrant_type: WarrantType::Principle,
is_explicit: true,
strength: 0.8,
});
self
}
pub fn warrant_full(
mut self,
principle: impl Into<String>,
warrant_type: WarrantType,
strength: f32,
) -> Self {
self.warrant = Some(Warrant {
principle: principle.into(),
warrant_type,
is_explicit: true,
strength,
});
self
}
pub fn backing(mut self, support: impl Into<String>) -> Self {
self.backing.push(Backing {
support: support.into(),
backing_type: BackingType::Scientific,
source: None,
});
self
}
pub fn qualifier(mut self, qualifier: Qualifier) -> Self {
self.qualifier = Some(qualifier);
self
}
pub fn rebuttal(mut self, exception: impl Into<String>) -> Self {
self.rebuttals.push(Rebuttal {
exception: exception.into(),
likelihood: 0.3,
severity: RebuttalSeverity::Moderate,
});
self
}
pub fn rebuttal_full(
mut self,
exception: impl Into<String>,
likelihood: f32,
severity: RebuttalSeverity,
) -> Self {
self.rebuttals.push(Rebuttal {
exception: exception.into(),
likelihood,
severity,
});
self
}
pub fn build(self) -> Result<ToulminArgument, ArgumentError> {
let claim = self.claim.ok_or(ArgumentError::MissingClaim)?;
if self.grounds.is_empty() {
return Err(ArgumentError::MissingGrounds);
}
Ok(ToulminArgument {
claim,
grounds: self.grounds,
warrant: self.warrant,
backing: self.backing,
qualifier: self.qualifier.unwrap_or(Qualifier::Probably),
rebuttals: self.rebuttals,
})
}
}
#[derive(Debug, Clone)]
pub enum ArgumentError {
MissingClaim,
MissingGrounds,
InvalidWarrant(String),
}
impl std::fmt::Display for ArgumentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArgumentError::MissingClaim => write!(f, "Argument requires a claim"),
ArgumentError::MissingGrounds => write!(f, "Argument requires at least one ground"),
ArgumentError::InvalidWarrant(msg) => write!(f, "Invalid warrant: {}", msg),
}
}
}
impl std::error::Error for ArgumentError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgumentEvaluation {
pub overall_strength: f32,
pub grounds_score: f32,
pub warrant_score: f32,
pub backing_score: f32,
pub rebuttal_impact: f32,
pub issues: Vec<ArgumentIssue>,
pub is_valid: bool,
pub is_sound: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArgumentIssue {
pub component: ToulminComponent,
pub issue: String,
pub severity: IssueSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToulminComponent {
Claim,
Grounds,
Warrant,
Backing,
Qualifier,
Rebuttal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IssueSeverity {
Minor,
Moderate,
Serious,
Critical,
}
impl ToulminArgument {
pub fn evaluate(&self) -> ArgumentEvaluation {
let mut issues = Vec::new();
let grounds_score = if self.grounds.is_empty() {
issues.push(ArgumentIssue {
component: ToulminComponent::Grounds,
issue: "No supporting evidence provided".into(),
severity: IssueSeverity::Critical,
});
0.0
} else {
self.grounds.iter().map(|g| g.credibility).sum::<f32>() / self.grounds.len() as f32
};
let warrant_score = if let Some(ref w) = self.warrant {
if !w.is_explicit {
issues.push(ArgumentIssue {
component: ToulminComponent::Warrant,
issue: "Warrant is implicit - should be made explicit".into(),
severity: IssueSeverity::Minor,
});
}
w.strength
} else {
issues.push(ArgumentIssue {
component: ToulminComponent::Warrant,
issue: "Missing warrant connecting evidence to claim".into(),
severity: IssueSeverity::Serious,
});
0.3 };
let backing_score = if self.backing.is_empty() {
0.5 } else {
0.7 + 0.1 * self.backing.len().min(3) as f32
};
let rebuttal_impact: f32 = self
.rebuttals
.iter()
.map(|r| {
let severity_weight = match r.severity {
RebuttalSeverity::Minor => 0.1,
RebuttalSeverity::Moderate => 0.25,
RebuttalSeverity::Major => 0.5,
RebuttalSeverity::Fatal => 1.0,
};
r.likelihood * severity_weight
})
.sum::<f32>()
.min(0.8);
for rebuttal in &self.rebuttals {
if rebuttal.likelihood > 0.5 && rebuttal.severity == RebuttalSeverity::Fatal {
issues.push(ArgumentIssue {
component: ToulminComponent::Rebuttal,
issue: format!("High likelihood fatal rebuttal: {}", rebuttal.exception),
severity: IssueSeverity::Critical,
});
}
}
let base_strength =
grounds_score * 0.35 + warrant_score * 0.30 + backing_score * 0.20 + 0.15;
let qualified_strength = base_strength * self.qualifier.confidence();
let overall_strength = (qualified_strength * (1.0 - rebuttal_impact)).max(0.0);
let is_valid = warrant_score >= 0.5 && grounds_score > 0.0;
let is_sound = is_valid
&& grounds_score >= 0.6
&& !issues.iter().any(|i| i.severity == IssueSeverity::Critical);
ArgumentEvaluation {
overall_strength,
grounds_score,
warrant_score,
backing_score,
rebuttal_impact,
issues,
is_valid,
is_sound,
}
}
pub fn format(&self) -> String {
let mut output = String::new();
output
.push_str("┌─────────────────────────────────────────────────────────────────────┐\n");
output
.push_str("│ TOULMIN ARGUMENT STRUCTURE │\n");
output
.push_str("├─────────────────────────────────────────────────────────────────────┤\n");
output.push_str(&format!(
"│ CLAIM ({:?}, {:?}): \n",
self.claim.claim_type, self.claim.scope
));
output.push_str(&format!(
"│ {} {}\n",
self.qualifier.label(),
self.claim.statement
));
output
.push_str("├─────────────────────────────────────────────────────────────────────┤\n");
output.push_str("│ GROUNDS (Evidence): \n");
for ground in &self.grounds {
output.push_str(&format!(
"│ • [{}] {} (credibility: {:.0}%)\n",
format!("{:?}", ground.evidence_type).to_uppercase(),
ground.evidence,
ground.credibility * 100.0
));
}
if let Some(ref warrant) = self.warrant {
output.push_str(
"├─────────────────────────────────────────────────────────────────────┤\n",
);
output.push_str(&format!(
"│ WARRANT ({:?}, strength: {:.0}%): \n",
warrant.warrant_type,
warrant.strength * 100.0
));
output.push_str(&format!("│ {}\n", warrant.principle));
}
if !self.backing.is_empty() {
output.push_str(
"├─────────────────────────────────────────────────────────────────────┤\n",
);
output.push_str(
"│ BACKING: \n",
);
for backing in &self.backing {
output.push_str(&format!("│ • {}\n", backing.support));
}
}
if !self.rebuttals.is_empty() {
output.push_str(
"├─────────────────────────────────────────────────────────────────────┤\n",
);
output.push_str(
"│ REBUTTALS (Exceptions): \n",
);
for rebuttal in &self.rebuttals {
output.push_str(&format!(
"│ • UNLESS: {} ({:?}, {:.0}% likely)\n",
rebuttal.exception,
rebuttal.severity,
rebuttal.likelihood * 100.0
));
}
}
output
.push_str("└─────────────────────────────────────────────────────────────────────┘\n");
output
}
}
pub struct ToulminPrompts;
impl ToulminPrompts {
pub fn analyze_claim(claim: &str) -> String {
format!(
r#"Analyze this claim using the Toulmin model of argumentation.
CLAIM: {claim}
Provide a structured analysis with:
1. CLAIM CLASSIFICATION
- Type: Fact/Value/Policy/Prediction/Causal/Definition
- Scope: Universal/General/Particular/Singular
2. GROUNDS (Evidence needed)
- What evidence would support this claim?
- What type of evidence (Statistical/Testimonial/Example/Documentary/Empirical)?
- Rate credibility (0-100%)
3. WARRANT (Logical bridge)
- What principle connects the evidence to the claim?
- Type: Authority/Causal/Classification/Sign/Comparison/Generalization/Principle
- Is it explicit or assumed?
4. BACKING (Warrant support)
- What supports the warrant itself?
- Source type: Legal/Scientific/Historical/Cultural/Consensus
5. QUALIFIER (Certainty level)
- How certain is this claim? (Certainly/Presumably/Probably/Possibly/Unlikely)
6. REBUTTALS (Exceptions)
- What conditions would invalidate this claim?
- How likely are these exceptions?
- Severity: Minor/Moderate/Major/Fatal
Respond in JSON format."#,
claim = claim
)
}
pub fn evaluate_argument(argument: &str) -> String {
format!(
r#"Evaluate this argument for logical strength and soundness.
ARGUMENT:
{argument}
Identify:
1. The main CLAIM
2. The supporting GROUNDS (evidence)
3. The WARRANT (logical connection)
4. Any BACKING for the warrant
5. The QUALIFIER (certainty level)
6. Potential REBUTTALS
Then evaluate:
- Grounds quality (0-100%)
- Warrant validity (0-100%)
- Overall argument strength (0-100%)
- Is it VALID? (logical structure correct)
- Is it SOUND? (valid AND true premises)
List any logical fallacies or weaknesses found.
Respond in JSON format."#,
argument = argument
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_argument_builder() {
let argument = ArgumentBuilder::new()
.claim("Climate change is caused by human activity")
.grounds("CO2 levels have risen 50% since industrialization")
.warrant("CO2 is a greenhouse gas that traps heat")
.backing("Established physics of radiative forcing")
.qualifier(Qualifier::Presumably)
.rebuttal("Unless natural cycles are the primary driver")
.build()
.unwrap();
assert_eq!(argument.grounds.len(), 1);
assert!(argument.warrant.is_some());
assert_eq!(argument.rebuttals.len(), 1);
}
#[test]
fn test_argument_evaluation() {
let argument = ArgumentBuilder::new()
.claim("Regular exercise improves health")
.grounds_full(
"Meta-analysis of 100 studies shows 30% reduction in mortality",
EvidenceType::Statistical,
Some("Lancet 2023".into()),
0.9,
)
.warrant_full(
"Physical activity strengthens cardiovascular system",
WarrantType::Causal,
0.95,
)
.backing("Established medical consensus")
.qualifier(Qualifier::Presumably)
.build()
.unwrap();
let eval = argument.evaluate();
assert!(eval.is_valid);
assert!(eval.is_sound);
assert!(eval.overall_strength > 0.7);
}
#[test]
fn test_weak_argument() {
let argument = ArgumentBuilder::new()
.claim("All swans are white")
.grounds_full(
"I've only seen white swans",
EvidenceType::Example,
None,
0.3,
)
.qualifier(Qualifier::Certainly)
.rebuttal_full(
"Black swans exist in Australia",
0.9,
RebuttalSeverity::Fatal,
)
.build()
.unwrap();
let eval = argument.evaluate();
assert!(!eval.is_sound);
assert!(eval.overall_strength < 0.5);
assert!(!eval.issues.is_empty());
}
#[test]
fn test_qualifier_confidence() {
assert!(Qualifier::Certainly.confidence() > Qualifier::Probably.confidence());
assert!(Qualifier::Probably.confidence() > Qualifier::Possibly.confidence());
}
}