1#[derive(Debug, Clone)]
2pub struct AiContext {
3 pub job_name: String,
4 pub source_name: String,
5 pub stage: String,
6 pub job_yaml: String,
7 pub runner_summary: String,
8 pub pipeline_summary: String,
9 pub runtime_summary: Option<String>,
10 pub log_excerpt: String,
11 pub failure_hint: Option<String>,
12}
13
14impl AiContext {
15 pub fn to_prompt(&self, system: Option<&str>) -> String {
16 let mut prompt = String::new();
17 if let Some(system) = system.filter(|value| !value.trim().is_empty()) {
18 prompt.push_str(system.trim());
19 prompt.push_str("\n\n");
20 }
21 prompt.push_str("You are helping troubleshoot a local GitLab-style CI job run in Opal. ");
22 prompt.push_str("Be concise, name the most likely root cause first, and then list concrete next debugging or fix steps. ");
23 prompt.push_str("If the evidence is inconclusive, say what extra signal is missing.\n\n");
24 prompt.push_str(&format!("Job: {}\n", self.job_name));
25 prompt.push_str(&format!("Source job: {}\n", self.source_name));
26 prompt.push_str(&format!("Stage: {}\n", self.stage));
27 prompt.push_str(&format!("Runner: {}\n\n", self.runner_summary));
28
29 if let Some(hint) = &self.failure_hint {
30 prompt.push_str("Current failure summary:\n");
31 prompt.push_str(hint.trim());
32 prompt.push_str("\n\n");
33 }
34
35 prompt.push_str("Selected job YAML:\n```yaml\n");
36 prompt.push_str(self.job_yaml.trim());
37 prompt.push_str("\n```\n\n");
38
39 prompt.push_str("Pipeline plan summary:\n```text\n");
40 prompt.push_str(self.pipeline_summary.trim());
41 prompt.push_str("\n```\n\n");
42
43 if let Some(runtime) = &self.runtime_summary {
44 prompt.push_str("Runtime summary:\n```text\n");
45 prompt.push_str(runtime.trim());
46 prompt.push_str("\n```\n\n");
47 }
48
49 prompt.push_str("Recent job log excerpt:\n```text\n");
50 prompt.push_str(self.log_excerpt.trim());
51 prompt.push_str("\n```\n\n");
52
53 prompt.push_str("Respond with:\n");
54 prompt.push_str("1. Root cause\n");
55 prompt.push_str("2. Why you think that\n");
56 prompt.push_str("3. Concrete next steps\n");
57 prompt
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::AiContext;
64
65 #[test]
66 fn prompt_contains_core_troubleshooting_sections() {
67 let context = AiContext {
68 job_name: "unit-tests".to_string(),
69 source_name: "unit-tests".to_string(),
70 stage: "test".to_string(),
71 job_yaml: "unit-tests:\n script:\n - cargo test".to_string(),
72 runner_summary: "engine=container arch=arm64 vcpu=6 ram=3g".to_string(),
73 pipeline_summary: "dependencies: fetch-sources, rust-checks".to_string(),
74 runtime_summary: Some("container: opal-unit-tests-01".to_string()),
75 log_excerpt: "error: linker failed".to_string(),
76 failure_hint: Some("container command exited with status Some(101)".to_string()),
77 };
78
79 let prompt = context.to_prompt(Some("system"));
80 assert!(prompt.contains("system"));
81 assert!(prompt.contains("Selected job YAML"));
82 assert!(prompt.contains("Recent job log excerpt"));
83 assert!(prompt.contains("Root cause"));
84 assert!(prompt.contains("container command exited with status Some(101)"));
85 }
86}