Skip to main content

ferro_cli/commands/
ai_explain.rs

1//! `ferro ai:explain <target>` — AI-powered service/route/model explanation.
2//!
3//! Resolves the target in SERVICE → ROUTE → MODEL order (projection-framed is
4//! primary; prose fallback when no projection exists). Assembles a prompt from
5//! introspected facts and produces prose via a raw `CompletionRequest { schema:
6//! None, .. }` call. `--dry-run` prints the assembled prompt without calling
7//! the LLM.
8//!
9//! All resolution and prompt-building logic lives in
10//! `ferro_mcp::tools::ai_explain_core`. This module is a thin CLI wrapper:
11//! tokio runtime bridge, console output, process exit.
12
13// ---------------------------------------------------------------------------
14// Command entry point
15// ---------------------------------------------------------------------------
16
17/// Run the `ferro ai:explain <target>` command.
18///
19/// Thin wrapper over the relocated `ferro_mcp::tools::ai_explain_core` public
20/// items. CLI behavior is unchanged: prose-only output, `--dry-run` prints the
21/// assembled prompt, no LLM call in dry-run mode.
22#[cfg(feature = "projections")]
23pub fn run(target: String, type_override: Option<String>, dry_run: bool) {
24    use console::style;
25    use ferro_ai::client::{Message, Role};
26    use ferro_ai::{AiConfig, CompletionRequest};
27    use ferro_mcp::tools::ai_explain_core::{
28        build_model_prompt, build_route_prompt, build_service_prompt,
29        resolve_max_tokens_with_default, resolve_target, ResolvedTarget,
30    };
31
32    // 1. Fail-fast: require AI provider unless --dry-run (D-06).
33    //    In dry-run mode, AI config is NOT checked — the assembled prompt is
34    //    printed and the function returns without calling the LLM or requiring
35    //    any env vars to be set.
36    let client_result = AiConfig::from_env();
37    if !dry_run {
38        if let Err(ref e) = client_result {
39            eprintln!(
40                "{} AI provider not configured: {e}\n  Set FERRO_AI_PROVIDER, FERRO_AI_API_KEY, and FERRO_AI_MODEL.",
41                style("Error:").red().bold()
42            );
43            std::process::exit(1);
44        }
45    }
46
47    // 2. Tokio runtime bridge
48    let rt = match tokio::runtime::Runtime::new() {
49        Ok(r) => r,
50        Err(e) => {
51            eprintln!(
52                "{} Failed to create tokio runtime: {e}",
53                style("Error:").red().bold()
54            );
55            std::process::exit(1);
56        }
57    };
58
59    // 3. Resolve target (service → route → model) via the relocated async core.
60    let resolved = rt.block_on(resolve_target(
61        std::path::Path::new("."),
62        &target,
63        type_override.as_deref(),
64    ));
65
66    match resolved {
67        ResolvedTarget::NotFound(msg) => {
68            eprintln!("{} {msg}", style("Error:").red().bold());
69            std::process::exit(1);
70        }
71        resolved => {
72            // 4. Build prompt using the relocated pub builders.
73            let (system_prompt, user_prompt) = match &resolved {
74                ResolvedTarget::Service(d) => build_service_prompt(d),
75                ResolvedTarget::Route(r) => build_route_prompt(r),
76                ResolvedTarget::Model(m) => build_model_prompt(m),
77                ResolvedTarget::NotFound(_) => unreachable!(),
78            };
79
80            // 5. --dry-run: print assembled prompt and return (no LLM call)
81            if dry_run {
82                println!("{system_prompt}");
83                println!("---");
84                println!("{user_prompt}");
85                return;
86            }
87
88            // 6. Cost guard (default 2048 for ai:explain)
89            let max_tokens = resolve_max_tokens_with_default(2048);
90
91            // 7. Raw prose completion — schema: None (unstructured, no JSON coercion)
92            let client = client_result.expect("already validated above");
93
94            let req = CompletionRequest {
95                system: Some(system_prompt),
96                messages: vec![Message {
97                    role: Role::User,
98                    content: user_prompt,
99                    tool_call_id: None,
100                }],
101                max_tokens,
102                model_override: None,
103                schema: None,
104                tools: None,
105                tool_choice: None,
106            };
107
108            match rt.block_on(client.complete(req)) {
109                Ok(prose) => {
110                    println!("{prose}");
111                }
112                Err(e) => {
113                    eprintln!(
114                        "{} LLM completion failed: {e}",
115                        style("Error:").red().bold()
116                    );
117                    std::process::exit(1);
118                }
119            }
120        }
121    }
122}