Skip to main content

project_examer/
llm.rs

1use crate::config::{LLMConfig, LLMProvider};
2use anyhow::{anyhow, Result};
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct AnalysisRequest {
9    pub prompt: String,
10    pub context: AnalysisContext,
11    pub analysis_type: AnalysisType,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AnalysisContext {
16    pub files: Vec<FileContext>,
17    pub dependencies: Vec<DependencyContext>,
18    pub project_info: ProjectInfo,
19    pub documentation: Vec<DocumentationContext>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FileContext {
24    pub path: String,
25    pub language: String,
26    pub content_summary: String,
27    pub functions: Vec<String>,
28    pub classes: Vec<String>,
29    pub imports: Vec<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct DocumentationContext {
34    pub path: String,
35    pub file_type: String,
36    pub content: String,
37    pub summary: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct DependencyContext {
42    pub from_file: String,
43    pub to_file: String,
44    pub dependency_type: String,
45    pub strength: f64,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ProjectInfo {
50    pub name: String,
51    pub total_files: usize,
52    pub total_lines: usize,
53    pub languages: Vec<String>,
54    pub architecture_patterns: Vec<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum AnalysisType {
59    Overview,
60    Architecture,
61    Dependencies,
62    Security,
63    Refactoring,
64    Documentation,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AnalysisResponse {
69    pub analysis: String,
70    pub insights: Vec<Insight>,
71    pub recommendations: Vec<Recommendation>,
72    pub confidence: f64,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Insight {
77    pub title: String,
78    pub description: String,
79    pub category: InsightCategory,
80    pub confidence: f64,
81    pub evidence: Vec<String>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub enum InsightCategory {
86    Architecture,
87    CodeQuality,
88    Performance,
89    Security,
90    Maintainability,
91    Testing,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct Recommendation {
96    pub title: String,
97    pub description: String,
98    pub priority: Priority,
99    pub effort: Effort,
100    pub impact: Impact,
101    pub action_items: Vec<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub enum Priority {
106    Low,
107    Medium,
108    High,
109    Critical,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum Effort {
114    Low,
115    Medium,
116    High,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum Impact {
121    Low,
122    Medium,
123    High,
124}
125
126pub struct LLMClient {
127    config: LLMConfig,
128    client: Client,
129    debug: bool,
130}
131
132impl LLMClient {
133    pub fn new(config: LLMConfig, debug: bool) -> Self {
134        let client = Client::builder()
135            .timeout(Duration::from_secs(config.timeout_seconds))
136            .build()
137            .unwrap();
138
139        Self { config, client, debug }
140    }
141
142    pub async fn analyze(&self, request: AnalysisRequest) -> Result<AnalysisResponse> {
143        match self.config.provider {
144            LLMProvider::OpenAI => self.analyze_with_openai(request).await,
145            LLMProvider::Ollama => self.analyze_with_ollama(request).await,
146            LLMProvider::Anthropic => self.analyze_with_anthropic(request).await,
147        }
148    }
149
150    async fn analyze_with_openai(&self, request: AnalysisRequest) -> Result<AnalysisResponse> {
151        let api_key = self.config.api_key.as_ref()
152            .ok_or_else(|| anyhow!("OpenAI API key not provided"))?;
153
154        let system_prompt = self.create_system_prompt(&request.analysis_type);
155        let user_prompt = self.create_user_prompt(&request);
156
157        let payload = serde_json::json!({
158            "model": self.config.model,
159            "messages": [
160                {
161                    "role": "system",
162                    "content": system_prompt
163                },
164                {
165                    "role": "user",
166                    "content": user_prompt
167                }
168            ],
169            "max_completion_tokens": self.config.max_tokens,
170            "temperature": self.config.temperature
171        });
172
173        if self.debug {
174            println!("\nšŸ” LLM Debug - OpenAI Request:");
175            println!("Model: {}", self.config.model);
176            println!("System prompt: {}", system_prompt);
177            println!("User prompt: {}", user_prompt);
178            println!("Payload: {}", serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "Failed to serialize".to_string()));
179        }
180
181        let response = self.client
182            .post("https://api.openai.com/v1/chat/completions")
183            .header("Authorization", format!("Bearer {}", api_key))
184            .header("Content-Type", "application/json")
185            .json(&payload)
186            .send()
187            .await?;
188
189        if !response.status().is_success() {
190            let error_text = response.text().await?;
191            return Err(anyhow!("OpenAI API error: {}", error_text));
192        }
193
194        let response_json: serde_json::Value = response.json().await?;
195        
196        if self.debug {
197            println!("\nšŸ” LLM Debug - OpenAI Response:");
198            println!("Raw response: {}", serde_json::to_string_pretty(&response_json).unwrap_or_else(|_| "Failed to serialize".to_string()));
199        }
200        
201        let content = response_json["choices"][0]["message"]["content"]
202            .as_str()
203            .ok_or_else(|| anyhow!("Invalid response format from OpenAI"))?;
204
205        if self.debug {
206            println!("Content: {}", content);
207        }
208
209        // Try to parse as JSON, but provide fallback for non-JSON responses
210        match serde_json::from_str::<AnalysisResponse>(content) {
211            Ok(analysis_response) => Ok(analysis_response),
212            Err(_) => {
213                // Fallback: create a basic response from plain text
214                Ok(AnalysisResponse {
215                    analysis: content.to_string(),
216                    insights: Vec::new(),
217                    recommendations: Vec::new(),
218                    confidence: 0.5,
219                })
220            }
221        }
222    }
223
224    async fn analyze_with_ollama(&self, request: AnalysisRequest) -> Result<AnalysisResponse> {
225        let default_url = "http://localhost:11434".to_string();
226        let base_url = self.config.base_url.as_ref().unwrap_or(&default_url);
227
228        let system_prompt = self.create_system_prompt(&request.analysis_type);
229        let user_prompt = self.create_user_prompt(&request);
230
231    let payload = serde_json::json!({
232        "model": self.config.model,
233        "prompt": format!("System: {}\n\nUser: {}", system_prompt, user_prompt),
234        "stream": false,
235        "format": "json",
236        "options": {
237            "temperature": self.config.temperature,
238            "num_predict": self.config.max_tokens
239        }
240    });
241
242        if self.debug {
243            println!("\nšŸ” LLM Debug - Ollama Request:");
244            println!("Model: {}", self.config.model);
245            println!("Base URL: {}", base_url);
246            println!("System prompt: {}", system_prompt);
247            println!("User prompt: {}", user_prompt);
248            println!("Payload: {}", serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "Failed to serialize".to_string()));
249        }
250
251        let response = self.client
252            .post(&format!("{}/api/generate", base_url))
253            .header("Content-Type", "application/json")
254            .json(&payload)
255            .send()
256            .await?;
257
258        if !response.status().is_success() {
259            let error_text = response.text().await?;
260            return Err(anyhow!("Ollama API error: {}", error_text));
261        }
262
263        let response_json: serde_json::Value = response.json().await?;
264        
265        if self.debug {
266            println!("\nšŸ” LLM Debug - Ollama Response:");
267            println!("Raw response: {}", serde_json::to_string_pretty(&response_json).unwrap_or_else(|_| "Failed to serialize".to_string()));
268        }
269        
270        let content = response_json["response"]
271            .as_str()
272            .ok_or_else(|| anyhow!("Invalid response format from Ollama"))?;
273
274        if self.debug {
275            println!("Content: {}", content);
276        }
277
278        // Try to parse as JSON, but provide fallback for non-JSON responses
279        match serde_json::from_str::<AnalysisResponse>(content) {
280            Ok(analysis_response) => Ok(analysis_response),
281            Err(_) => {
282                // Fallback: create a basic response from plain text
283                Ok(AnalysisResponse {
284                    analysis: content.to_string(),
285                    insights: Vec::new(),
286                    recommendations: Vec::new(),
287                    confidence: 0.5,
288                })
289            }
290        }
291    }
292
293    async fn analyze_with_anthropic(&self, request: AnalysisRequest) -> Result<AnalysisResponse> {
294        let api_key = self.config.api_key.as_ref()
295            .ok_or_else(|| anyhow!("Anthropic API key not provided"))?;
296
297        let system_prompt = self.create_system_prompt(&request.analysis_type);
298        let user_prompt = self.create_user_prompt(&request);
299
300        let payload = serde_json::json!({
301            "model": self.config.model,
302            "max_tokens": self.config.max_tokens,
303            "system": system_prompt,
304            "messages": [
305                {
306                    "role": "user",
307                    "content": user_prompt
308                }
309            ]
310        });
311
312        if self.debug {
313            println!("\nšŸ” LLM Debug - Anthropic Request:");
314            println!("Model: {}", self.config.model);
315            println!("System prompt: {}", system_prompt);
316            println!("User prompt: {}", user_prompt);
317            println!("Payload: {}", serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "Failed to serialize".to_string()));
318        }
319
320        let response = self.client
321            .post("https://api.anthropic.com/v1/messages")
322            .header("x-api-key", api_key)
323            .header("Content-Type", "application/json")
324            .header("anthropic-version", "2023-06-01")
325            .json(&payload)
326            .send()
327            .await?;
328
329        if !response.status().is_success() {
330            let error_text = response.text().await?;
331            return Err(anyhow!("Anthropic API error: {}", error_text));
332        }
333
334        let response_json: serde_json::Value = response.json().await?;
335        
336        if self.debug {
337            println!("\nšŸ” LLM Debug - Anthropic Response:");
338            println!("Raw response: {}", serde_json::to_string_pretty(&response_json).unwrap_or_else(|_| "Failed to serialize".to_string()));
339        }
340        
341        let content = response_json["content"][0]["text"]
342            .as_str()
343            .ok_or_else(|| anyhow!("Invalid response format from Anthropic"))?;
344
345        if self.debug {
346            println!("Content: {}", content);
347        }
348
349        // Try to parse as JSON, but provide fallback for non-JSON responses
350        match serde_json::from_str::<AnalysisResponse>(content) {
351            Ok(analysis_response) => Ok(analysis_response),
352            Err(_) => {
353                // Fallback: create a basic response from plain text
354                Ok(AnalysisResponse {
355                    analysis: content.to_string(),
356                    insights: Vec::new(),
357                    recommendations: Vec::new(),
358                    confidence: 0.5,
359                })
360            }
361        }
362    }
363
364    fn create_system_prompt(&self, analysis_type: &AnalysisType) -> String {
365        match analysis_type {
366            AnalysisType::Overview => {
367                "You are a senior software architect analyzing a codebase. Provide a comprehensive overview of the software architecture, including key components, patterns used, and overall design philosophy. 
368
369If possible, return your response as JSON with this structure: {\"analysis\": \"detailed overview\", \"insights\": [{\"title\": \"...\", \"description\": \"...\", \"category\": \"Architecture\", \"confidence\": 0.8, \"evidence\": [\"...\"]}], \"recommendations\": [{\"title\": \"...\", \"description\": \"...\", \"priority\": \"High\", \"effort\": \"Medium\", \"impact\": \"High\", \"action_items\": [\"...\"]}], \"confidence\": 0.8}
370
371If JSON formatting is not working, provide a well-structured text response with clear sections for analysis, insights, and recommendations.".to_string()
372            }
373            AnalysisType::Architecture => {
374                "You are a software architect expert. Analyze the architectural patterns, design principles, and structural organization of this codebase. Identify patterns like MVC, microservices, layered architecture, etc. 
375
376Provide your analysis in a clear, structured format covering:
377- Architecture style and patterns
378- Key design principles
379- Structural organization
380- Strengths and weaknesses
381- Recommendations for improvement".to_string()
382            }
383            AnalysisType::Dependencies => {
384                "You are a dependency analysis expert. Examine the dependency relationships, identify potential issues like circular dependencies, tight coupling, or unused dependencies.
385
386Provide analysis covering:
387- Dependency structure overview
388- Potential issues (circular deps, tight coupling)
389- Unused or redundant dependencies
390- Recommendations for improvement
391- Modularity assessment".to_string()
392            }
393            AnalysisType::Security => {
394                "You are a security expert analyzing code for potential vulnerabilities. Look for common security issues, insecure patterns, and provide recommendations for improvement.
395
396Cover these areas:
397- Security vulnerabilities identified
398- Insecure coding patterns
399- Data handling and validation issues
400- Authentication and authorization concerns
401- Recommendations and best practices".to_string()
402            }
403            AnalysisType::Refactoring => {
404                "You are a code quality expert. Identify opportunities for refactoring, code smells, and suggest improvements for maintainability and readability.
405
406Analyze:
407- Code smells and anti-patterns
408- Duplication and redundancy
409- Complex or unclear code sections
410- Maintainability issues
411- Specific refactoring recommendations".to_string()
412            }
413            AnalysisType::Documentation => {
414                "You are a technical documentation expert. Generate comprehensive documentation based on the code structure and patterns. Create explanations for how the software works.
415
416Provide:
417- High-level system overview
418- Key components and their purposes
419- Data flow and interactions
420- Usage examples
421- Setup and configuration guidance".to_string()
422            }
423        }
424    }
425
426    fn create_user_prompt(&self, request: &AnalysisRequest) -> String {
427        let mut prompt = format!("Analyze this codebase:\n\n{}\n\n", request.prompt);
428
429        prompt.push_str("Project Information:\n");
430        prompt.push_str(&format!("- Name: {}\n", request.context.project_info.name));
431        prompt.push_str(&format!("- Total files: {}\n", request.context.project_info.total_files));
432        prompt.push_str(&format!("- Languages: {}\n", request.context.project_info.languages.join(", ")));
433
434        if !request.context.files.is_empty() {
435            prompt.push_str("\nFile Structure:\n");
436            for file in &request.context.files {
437                prompt.push_str(&format!("- {} ({})\n", file.path, file.language));
438                prompt.push_str(&format!("  Functions: {}\n", file.functions.join(", ")));
439                if !file.classes.is_empty() {
440                    prompt.push_str(&format!("  Classes: {}\n", file.classes.join(", ")));
441                }
442                if !file.imports.is_empty() {
443                    prompt.push_str(&format!("  Imports: {}\n", file.imports.join(", ")));
444                }
445            }
446        }
447
448        if !request.context.dependencies.is_empty() {
449            prompt.push_str("\nDependency Relationships:\n");
450            for dep in &request.context.dependencies {
451                prompt.push_str(&format!("- {} -> {} ({}, strength: {:.2})\n", 
452                    dep.from_file, dep.to_file, dep.dependency_type, dep.strength));
453            }
454        }
455
456        prompt.push_str("\nPlease provide a detailed analysis with specific insights and actionable recommendations.");
457        prompt
458    }
459
460    pub async fn batch_analyze(&self, requests: Vec<AnalysisRequest>) -> Result<Vec<AnalysisResponse>> {
461        let mut responses = Vec::new();
462        
463        for request in requests {
464            let response = self.analyze(request).await?;
465            responses.push(response);
466            
467            tokio::time::sleep(Duration::from_millis(100)).await;
468        }
469        
470        Ok(responses)
471    }
472}