reasonkit/thinktool/
yaml_loader.rs

1//! YAML Protocol Loader
2//!
3//! Loads ThinkTool protocols from YAML files (thinktools.yaml format).
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::Path;
8
9use super::profiles::ReasoningProfile;
10use super::protocol::{
11    AggregationType, CritiqueSeverity, DecisionMethod, InputSpec, OutputSpec, Protocol,
12    ProtocolMetadata, ProtocolStep, ReasoningStrategy, StepAction, StepOutputFormat,
13};
14use crate::error::{Error, Result};
15
16/// YAML ThinkTool module definition
17#[derive(Debug, Clone, Deserialize, Serialize)]
18struct YamlThinkToolModule {
19    id: String,
20    name: String,
21    #[serde(default)]
22    shortcode: String,
23    category: String,
24    tier: String,
25    description: String,
26    capabilities: Vec<String>,
27    #[serde(default)]
28    output_schema: String,
29    parameters: HashMap<String, serde_json::Value>,
30    #[serde(default)]
31    confidence_factors: Vec<YamlConfidenceFactor>,
32    thinking_pattern: YamlThinkingPattern,
33    #[serde(default)]
34    typical_duration: String,
35    #[serde(default)]
36    token_cost_estimate: String,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize)]
40struct YamlConfidenceFactor {
41    factor: String,
42    weight: f64,
43    formula: String,
44}
45
46#[derive(Debug, Clone, Deserialize, Serialize)]
47struct YamlThinkingPattern {
48    #[serde(rename = "type")]
49    pattern_type: String,
50    steps: Vec<String>,
51}
52
53/// Root YAML structure for thinktools.yaml
54#[derive(Debug, Clone, Deserialize, Serialize)]
55struct YamlThinkToolsV2 {
56    version: String,
57    schema: String,
58    #[serde(default)]
59    thinktool_modules: HashMap<String, YamlThinkToolModule>,
60}
61
62/// Load protocols from the thinktools.yaml file
63pub fn load_from_yaml_file(path: &Path) -> Result<Vec<Protocol>> {
64    let content = std::fs::read_to_string(path).map_err(|e| Error::IoMessage {
65        message: format!("Failed to read YAML file {}: {}", path.display(), e),
66    })?;
67
68    load_from_yaml_string(&content)
69}
70
71/// Load protocols from a YAML string
72pub fn load_from_yaml_string(yaml_content: &str) -> Result<Vec<Protocol>> {
73    let yaml_data: YamlThinkToolsV2 =
74        serde_yaml::from_str(yaml_content).map_err(|e| Error::Parse {
75            message: format!("Failed to parse YAML: {}", e),
76        })?;
77
78    let mut protocols = Vec::new();
79
80    for (module_key, module) in yaml_data.thinktool_modules {
81        let protocol = convert_yaml_module_to_protocol(&module_key, &module)?;
82        protocols.push(protocol);
83    }
84
85    Ok(protocols)
86}
87
88/// Load profiles from a YAML file
89pub fn load_profiles_from_yaml_file(path: &Path) -> Result<Vec<ReasoningProfile>> {
90    let content = std::fs::read_to_string(path).map_err(|e| Error::IoMessage {
91        message: format!("Failed to read YAML file {}: {}", path.display(), e),
92    })?;
93
94    let profiles: Vec<ReasoningProfile> =
95        serde_yaml::from_str(&content).map_err(|e| Error::Parse {
96            message: format!("Failed to parse profiles YAML: {}", e),
97        })?;
98
99    Ok(profiles)
100}
101
102/// Convert a YAML module definition to a Protocol struct
103fn convert_yaml_module_to_protocol(
104    module_key: &str,
105    yaml_module: &YamlThinkToolModule,
106) -> Result<Protocol> {
107    // Determine reasoning strategy from category
108    let strategy = match yaml_module.category.as_str() {
109        "divergent" => ReasoningStrategy::Expansive,
110        "convergent" => ReasoningStrategy::Deductive,
111        "foundational" => ReasoningStrategy::Analytical,
112        "verification" => ReasoningStrategy::Verification,
113        "adversarial" => ReasoningStrategy::Adversarial,
114        _ => ReasoningStrategy::Analytical,
115    };
116
117    // Build input spec based on module type
118    let input = build_input_spec(module_key);
119
120    // Build steps from thinking pattern
121    let steps = build_steps_from_pattern(&yaml_module.thinking_pattern, module_key)?;
122
123    // Build output spec
124    let output = build_output_spec(&yaml_module.name);
125
126    // Build metadata
127    let metadata = ProtocolMetadata {
128        category: yaml_module.category.clone(),
129        composable_with: get_composable_modules(module_key),
130        typical_tokens: estimate_tokens(&yaml_module.token_cost_estimate),
131        estimated_latency_ms: estimate_latency(&yaml_module.typical_duration),
132        ..Default::default()
133    };
134
135    let protocol = Protocol {
136        id: module_key.to_string(),
137        name: yaml_module.name.clone(),
138        version: "2.0.0".to_string(),
139        description: yaml_module.description.trim().to_string(),
140        strategy,
141        input,
142        steps,
143        output,
144        validation: Vec::new(),
145        metadata,
146    };
147
148    // Validate the protocol
149    protocol.validate().map_err(|errors| {
150        Error::Validation(format!(
151            "Invalid protocol {}: {}",
152            protocol.id,
153            errors.join(", ")
154        ))
155    })?;
156
157    Ok(protocol)
158}
159
160/// Build input specification based on module type
161fn build_input_spec(module_key: &str) -> InputSpec {
162    match module_key {
163        "gigathink" => InputSpec {
164            required: vec!["query".to_string()],
165            optional: vec!["context".to_string(), "constraints".to_string()],
166        },
167        "laserlogic" => InputSpec {
168            required: vec!["argument".to_string()],
169            optional: vec!["context".to_string()],
170        },
171        "bedrock" => InputSpec {
172            required: vec!["statement".to_string()],
173            optional: vec!["domain".to_string()],
174        },
175        "proofguard" => InputSpec {
176            required: vec!["claim".to_string()],
177            optional: vec!["sources".to_string()],
178        },
179        "brutalhonesty" => InputSpec {
180            required: vec!["work".to_string()],
181            optional: vec!["criteria".to_string()],
182        },
183        _ => InputSpec::default(),
184    }
185}
186
187/// Build protocol steps from thinking pattern
188fn build_steps_from_pattern(
189    _pattern: &YamlThinkingPattern,
190    module_key: &str,
191) -> Result<Vec<ProtocolStep>> {
192    match module_key {
193        "gigathink" => Ok(build_gigathink_steps()),
194        "laserlogic" => Ok(build_laserlogic_steps()),
195        "bedrock" => Ok(build_bedrock_steps()),
196        "proofguard" => Ok(build_proofguard_steps()),
197        "brutalhonesty" => Ok(build_brutalhonesty_steps()),
198        "powercombo" => Ok(build_gigathink_steps()), // Fallback to GigaThink for now
199        _ => Err(Error::Validation(format!(
200            "Unknown module type: {}",
201            module_key
202        ))),
203    }
204}
205
206/// Build GigaThink steps
207fn build_gigathink_steps() -> Vec<ProtocolStep> {
208    vec![
209        ProtocolStep {
210            id: "identify_dimensions".to_string(),
211            action: StepAction::Generate {
212                min_count: 5,
213                max_count: 10,
214            },
215            prompt_template:
216                r#"Identify 5-10 distinct dimensions or angles to analyze this question:
217
218Question: {{query}}
219{{#if context}}Context: {{context}}{{/if}}
220{{#if constraints}}Constraints: {{constraints}}{{/if}}
221
222For each dimension, provide a brief label. Format as a numbered list."#
223                    .to_string(),
224            output_format: StepOutputFormat::List,
225            min_confidence: 0.7,
226            depends_on: vec![],
227            branch: None,
228        },
229        ProtocolStep {
230            id: "explore_perspectives".to_string(),
231            action: StepAction::Analyze {
232                criteria: vec![
233                    "novelty".to_string(),
234                    "relevance".to_string(),
235                    "depth".to_string(),
236                ],
237            },
238            prompt_template: r#"For each dimension identified, provide:
2391. Key insight from this perspective
2402. Supporting evidence or example
2413. Implications or consequences
2424. Confidence score (0.0-1.0)
243
244Dimensions to explore:
245{{identify_dimensions}}
246
247Question: {{query}}"#
248                .to_string(),
249            output_format: StepOutputFormat::Structured,
250            min_confidence: 0.6,
251            depends_on: vec!["identify_dimensions".to_string()],
252            branch: None,
253        },
254        ProtocolStep {
255            id: "synthesize".to_string(),
256            action: StepAction::Synthesize {
257                aggregation: AggregationType::ThematicClustering,
258            },
259            prompt_template:
260                r#"Synthesize the perspectives into key themes and actionable insights:
261
262Perspectives:
263{{explore_perspectives}}
264
265Provide:
2661. Major themes (2-4)
2672. Key insights (3-5)
2683. Recommended actions (if applicable)
2694. Areas of uncertainty"#
270                    .to_string(),
271            output_format: StepOutputFormat::Structured,
272            min_confidence: 0.8,
273            depends_on: vec!["explore_perspectives".to_string()],
274            branch: None,
275        },
276    ]
277}
278
279/// Build LaserLogic steps
280fn build_laserlogic_steps() -> Vec<ProtocolStep> {
281    vec![
282        ProtocolStep {
283            id: "extract_claims".to_string(),
284            action: StepAction::Analyze {
285                criteria: vec!["clarity".to_string(), "completeness".to_string()],
286            },
287            prompt_template: r#"Extract the logical structure from this argument:
288
289Argument: {{argument}}
290
291Identify:
2921. Main conclusion
2932. Supporting premises
2943. Implicit assumptions
2954. Causal claims (if any)
296
297Format each as a clear statement."#
298                .to_string(),
299            output_format: StepOutputFormat::Structured,
300            min_confidence: 0.7,
301            depends_on: vec![],
302            branch: None,
303        },
304        ProtocolStep {
305            id: "check_validity".to_string(),
306            action: StepAction::Validate {
307                rules: vec![
308                    "logical_consistency".to_string(),
309                    "premise_support".to_string(),
310                ],
311            },
312            prompt_template: r#"Evaluate the logical validity of this argument analysis:
313
314{{extract_claims}}
315
316Based on the claims identified above, check:
3171. Do the premises logically lead to the conclusion?
3182. Are there gaps in the reasoning chain?
3193. Is the argument valid (logical structure) vs sound (true premises)?
3204. Rate the logical strength (0.0-1.0) with justification"#
321                .to_string(),
322            output_format: StepOutputFormat::Structured,
323            min_confidence: 0.8,
324            depends_on: vec!["extract_claims".to_string()],
325            branch: None,
326        },
327        ProtocolStep {
328            id: "detect_fallacies".to_string(),
329            action: StepAction::Critique {
330                severity: CritiqueSeverity::Standard,
331            },
332            prompt_template: r#"Check for logical fallacies in the argument:
333
334Argument structure:
335{{extract_claims}}
336
337Common fallacies to check:
338- Ad hominem, Straw man, False dichotomy
339- Appeal to authority, Circular reasoning
340- Hasty generalization, Post hoc
341- Slippery slope, Red herring
342
343For each fallacy found, explain where and why."#
344                .to_string(),
345            output_format: StepOutputFormat::List,
346            min_confidence: 0.7,
347            depends_on: vec!["extract_claims".to_string()],
348            branch: None,
349        },
350    ]
351}
352
353/// Build BedRock steps
354fn build_bedrock_steps() -> Vec<ProtocolStep> {
355    vec![
356        ProtocolStep {
357            id: "decompose".to_string(),
358            action: StepAction::Analyze {
359                criteria: vec!["fundamentality".to_string(), "independence".to_string()],
360            },
361            prompt_template: r#"Decompose this statement to first principles:
362
363Statement: {{statement}}
364{{#if domain}}Domain: {{domain}}{{/if}}
365
366Ask repeatedly: "What is this based on? Why is this true?"
367Continue until reaching fundamental axioms or assumptions.
368
369Format as a tree structure showing dependencies."#
370                .to_string(),
371            output_format: StepOutputFormat::Structured,
372            min_confidence: 0.7,
373            depends_on: vec![],
374            branch: None,
375        },
376        ProtocolStep {
377            id: "identify_axioms".to_string(),
378            action: StepAction::Generate {
379                min_count: 3,
380                max_count: 7,
381            },
382            prompt_template: r#"From the decomposition, identify the foundational axioms:
383
384Decomposition:
385{{decompose}}
386
387For each axiom:
3881. State clearly
3892. Explain why it's fundamental (cannot be further reduced)
3903. Note if it's empirical, logical, or definitional
3914. Rate certainty (0.0-1.0)"#
392                .to_string(),
393            output_format: StepOutputFormat::List,
394            min_confidence: 0.8,
395            depends_on: vec!["decompose".to_string()],
396            branch: None,
397        },
398        ProtocolStep {
399            id: "reconstruct".to_string(),
400            action: StepAction::Synthesize {
401                aggregation: AggregationType::WeightedMerge,
402            },
403            prompt_template: r#"Reconstruct the original statement from axioms:
404
405Axioms:
406{{identify_axioms}}
407
408Original statement: {{statement}}
409
410Show the logical path from axioms to statement.
411Identify any gaps or leaps in reasoning.
412Calculate overall confidence based on axiom certainties."#
413                .to_string(),
414            output_format: StepOutputFormat::Structured,
415            min_confidence: 0.75,
416            depends_on: vec!["identify_axioms".to_string()],
417            branch: None,
418        },
419    ]
420}
421
422/// Build ProofGuard steps
423fn build_proofguard_steps() -> Vec<ProtocolStep> {
424    vec![
425        ProtocolStep {
426            id: "identify_sources".to_string(),
427            action: StepAction::CrossReference { min_sources: 3 },
428            prompt_template: r#"Identify potential sources to verify this claim:
429
430Claim: {{claim}}
431{{#if sources}}Known sources: {{sources}}{{/if}}
432
433List 3+ independent sources that could verify or refute this claim.
434Prioritize: official docs, peer-reviewed, primary sources."#
435                .to_string(),
436            output_format: StepOutputFormat::List,
437            min_confidence: 0.6,
438            depends_on: vec![],
439            branch: None,
440        },
441        ProtocolStep {
442            id: "verify_each".to_string(),
443            action: StepAction::Validate {
444                rules: vec![
445                    "source_reliability".to_string(),
446                    "claim_support".to_string(),
447                ],
448            },
449            prompt_template: r#"For each source, evaluate support for the claim:
450
451Claim: {{claim}}
452Sources to check:
453{{identify_sources}}
454
455For each source:
4561. What does it say about the claim?
4572. Support level: Confirms / Partially confirms / Neutral / Contradicts
4583. Source reliability (0.0-1.0)
4594. Key quote or evidence"#
460                .to_string(),
461            output_format: StepOutputFormat::Structured,
462            min_confidence: 0.7,
463            depends_on: vec!["identify_sources".to_string()],
464            branch: None,
465        },
466        ProtocolStep {
467            id: "triangulate".to_string(),
468            action: StepAction::Synthesize {
469                aggregation: AggregationType::Consensus,
470            },
471            prompt_template: r#"Apply triangulation to determine claim validity:
472
473Claim: {{claim}}
474Source evaluations:
475{{verify_each}}
476
477Triangulation rules:
478- 3+ independent confirming sources = HIGH confidence
479- 2 confirming, 1 neutral = MEDIUM confidence
480- Mixed results = LOW confidence, note discrepancies
481- Any contradiction = FLAG for review
482
483Provide final verdict and confidence score."#
484                .to_string(),
485            output_format: StepOutputFormat::Structured,
486            min_confidence: 0.8,
487            depends_on: vec!["verify_each".to_string()],
488            branch: None,
489        },
490    ]
491}
492
493/// Build BrutalHonesty steps
494fn build_brutalhonesty_steps() -> Vec<ProtocolStep> {
495    vec![
496        ProtocolStep {
497            id: "steelman".to_string(),
498            action: StepAction::Analyze {
499                criteria: vec!["strengths".to_string()],
500            },
501            prompt_template: r#"First, steelman the work - what are its genuine strengths?
502
503Work to critique:
504{{work}}
505
506Identify:
5071. What does this do well?
5082. What problems does it solve?
5093. What is genuinely valuable here?
510
511Be generous but honest."#
512                .to_string(),
513            output_format: StepOutputFormat::List,
514            min_confidence: 0.7,
515            depends_on: vec![],
516            branch: None,
517        },
518        ProtocolStep {
519            id: "attack".to_string(),
520            action: StepAction::Critique {
521                severity: CritiqueSeverity::Brutal,
522            },
523            prompt_template: r#"Now be brutally honest - what's wrong with this?
524
525Work:
526{{work}}
527
528Strengths identified:
529{{steelman}}
530
531Attack from all angles:
5321. Logical flaws
5332. Missing considerations
5343. Weak assumptions
5354. Implementation problems
5365. Unintended consequences
5376. What would a harsh critic say?
538
539Don't hold back. Be specific."#
540                .to_string(),
541            output_format: StepOutputFormat::List,
542            min_confidence: 0.6,
543            depends_on: vec!["steelman".to_string()],
544            branch: None,
545        },
546        ProtocolStep {
547            id: "verdict".to_string(),
548            action: StepAction::Decide {
549                method: DecisionMethod::ProsCons,
550            },
551            prompt_template: r#"Final verdict - is this work acceptable?
552
553Strengths:
554{{steelman}}
555
556Flaws:
557{{attack}}
558
559Provide:
5601. Overall assessment (Pass / Conditional Pass / Fail)
5612. Most critical issue to fix
5623. Confidence in verdict (0.0-1.0)
5634. What would make this excellent?"#
564                .to_string(),
565            output_format: StepOutputFormat::Structured,
566            min_confidence: 0.75,
567            depends_on: vec!["steelman".to_string(), "attack".to_string()],
568            branch: None,
569        },
570    ]
571}
572
573/// Build output specification
574fn build_output_spec(module_name: &str) -> OutputSpec {
575    let format = format!("{}Result", module_name.replace(" ", ""));
576    let fields = match module_name {
577        "GigaThink" => vec![
578            "dimensions".to_string(),
579            "perspectives".to_string(),
580            "themes".to_string(),
581            "insights".to_string(),
582            "confidence".to_string(),
583        ],
584        "LaserLogic" => vec![
585            "conclusion".to_string(),
586            "premises".to_string(),
587            "validity".to_string(),
588            "fallacies".to_string(),
589            "confidence".to_string(),
590        ],
591        "BedRock" => vec![
592            "axioms".to_string(),
593            "decomposition".to_string(),
594            "reconstruction".to_string(),
595            "gaps".to_string(),
596            "confidence".to_string(),
597        ],
598        "ProofGuard" => vec![
599            "verdict".to_string(),
600            "sources".to_string(),
601            "evidence".to_string(),
602            "discrepancies".to_string(),
603            "confidence".to_string(),
604        ],
605        "BrutalHonesty" => vec![
606            "strengths".to_string(),
607            "flaws".to_string(),
608            "verdict".to_string(),
609            "critical_fix".to_string(),
610            "confidence".to_string(),
611        ],
612        _ => vec!["confidence".to_string()],
613    };
614
615    OutputSpec { format, fields }
616}
617
618/// Get composable modules for a given module
619fn get_composable_modules(module_key: &str) -> Vec<String> {
620    match module_key {
621        "gigathink" => vec!["laserlogic".to_string(), "brutalhonesty".to_string()],
622        "laserlogic" => vec!["gigathink".to_string(), "bedrock".to_string()],
623        "bedrock" => vec!["laserlogic".to_string(), "proofguard".to_string()],
624        "proofguard" => vec!["bedrock".to_string(), "brutalhonesty".to_string()],
625        "brutalhonesty" => vec!["gigathink".to_string(), "proofguard".to_string()],
626        _ => vec![],
627    }
628}
629
630/// Estimate token usage from cost estimate string
631fn estimate_tokens(cost_estimate: &str) -> u32 {
632    match cost_estimate {
633        "low" => 1000,
634        "medium" => 2000,
635        "medium-high" => 2500,
636        "high" => 3000,
637        _ => 2000,
638    }
639}
640
641/// Estimate latency from duration string
642fn estimate_latency(duration: &str) -> u32 {
643    // Parse duration strings like "30-90s" or "60-180s"
644    if let Some(range) = duration.strip_suffix('s') {
645        if let Some((low, high)) = range.split_once('-') {
646            if let (Ok(low_val), Ok(high_val)) = (low.parse::<u32>(), high.parse::<u32>()) {
647                return ((low_val + high_val) / 2) * 1000; // Convert to ms
648            }
649        }
650    }
651    5000 // Default 5 seconds
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    #[test]
659    fn test_estimate_tokens() {
660        assert_eq!(estimate_tokens("low"), 1000);
661        assert_eq!(estimate_tokens("medium"), 2000);
662        assert_eq!(estimate_tokens("high"), 3000);
663    }
664
665    #[test]
666    fn test_estimate_latency() {
667        assert_eq!(estimate_latency("30-90s"), 60000);
668        assert_eq!(estimate_latency("60-180s"), 120000);
669    }
670
671    #[test]
672    fn test_build_input_spec() {
673        let spec = build_input_spec("gigathink");
674        assert_eq!(spec.required, vec!["query"]);
675        assert!(spec.optional.contains(&"context".to_string()));
676    }
677}