1use serde::{Deserialize, Serialize};
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToulminArgument {
53 pub claim: Claim,
55 pub grounds: Vec<Ground>,
57 pub warrant: Option<Warrant>,
59 pub backing: Vec<Backing>,
61 pub qualifier: Qualifier,
63 pub rebuttals: Vec<Rebuttal>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Claim {
70 pub statement: String,
72 pub claim_type: ClaimType,
74 pub scope: Scope,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub enum ClaimType {
80 Fact,
82 Value,
84 Policy,
86 Prediction,
88 Causal,
90 Definition,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95pub enum Scope {
96 Universal,
98 General,
100 Particular,
102 Singular,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Ground {
109 pub evidence: String,
111 pub evidence_type: EvidenceType,
113 pub source: Option<String>,
115 pub credibility: f32,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120pub enum EvidenceType {
121 Statistical,
123 Testimonial,
125 Example,
127 Documentary,
129 Empirical,
131 CommonGround,
133 Analogical,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Warrant {
140 pub principle: String,
142 pub warrant_type: WarrantType,
144 pub is_explicit: bool,
146 pub strength: f32,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151pub enum WarrantType {
152 Authority,
154 Causal,
156 Classification,
158 Sign,
160 Comparison,
162 Generalization,
164 Principle,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct Backing {
171 pub support: String,
173 pub backing_type: BackingType,
175 pub source: Option<String>,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180pub enum BackingType {
181 Legal,
183 Scientific,
185 Historical,
187 Cultural,
189 Consensus,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195pub enum Qualifier {
196 Certainly,
198 Presumably,
200 Probably,
202 Possibly,
204 Unlikely,
206 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#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct Rebuttal {
237 pub exception: String,
239 pub likelihood: f32,
241 pub severity: RebuttalSeverity,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub enum RebuttalSeverity {
247 Minor,
249 Moderate,
251 Major,
253 Fatal,
255}
256
257#[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#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ArgumentEvaluation {
424 pub overall_strength: f32,
426 pub grounds_score: f32,
428 pub warrant_score: f32,
430 pub backing_score: f32,
432 pub rebuttal_impact: f32,
434 pub issues: Vec<ArgumentIssue>,
436 pub is_valid: bool,
438 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 pub fn evaluate(&self) -> ArgumentEvaluation {
470 let mut issues = Vec::new();
471
472 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 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 };
502
503 let backing_score = if self.backing.is_empty() {
505 0.5 } else {
507 0.7 + 0.1 * self.backing.len().min(3) as f32
508 };
509
510 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); 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 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 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 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 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 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 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 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 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
648pub struct ToulminPrompts;
650
651impl ToulminPrompts {
652 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 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}