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