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 match serde_json::from_str::<AnalysisResponse>(content) {
211 Ok(analysis_response) => Ok(analysis_response),
212 Err(_) => {
213 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 match serde_json::from_str::<AnalysisResponse>(content) {
280 Ok(analysis_response) => Ok(analysis_response),
281 Err(_) => {
282 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 match serde_json::from_str::<AnalysisResponse>(content) {
351 Ok(analysis_response) => Ok(analysis_response),
352 Err(_) => {
353 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}