reasonkit/thinktool/
toulmin.rs

1//! # Toulmin Argumentation Model
2//!
3//! Implements the Toulmin model of argumentation for structured reasoning.
4//! Based on Stephen Toulmin's "The Uses of Argument" (1958).
5//!
6//! ## Scientific Foundation
7//!
8//! - Toulmin (1958): The Uses of Argument - foundational argumentation theory
9//! - NL2FOL (arXiv:2405.02318): Achieves 78-80% F1 on fallacy detection
10//!
11//! ## The Six Components
12//!
13//! ```text
14//! ┌─────────────────────────────────────────────────────────────────────┐
15//! │                    TOULMIN ARGUMENT MODEL                          │
16//! ├─────────────────────────────────────────────────────────────────────┤
17//! │                                                                     │
18//! │   GROUNDS ──────────► WARRANT ──────────► CLAIM                    │
19//! │   (Evidence)           (Connection)        (Conclusion)            │
20//! │       ▲                    │                   ▲                   │
21//! │       │                    ▼                   │                   │
22//! │       │               BACKING             QUALIFIER                │
23//! │       │           (Support for           (Strength)                │
24//! │       │             warrant)                 │                     │
25//! │       │                                      ▼                     │
26//! │       └──────────────────────────────── REBUTTAL                  │
27//! │                                       (Exceptions)                 │
28//! └─────────────────────────────────────────────────────────────────────┘
29//! ```
30//!
31//! ## Usage
32//!
33//! ```rust,ignore
34//! use reasonkit::thinktool::toulmin::{ToulminArgument, ArgumentBuilder};
35//!
36//! let argument = ArgumentBuilder::new()
37//!     .claim("AI will transform education")
38//!     .grounds("Studies show 40% improvement in learning outcomes")
39//!     .warrant("Personalized learning leads to better outcomes")
40//!     .backing("Meta-analysis of 200 educational AI studies")
41//!     .qualifier(Qualifier::Probably)
42//!     .rebuttal("Unless access inequality persists")
43//!     .build()?;
44//!
45//! let evaluation = argument.evaluate()?;
46//! ```
47
48use serde::{Deserialize, Serialize};
49
50/// The six components of a Toulmin argument
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToulminArgument {
53    /// The assertion being made
54    pub claim: Claim,
55    /// Evidence supporting the claim
56    pub grounds: Vec<Ground>,
57    /// Logical bridge from grounds to claim
58    pub warrant: Option<Warrant>,
59    /// Support for the warrant itself
60    pub backing: Vec<Backing>,
61    /// Strength/certainty modifier
62    pub qualifier: Qualifier,
63    /// Conditions that would invalidate the claim
64    pub rebuttals: Vec<Rebuttal>,
65}
66
67/// The main claim/conclusion of the argument
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Claim {
70    /// The assertion text
71    pub statement: String,
72    /// Type of claim
73    pub claim_type: ClaimType,
74    /// Scope of the claim
75    pub scope: Scope,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum ClaimType {
80    /// Statement of fact
81    Fact,
82    /// Value judgment
83    Value,
84    /// Proposed action/policy
85    Policy,
86    /// Predicted outcome
87    Prediction,
88    /// Causal relationship
89    Causal,
90    /// Definition or classification
91    Definition,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95pub enum Scope {
96    /// Applies to all cases
97    Universal,
98    /// Applies to most cases
99    General,
100    /// Applies to some cases
101    Particular,
102    /// Applies to specific case
103    Singular,
104}
105
106/// Evidence/data supporting the claim
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Ground {
109    /// The evidence statement
110    pub evidence: String,
111    /// Type of evidence
112    pub evidence_type: EvidenceType,
113    /// Source of the evidence
114    pub source: Option<String>,
115    /// Credibility score (0.0-1.0)
116    pub credibility: f32,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120pub enum EvidenceType {
121    /// Statistical data
122    Statistical,
123    /// Expert testimony
124    Testimonial,
125    /// Concrete example
126    Example,
127    /// Documentary evidence
128    Documentary,
129    /// Physical/empirical evidence
130    Empirical,
131    /// Common knowledge
132    CommonGround,
133    /// Analogical reasoning
134    Analogical,
135}
136
137/// Logical principle connecting grounds to claim
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Warrant {
140    /// The principle/rule being invoked
141    pub principle: String,
142    /// Type of warrant
143    pub warrant_type: WarrantType,
144    /// Whether this is explicit or implicit
145    pub is_explicit: bool,
146    /// Strength of the warrant
147    pub strength: f32,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum WarrantType {
152    /// Based on authority
153    Authority,
154    /// Based on cause-effect
155    Causal,
156    /// Based on classification
157    Classification,
158    /// Based on signs/indicators
159    Sign,
160    /// Based on comparison
161    Comparison,
162    /// Based on generalization
163    Generalization,
164    /// Based on principle/rule
165    Principle,
166}
167
168/// Support for the warrant itself
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct Backing {
171    /// The supporting statement
172    pub support: String,
173    /// Type of backing
174    pub backing_type: BackingType,
175    /// Source if applicable
176    pub source: Option<String>,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum BackingType {
181    /// Legal or regulatory
182    Legal,
183    /// Scientific research
184    Scientific,
185    /// Historical precedent
186    Historical,
187    /// Cultural norm
188    Cultural,
189    /// Expert consensus
190    Consensus,
191}
192
193/// Qualifier - degree of certainty
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195pub enum Qualifier {
196    /// Certainly true (100%)
197    Certainly,
198    /// Very likely (90%+)
199    Presumably,
200    /// Probably (70%+)
201    Probably,
202    /// Possibly (50%+)
203    Possibly,
204    /// Unlikely (<50%)
205    Unlikely,
206    /// Qualified by specific condition
207    Conditionally,
208}
209
210impl Qualifier {
211    pub fn confidence(&self) -> f32 {
212        match self {
213            Qualifier::Certainly => 0.99,
214            Qualifier::Presumably => 0.90,
215            Qualifier::Probably => 0.75,
216            Qualifier::Possibly => 0.50,
217            Qualifier::Unlikely => 0.25,
218            Qualifier::Conditionally => 0.60,
219        }
220    }
221
222    pub fn label(&self) -> &'static str {
223        match self {
224            Qualifier::Certainly => "certainly",
225            Qualifier::Presumably => "presumably",
226            Qualifier::Probably => "probably",
227            Qualifier::Possibly => "possibly",
228            Qualifier::Unlikely => "unlikely",
229            Qualifier::Conditionally => "if conditions hold",
230        }
231    }
232}
233
234/// Conditions that would invalidate the claim
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct Rebuttal {
237    /// The exception/condition
238    pub exception: String,
239    /// How likely is this exception
240    pub likelihood: f32,
241    /// Severity if exception occurs
242    pub severity: RebuttalSeverity,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub enum RebuttalSeverity {
247    /// Minor qualification
248    Minor,
249    /// Moderate impact
250    Moderate,
251    /// Major impact on claim
252    Major,
253    /// Completely defeats claim
254    Fatal,
255}
256
257/// Builder for constructing Toulmin arguments
258#[derive(Debug, Default)]
259pub struct ArgumentBuilder {
260    claim: Option<Claim>,
261    grounds: Vec<Ground>,
262    warrant: Option<Warrant>,
263    backing: Vec<Backing>,
264    qualifier: Option<Qualifier>,
265    rebuttals: Vec<Rebuttal>,
266}
267
268impl ArgumentBuilder {
269    pub fn new() -> Self {
270        Self::default()
271    }
272
273    pub fn claim(mut self, statement: impl Into<String>) -> Self {
274        self.claim = Some(Claim {
275            statement: statement.into(),
276            claim_type: ClaimType::Fact,
277            scope: Scope::General,
278        });
279        self
280    }
281
282    pub fn claim_full(
283        mut self,
284        statement: impl Into<String>,
285        claim_type: ClaimType,
286        scope: Scope,
287    ) -> Self {
288        self.claim = Some(Claim {
289            statement: statement.into(),
290            claim_type,
291            scope,
292        });
293        self
294    }
295
296    pub fn grounds(mut self, evidence: impl Into<String>) -> Self {
297        self.grounds.push(Ground {
298            evidence: evidence.into(),
299            evidence_type: EvidenceType::Empirical,
300            source: None,
301            credibility: 0.7,
302        });
303        self
304    }
305
306    pub fn grounds_full(
307        mut self,
308        evidence: impl Into<String>,
309        evidence_type: EvidenceType,
310        source: Option<String>,
311        credibility: f32,
312    ) -> Self {
313        self.grounds.push(Ground {
314            evidence: evidence.into(),
315            evidence_type,
316            source,
317            credibility,
318        });
319        self
320    }
321
322    pub fn warrant(mut self, principle: impl Into<String>) -> Self {
323        self.warrant = Some(Warrant {
324            principle: principle.into(),
325            warrant_type: WarrantType::Principle,
326            is_explicit: true,
327            strength: 0.8,
328        });
329        self
330    }
331
332    pub fn warrant_full(
333        mut self,
334        principle: impl Into<String>,
335        warrant_type: WarrantType,
336        strength: f32,
337    ) -> Self {
338        self.warrant = Some(Warrant {
339            principle: principle.into(),
340            warrant_type,
341            is_explicit: true,
342            strength,
343        });
344        self
345    }
346
347    pub fn backing(mut self, support: impl Into<String>) -> Self {
348        self.backing.push(Backing {
349            support: support.into(),
350            backing_type: BackingType::Scientific,
351            source: None,
352        });
353        self
354    }
355
356    pub fn qualifier(mut self, qualifier: Qualifier) -> Self {
357        self.qualifier = Some(qualifier);
358        self
359    }
360
361    pub fn rebuttal(mut self, exception: impl Into<String>) -> Self {
362        self.rebuttals.push(Rebuttal {
363            exception: exception.into(),
364            likelihood: 0.3,
365            severity: RebuttalSeverity::Moderate,
366        });
367        self
368    }
369
370    pub fn rebuttal_full(
371        mut self,
372        exception: impl Into<String>,
373        likelihood: f32,
374        severity: RebuttalSeverity,
375    ) -> Self {
376        self.rebuttals.push(Rebuttal {
377            exception: exception.into(),
378            likelihood,
379            severity,
380        });
381        self
382    }
383
384    pub fn build(self) -> Result<ToulminArgument, ArgumentError> {
385        let claim = self.claim.ok_or(ArgumentError::MissingClaim)?;
386
387        if self.grounds.is_empty() {
388            return Err(ArgumentError::MissingGrounds);
389        }
390
391        Ok(ToulminArgument {
392            claim,
393            grounds: self.grounds,
394            warrant: self.warrant,
395            backing: self.backing,
396            qualifier: self.qualifier.unwrap_or(Qualifier::Probably),
397            rebuttals: self.rebuttals,
398        })
399    }
400}
401
402#[derive(Debug, Clone)]
403pub enum ArgumentError {
404    MissingClaim,
405    MissingGrounds,
406    InvalidWarrant(String),
407}
408
409impl std::fmt::Display for ArgumentError {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        match self {
412            ArgumentError::MissingClaim => write!(f, "Argument requires a claim"),
413            ArgumentError::MissingGrounds => write!(f, "Argument requires at least one ground"),
414            ArgumentError::InvalidWarrant(msg) => write!(f, "Invalid warrant: {}", msg),
415        }
416    }
417}
418
419impl std::error::Error for ArgumentError {}
420
421/// Evaluation of an argument's strength
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ArgumentEvaluation {
424    /// Overall argument strength (0.0-1.0)
425    pub overall_strength: f32,
426    /// Grounds quality
427    pub grounds_score: f32,
428    /// Warrant validity
429    pub warrant_score: f32,
430    /// Backing support
431    pub backing_score: f32,
432    /// Impact of rebuttals
433    pub rebuttal_impact: f32,
434    /// Issues found
435    pub issues: Vec<ArgumentIssue>,
436    /// Is the argument valid?
437    pub is_valid: bool,
438    /// Is the argument sound?
439    pub is_sound: bool,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct ArgumentIssue {
444    pub component: ToulminComponent,
445    pub issue: String,
446    pub severity: IssueSeverity,
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
450pub enum ToulminComponent {
451    Claim,
452    Grounds,
453    Warrant,
454    Backing,
455    Qualifier,
456    Rebuttal,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
460pub enum IssueSeverity {
461    Minor,
462    Moderate,
463    Serious,
464    Critical,
465}
466
467impl ToulminArgument {
468    /// Evaluate the argument's strength
469    pub fn evaluate(&self) -> ArgumentEvaluation {
470        let mut issues = Vec::new();
471
472        // Score grounds
473        let grounds_score = if self.grounds.is_empty() {
474            issues.push(ArgumentIssue {
475                component: ToulminComponent::Grounds,
476                issue: "No supporting evidence provided".into(),
477                severity: IssueSeverity::Critical,
478            });
479            0.0
480        } else {
481            self.grounds.iter().map(|g| g.credibility).sum::<f32>() / self.grounds.len() as f32
482        };
483
484        // Score warrant
485        let warrant_score = if let Some(ref w) = self.warrant {
486            if !w.is_explicit {
487                issues.push(ArgumentIssue {
488                    component: ToulminComponent::Warrant,
489                    issue: "Warrant is implicit - should be made explicit".into(),
490                    severity: IssueSeverity::Minor,
491                });
492            }
493            w.strength
494        } else {
495            issues.push(ArgumentIssue {
496                component: ToulminComponent::Warrant,
497                issue: "Missing warrant connecting evidence to claim".into(),
498                severity: IssueSeverity::Serious,
499            });
500            0.3 // Implicit warrant assumed
501        };
502
503        // Score backing
504        let backing_score = if self.backing.is_empty() {
505            0.5 // Neutral - backing not always needed
506        } else {
507            0.7 + 0.1 * self.backing.len().min(3) as f32
508        };
509
510        // Calculate rebuttal impact
511        let rebuttal_impact: f32 = self
512            .rebuttals
513            .iter()
514            .map(|r| {
515                let severity_weight = match r.severity {
516                    RebuttalSeverity::Minor => 0.1,
517                    RebuttalSeverity::Moderate => 0.25,
518                    RebuttalSeverity::Major => 0.5,
519                    RebuttalSeverity::Fatal => 1.0,
520                };
521                r.likelihood * severity_weight
522            })
523            .sum::<f32>()
524            .min(0.8); // Cap at 80% reduction
525
526        // Add issues for high-impact rebuttals
527        for rebuttal in &self.rebuttals {
528            if rebuttal.likelihood > 0.5 && rebuttal.severity == RebuttalSeverity::Fatal {
529                issues.push(ArgumentIssue {
530                    component: ToulminComponent::Rebuttal,
531                    issue: format!("High likelihood fatal rebuttal: {}", rebuttal.exception),
532                    severity: IssueSeverity::Critical,
533                });
534            }
535        }
536
537        // Overall strength calculation
538        let base_strength =
539            grounds_score * 0.35 + warrant_score * 0.30 + backing_score * 0.20 + 0.15;
540
541        let qualified_strength = base_strength * self.qualifier.confidence();
542        let overall_strength = (qualified_strength * (1.0 - rebuttal_impact)).max(0.0);
543
544        // Determine validity and soundness
545        let is_valid = warrant_score >= 0.5 && grounds_score > 0.0;
546        let is_sound = is_valid
547            && grounds_score >= 0.6
548            && !issues.iter().any(|i| i.severity == IssueSeverity::Critical);
549
550        ArgumentEvaluation {
551            overall_strength,
552            grounds_score,
553            warrant_score,
554            backing_score,
555            rebuttal_impact,
556            issues,
557            is_valid,
558            is_sound,
559        }
560    }
561
562    /// Format as structured text
563    pub fn format(&self) -> String {
564        let mut output = String::new();
565
566        output
567            .push_str("┌─────────────────────────────────────────────────────────────────────┐\n");
568        output
569            .push_str("│                    TOULMIN ARGUMENT STRUCTURE                       │\n");
570        output
571            .push_str("├─────────────────────────────────────────────────────────────────────┤\n");
572
573        // Claim
574        output.push_str(&format!(
575            "│ CLAIM ({:?}, {:?}):                                                   \n",
576            self.claim.claim_type, self.claim.scope
577        ));
578        output.push_str(&format!(
579            "│   {} {}\n",
580            self.qualifier.label(),
581            self.claim.statement
582        ));
583
584        // Grounds
585        output
586            .push_str("├─────────────────────────────────────────────────────────────────────┤\n");
587        output.push_str("│ GROUNDS (Evidence):                                                 \n");
588        for ground in &self.grounds {
589            output.push_str(&format!(
590                "│   • [{}] {} (credibility: {:.0}%)\n",
591                format!("{:?}", ground.evidence_type).to_uppercase(),
592                ground.evidence,
593                ground.credibility * 100.0
594            ));
595        }
596
597        // Warrant
598        if let Some(ref warrant) = self.warrant {
599            output.push_str(
600                "├─────────────────────────────────────────────────────────────────────┤\n",
601            );
602            output.push_str(&format!(
603                "│ WARRANT ({:?}, strength: {:.0}%):                                   \n",
604                warrant.warrant_type,
605                warrant.strength * 100.0
606            ));
607            output.push_str(&format!("│   {}\n", warrant.principle));
608        }
609
610        // Backing
611        if !self.backing.is_empty() {
612            output.push_str(
613                "├─────────────────────────────────────────────────────────────────────┤\n",
614            );
615            output.push_str(
616                "│ BACKING:                                                            \n",
617            );
618            for backing in &self.backing {
619                output.push_str(&format!("│   • {}\n", backing.support));
620            }
621        }
622
623        // Rebuttals
624        if !self.rebuttals.is_empty() {
625            output.push_str(
626                "├─────────────────────────────────────────────────────────────────────┤\n",
627            );
628            output.push_str(
629                "│ REBUTTALS (Exceptions):                                             \n",
630            );
631            for rebuttal in &self.rebuttals {
632                output.push_str(&format!(
633                    "│   • UNLESS: {} ({:?}, {:.0}% likely)\n",
634                    rebuttal.exception,
635                    rebuttal.severity,
636                    rebuttal.likelihood * 100.0
637                ));
638            }
639        }
640
641        output
642            .push_str("└─────────────────────────────────────────────────────────────────────┘\n");
643
644        output
645    }
646}
647
648/// Prompt templates for generating Toulmin arguments
649pub struct ToulminPrompts;
650
651impl ToulminPrompts {
652    /// Generate a Toulmin analysis of a claim
653    pub fn analyze_claim(claim: &str) -> String {
654        format!(
655            r#"Analyze this claim using the Toulmin model of argumentation.
656
657CLAIM: {claim}
658
659Provide a structured analysis with:
660
6611. CLAIM CLASSIFICATION
662   - Type: Fact/Value/Policy/Prediction/Causal/Definition
663   - Scope: Universal/General/Particular/Singular
664
6652. GROUNDS (Evidence needed)
666   - What evidence would support this claim?
667   - What type of evidence (Statistical/Testimonial/Example/Documentary/Empirical)?
668   - Rate credibility (0-100%)
669
6703. WARRANT (Logical bridge)
671   - What principle connects the evidence to the claim?
672   - Type: Authority/Causal/Classification/Sign/Comparison/Generalization/Principle
673   - Is it explicit or assumed?
674
6754. BACKING (Warrant support)
676   - What supports the warrant itself?
677   - Source type: Legal/Scientific/Historical/Cultural/Consensus
678
6795. QUALIFIER (Certainty level)
680   - How certain is this claim? (Certainly/Presumably/Probably/Possibly/Unlikely)
681
6826. REBUTTALS (Exceptions)
683   - What conditions would invalidate this claim?
684   - How likely are these exceptions?
685   - Severity: Minor/Moderate/Major/Fatal
686
687Respond in JSON format."#,
688            claim = claim
689        )
690    }
691
692    /// Evaluate argument strength
693    pub fn evaluate_argument(argument: &str) -> String {
694        format!(
695            r#"Evaluate this argument for logical strength and soundness.
696
697ARGUMENT:
698{argument}
699
700Identify:
7011. The main CLAIM
7022. The supporting GROUNDS (evidence)
7033. The WARRANT (logical connection)
7044. Any BACKING for the warrant
7055. The QUALIFIER (certainty level)
7066. Potential REBUTTALS
707
708Then evaluate:
709- Grounds quality (0-100%)
710- Warrant validity (0-100%)
711- Overall argument strength (0-100%)
712- Is it VALID? (logical structure correct)
713- Is it SOUND? (valid AND true premises)
714
715List any logical fallacies or weaknesses found.
716
717Respond in JSON format."#,
718            argument = argument
719        )
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[test]
728    fn test_argument_builder() {
729        let argument = ArgumentBuilder::new()
730            .claim("Climate change is caused by human activity")
731            .grounds("CO2 levels have risen 50% since industrialization")
732            .warrant("CO2 is a greenhouse gas that traps heat")
733            .backing("Established physics of radiative forcing")
734            .qualifier(Qualifier::Presumably)
735            .rebuttal("Unless natural cycles are the primary driver")
736            .build()
737            .unwrap();
738
739        assert_eq!(argument.grounds.len(), 1);
740        assert!(argument.warrant.is_some());
741        assert_eq!(argument.rebuttals.len(), 1);
742    }
743
744    #[test]
745    fn test_argument_evaluation() {
746        let argument = ArgumentBuilder::new()
747            .claim("Regular exercise improves health")
748            .grounds_full(
749                "Meta-analysis of 100 studies shows 30% reduction in mortality",
750                EvidenceType::Statistical,
751                Some("Lancet 2023".into()),
752                0.9,
753            )
754            .warrant_full(
755                "Physical activity strengthens cardiovascular system",
756                WarrantType::Causal,
757                0.95,
758            )
759            .backing("Established medical consensus")
760            .qualifier(Qualifier::Presumably)
761            .build()
762            .unwrap();
763
764        let eval = argument.evaluate();
765
766        assert!(eval.is_valid);
767        assert!(eval.is_sound);
768        assert!(eval.overall_strength > 0.7);
769    }
770
771    #[test]
772    fn test_weak_argument() {
773        let argument = ArgumentBuilder::new()
774            .claim("All swans are white")
775            .grounds_full(
776                "I've only seen white swans",
777                EvidenceType::Example,
778                None,
779                0.3,
780            )
781            .qualifier(Qualifier::Certainly)
782            .rebuttal_full(
783                "Black swans exist in Australia",
784                0.9,
785                RebuttalSeverity::Fatal,
786            )
787            .build()
788            .unwrap();
789
790        let eval = argument.evaluate();
791
792        assert!(!eval.is_sound);
793        assert!(eval.overall_strength < 0.5);
794        assert!(!eval.issues.is_empty());
795    }
796
797    #[test]
798    fn test_qualifier_confidence() {
799        assert!(Qualifier::Certainly.confidence() > Qualifier::Probably.confidence());
800        assert!(Qualifier::Probably.confidence() > Qualifier::Possibly.confidence());
801    }
802}