Skip to main content

opal/ai/
context.rs

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}