aivim 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aivim/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ AIVim - An AI-enhanced version of Vim
3
+ """
4
+ from .editor import Editor
5
+ from .buffer import Buffer
6
+ from .display import Display
7
+ from .command_handler import CommandHandler
8
+ from .ai_service import AIService
9
+ from .history import History
10
+
11
+ __version__ = "0.1.0"
aivim/ai_service.py ADDED
@@ -0,0 +1,457 @@
1
+ """
2
+ AI services for AIVim using multiple AI providers including OpenAI, Anthropic, and local LLMs
3
+ """
4
+ import logging
5
+ import os
6
+ import time
7
+ from typing import Optional, Dict, Any
8
+
9
+ try:
10
+ from openai import OpenAI
11
+ OPENAI_AVAILABLE = True
12
+ except ImportError:
13
+ OPENAI_AVAILABLE = False
14
+
15
+ try:
16
+ from llama_cpp import Llama
17
+ LLAMA_AVAILABLE = True
18
+ except ImportError:
19
+ LLAMA_AVAILABLE = False
20
+
21
+
22
+ class AIService:
23
+ """
24
+ Service for interacting with AI models
25
+ """
26
+ def __init__(self):
27
+ """Initialize AI service"""
28
+ # OpenAI setup
29
+ self.openai_api_key = os.environ.get("OPENAI_API_KEY")
30
+ self.openai_client = None
31
+
32
+ # Anthropic setup
33
+ self.anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
34
+ self.anthropic_client = None
35
+
36
+ # Local LLM setup
37
+ self.llama_model_path = os.environ.get("LLAMA_MODEL_PATH")
38
+ self.local_llm = None
39
+
40
+ # Default model provider
41
+ self.current_model = "openai" # Options: "openai", "claude", "local"
42
+
43
+ # Initialize available clients
44
+ self._initialize_clients()
45
+
46
+ def _initialize_clients(self):
47
+ """Initialize available AI clients based on API keys"""
48
+ # Initialize OpenAI
49
+ if OPENAI_AVAILABLE:
50
+ if self.openai_api_key:
51
+ try:
52
+ self.openai_client = OpenAI(api_key=self.openai_api_key)
53
+ logging.info("OpenAI client initialized successfully")
54
+ except Exception as e:
55
+ logging.error(f"Error initializing OpenAI client: {str(e)}")
56
+ else:
57
+ logging.warning("OPENAI_API_KEY environment variable not set. OpenAI features will not work.")
58
+ else:
59
+ logging.warning("OpenAI package not installed. OpenAI features will not work.")
60
+
61
+ # Initialize Anthropic
62
+ try:
63
+ import anthropic
64
+ if self.anthropic_api_key:
65
+ try:
66
+ self.anthropic_client = anthropic.Anthropic(api_key=self.anthropic_api_key)
67
+ logging.info("Anthropic client initialized successfully")
68
+ except Exception as e:
69
+ logging.error(f"Error initializing Anthropic client: {str(e)}")
70
+ else:
71
+ logging.warning("ANTHROPIC_API_KEY environment variable not set. Claude features will not work.")
72
+ except ImportError:
73
+ logging.warning("Anthropic package not installed. Claude features will not work.")
74
+
75
+ # Initialize Local LLM (llama.cpp)
76
+ if LLAMA_AVAILABLE:
77
+ # Try to load a default model if path is not provided
78
+ model_path = self.llama_model_path
79
+ if not model_path:
80
+ # First check for models in common locations
81
+ possible_paths = [
82
+ os.path.expanduser("~/.local/share/llama.cpp/models"),
83
+ os.path.expanduser("~/models"),
84
+ "./models"
85
+ ]
86
+
87
+ # Names of popular open models to check for
88
+ model_names = [
89
+ "llama-2-7b-chat.gguf",
90
+ "ggml-model-q4_0.bin",
91
+ "mistral-7b-instruct-v0.1.Q4_0.gguf",
92
+ "llama-2-13b-chat.gguf"
93
+ ]
94
+
95
+ # Search for models
96
+ for path in possible_paths:
97
+ if os.path.exists(path):
98
+ for model in model_names:
99
+ model_path = os.path.join(path, model)
100
+ if os.path.exists(model_path):
101
+ logging.info(f"Found local model: {model_path}")
102
+ break
103
+ if model_path:
104
+ break
105
+
106
+ if model_path and os.path.exists(model_path):
107
+ try:
108
+ # Initialize with minimal settings
109
+ self.local_llm = Llama(
110
+ model_path=model_path,
111
+ n_ctx=2048, # Context window size
112
+ n_threads=4 # Number of CPU threads to use
113
+ )
114
+ logging.info(f"Local LLM initialized with model: {model_path}")
115
+ except Exception as e:
116
+ logging.error(f"Error initializing local LLM: {str(e)}")
117
+ else:
118
+ logging.warning("No local model found. Set LLAMA_MODEL_PATH environment variable to use local LLM.")
119
+ else:
120
+ logging.warning("llama-cpp-python package not installed. Local LLM features will not work.")
121
+
122
+ def set_model(self, model_name: str) -> bool:
123
+ """
124
+ Set the AI model provider to use
125
+
126
+ Args:
127
+ model_name: Model provider name ("openai", "claude", "local")
128
+
129
+ Returns:
130
+ True if successful, False otherwise
131
+ """
132
+ model_name = model_name.lower()
133
+
134
+ if model_name == "openai" and not self.openai_client:
135
+ logging.error("OpenAI client not available. Check API key and package installation.")
136
+ return False
137
+ elif model_name == "claude" and not self.anthropic_client:
138
+ logging.error("Claude client not available. Check API key and package installation.")
139
+ return False
140
+ elif model_name == "local" and not self.local_llm:
141
+ if not LLAMA_AVAILABLE:
142
+ logging.error("Local LLM not available. Please install llama-cpp-python package.")
143
+ else:
144
+ logging.error("Local LLM not initialized. Set LLAMA_MODEL_PATH environment variable.")
145
+ return False
146
+ elif model_name not in ["openai", "claude", "local"]:
147
+ logging.error(f"Unknown model: {model_name}")
148
+ return False
149
+
150
+ self.current_model = model_name
151
+ logging.info(f"AI model set to: {model_name}")
152
+ return True
153
+
154
+ def _create_completion(self, system_prompt: str, user_prompt: str) -> Optional[str]:
155
+ """
156
+ Create an AI completion using the selected model provider
157
+
158
+ Args:
159
+ system_prompt: System instructions
160
+ user_prompt: User query
161
+
162
+ Returns:
163
+ Generated text or None if the request failed
164
+ """
165
+ # Check which model is currently selected
166
+ if self.current_model == "openai":
167
+ return self._openai_completion(system_prompt, user_prompt)
168
+ elif self.current_model == "claude":
169
+ return self._anthropic_completion(system_prompt, user_prompt)
170
+ elif self.current_model == "local":
171
+ return self._local_completion(system_prompt, user_prompt)
172
+ else:
173
+ return f"Unknown model type: {self.current_model}"
174
+
175
+ def _openai_completion(self, system_prompt: str, user_prompt: str) -> Optional[str]:
176
+ """Create a completion using OpenAI"""
177
+ if not OPENAI_AVAILABLE:
178
+ return "OpenAI package not installed. Please install it with 'pip install openai'."
179
+
180
+ if not self.openai_client:
181
+ return "OpenAI API unavailable. Please set OPENAI_API_KEY environment variable."
182
+
183
+ try:
184
+ # Use the gpt-4o model released after the knowledge cutoff
185
+ # the newest OpenAI model is "gpt-4o" which was released May 13, 2024.
186
+ # do not change this unless explicitly requested by the user
187
+ response = self.openai_client.chat.completions.create(
188
+ model="gpt-4o",
189
+ messages=[
190
+ {"role": "system", "content": system_prompt},
191
+ {"role": "user", "content": user_prompt}
192
+ ],
193
+ temperature=0.2,
194
+ max_tokens=1000
195
+ )
196
+ return response.choices[0].message.content
197
+ except Exception as e:
198
+ logging.error(f"OpenAI API error: {str(e)}")
199
+ return f"Error: {str(e)}"
200
+
201
+ def _anthropic_completion(self, system_prompt: str, user_prompt: str) -> Optional[str]:
202
+ """Create a completion using Anthropic Claude"""
203
+ if not self.anthropic_client:
204
+ return "Anthropic Claude API unavailable. Please set ANTHROPIC_API_KEY environment variable."
205
+
206
+ try:
207
+ # Use the claude-3-5-sonnet-20241022 model, the latest available
208
+ response = self.anthropic_client.messages.create(
209
+ model="claude-3-5-sonnet-20241022",
210
+ system=system_prompt,
211
+ messages=[
212
+ {"role": "user", "content": user_prompt}
213
+ ],
214
+ temperature=0.2,
215
+ max_tokens=1000
216
+ )
217
+ return response.content[0].text
218
+ except Exception as e:
219
+ logging.error(f"Anthropic API error: {str(e)}")
220
+ return f"Error: {str(e)}"
221
+
222
+ def _local_completion(self, system_prompt: str, user_prompt: str) -> Optional[str]:
223
+ """Create a completion using a local model with llama.cpp"""
224
+ if not LLAMA_AVAILABLE:
225
+ return "llama-cpp-python package not installed. Please install it with 'pip install llama-cpp-python'."
226
+
227
+ if not self.local_llm:
228
+ return ("Local LLM not initialized. Please set LLAMA_MODEL_PATH environment variable "
229
+ "or place a supported model in ./models directory.")
230
+
231
+ try:
232
+ # Format the prompt in a chat-like format that local models can understand
233
+ formatted_prompt = f"""
234
+ <|system|>
235
+ {system_prompt}
236
+ <|user|>
237
+ {user_prompt}
238
+ <|assistant|>
239
+ """
240
+ # Generate completion with the local model
241
+ start_time = time.time()
242
+ logging.info("Starting local LLM inference...")
243
+
244
+ # Use the llama.cpp API to generate text
245
+ output = self.local_llm(
246
+ formatted_prompt,
247
+ max_tokens=1000,
248
+ stop=["<|user|>", "<|system|>"], # Stop tokens
249
+ echo=False, # Don't echo the prompt
250
+ temperature=0.2, # Lower temp for more deterministic outputs
251
+ top_p=0.95, # Nucleus sampling for more focused outputs
252
+ repeat_penalty=1.1 # Slight penalty for repetition
253
+ )
254
+
255
+ # Extract the generated text from the model output
256
+ response = output["choices"][0]["text"].strip()
257
+
258
+ elapsed_time = time.time() - start_time
259
+ logging.info(f"Local LLM inference completed in {elapsed_time:.2f} seconds.")
260
+
261
+ return response
262
+ except Exception as e:
263
+ logging.error(f"Local LLM error: {str(e)}")
264
+ return f"Error using local LLM: {str(e)}"
265
+
266
+ def get_explanation(self, code: str, context: str) -> str:
267
+ """
268
+ Get an explanation of the provided code
269
+
270
+ Args:
271
+ code: The specific code to explain
272
+ context: The surrounding code for context
273
+
274
+ Returns:
275
+ A detailed explanation of the code
276
+ """
277
+ system_prompt = (
278
+ "You are an expert code analyst. "
279
+ "Provide a detailed explanation of the provided code, "
280
+ "including its purpose, how it works, and any potential issues. "
281
+ "Focus on clarity and depth of explanation."
282
+ )
283
+
284
+ user_prompt = f"""
285
+ # Code to explain:
286
+ ```
287
+ {code}
288
+ ```
289
+
290
+ # Context (surrounding code):
291
+ ```
292
+ {context}
293
+ ```
294
+
295
+ Please explain this code in detail.
296
+ """
297
+
298
+ explanation = self._create_completion(system_prompt, user_prompt)
299
+ return explanation or "Failed to generate explanation."
300
+
301
+ def get_improvement(self, code: str, context: str) -> str:
302
+ """
303
+ Get an improved version of the provided code with structured output
304
+
305
+ Args:
306
+ code: The specific code to improve
307
+ context: The surrounding code for context
308
+
309
+ Returns:
310
+ A structured string with EXPLANATION and IMPROVED_CODE sections
311
+ """
312
+ system_prompt = (
313
+ "You are an expert code improver. "
314
+ "Analyze the provided code and suggest improvements. "
315
+ "Maintain the original functionality while making enhancements for: "
316
+ "performance, readability, maintainability, or error handling. "
317
+ "YOUR RESPONSE MUST USE THIS EXACT FORMAT with these section headers:\n\n"
318
+ "# EXPLANATION\n<Your detailed explanation of all improvements>\n\n"
319
+ "# IMPROVED_CODE\n<The complete improved code without any markdown formatting>\n\n"
320
+ )
321
+
322
+ user_prompt = f"""
323
+ # Code to improve:
324
+ ```
325
+ {code}
326
+ ```
327
+
328
+ # Context (surrounding code):
329
+ ```
330
+ {context}
331
+ ```
332
+
333
+ Please provide your response using the EXACT format with these section headers:
334
+ 1. Start with "# EXPLANATION" followed by your detailed explanation
335
+ 2. Then include "# IMPROVED_CODE" followed by just the improved code (no markdown code blocks)
336
+ """
337
+
338
+ improvement = self._create_completion(system_prompt, user_prompt)
339
+ if not improvement:
340
+ return "Failed to generate improvement."
341
+
342
+ # Ensure we have the two required sections
343
+ if "# EXPLANATION" not in improvement or "# IMPROVED_CODE" not in improvement:
344
+ # Try to parse it anyway by adding the headers
345
+ processed = "# EXPLANATION\n" + improvement + "\n\n# IMPROVED_CODE\n" + code
346
+ return processed
347
+
348
+ return improvement
349
+
350
+ def generate_code(self, specification: str, context: str) -> str:
351
+ """
352
+ Generate code based on a specification
353
+
354
+ Args:
355
+ specification: The code or comments describing what to generate
356
+ context: The surrounding code for context
357
+
358
+ Returns:
359
+ Generated code based on the specification
360
+ """
361
+ system_prompt = (
362
+ "You are an expert code generator. "
363
+ "Generate high-quality, efficient code based on the specification. "
364
+ "Ensure the generated code fits well with the provided context. "
365
+ "Focus on correctness, efficiency, and readability. "
366
+ "Include helpful comments where appropriate."
367
+ )
368
+
369
+ user_prompt = f"""
370
+ # Specification:
371
+ {specification}
372
+
373
+ # Context (surrounding code):
374
+ ```
375
+ {context}
376
+ ```
377
+
378
+ Please generate code that meets this specification and fits well with the context.
379
+ """
380
+
381
+ generated_code = self._create_completion(system_prompt, user_prompt)
382
+ return generated_code or "Failed to generate code."
383
+
384
+ def custom_query(self, query: str, context: str) -> str:
385
+ """
386
+ Process a custom query about the code
387
+
388
+ Args:
389
+ query: The user's query
390
+ context: The code context for reference
391
+
392
+ Returns:
393
+ The AI's response to the query
394
+ """
395
+ system_prompt = (
396
+ "You are an expert programming assistant. "
397
+ "Answer the user's query about their code accurately and helpfully. "
398
+ "If the query is unclear, ask for clarification. "
399
+ "Provide factual, specific information without making assumptions. "
400
+ "When relevant, include code examples."
401
+ )
402
+
403
+ user_prompt = f"""
404
+ # Query:
405
+ {query}
406
+
407
+ # Code context:
408
+ ```
409
+ {context}
410
+ ```
411
+
412
+ Please respond to this query considering the code context.
413
+ """
414
+
415
+ response = self._create_completion(system_prompt, user_prompt)
416
+ return response or "Failed to process query."
417
+
418
+ def analyze_code(self, code: str, context: str) -> str:
419
+ """
420
+ Analyze code complexity and identify potential bugs
421
+
422
+ Args:
423
+ code: The specific code to analyze
424
+ context: The surrounding code for context
425
+
426
+ Returns:
427
+ A detailed analysis of code complexity and potential bugs
428
+ """
429
+ system_prompt = (
430
+ "You are an expert code analyzer specializing in identifying complexity issues and potential bugs. "
431
+ "Analyze the provided code thoroughly and provide detailed feedback on: "
432
+ "1. Cyclomatic complexity - identify complex functions or methods and suggest simplification "
433
+ "2. Potential bugs - edge cases, error handling gaps, race conditions, etc. "
434
+ "3. Code smells - duplicate code, long methods, long parameter lists "
435
+ "4. Performance issues - inefficient algorithms, memory usage concerns "
436
+ "5. Security vulnerabilities - if any are evident "
437
+ "6. Maintainability concerns "
438
+ "Format your response with clear sections for each category and provide line references. "
439
+ "For each issue, explain why it's problematic and suggest a practical solution."
440
+ )
441
+
442
+ user_prompt = f"""
443
+ # Code to analyze:
444
+ ```
445
+ {code}
446
+ ```
447
+
448
+ # Context (surrounding code):
449
+ ```
450
+ {context}
451
+ ```
452
+
453
+ Please provide a comprehensive analysis of this code, focusing on complexity and potential bugs.
454
+ """
455
+
456
+ analysis = self._create_completion(system_prompt, user_prompt)
457
+ return analysis or "Failed to analyze code."
aivim/buffer.py ADDED
@@ -0,0 +1,120 @@
1
+ """
2
+ Buffer implementation for AIVim
3
+ """
4
+ from typing import List, Optional, Tuple
5
+
6
+
7
+ class Buffer:
8
+ """
9
+ Represents an editing buffer that holds text content
10
+ """
11
+ def __init__(self):
12
+ """Initialize an empty buffer"""
13
+ self.lines = [""]
14
+ self.modified = False
15
+
16
+ # Selection state
17
+ self.selection_start = None # (y, x)
18
+ self.selection_end = None # (y, x)
19
+
20
+ def get_lines(self) -> List[str]:
21
+ """Get all lines in the buffer"""
22
+ return self.lines
23
+
24
+ def get_line(self, index: int) -> str:
25
+ """Get a specific line by index"""
26
+ if 0 <= index < len(self.lines):
27
+ return self.lines[index]
28
+ return ""
29
+
30
+ def set_line(self, index: int, content: str) -> None:
31
+ """Set the content of a specific line"""
32
+ if 0 <= index < len(self.lines):
33
+ if self.lines[index] != content:
34
+ self.lines[index] = content
35
+ self.modified = True
36
+
37
+ def insert_line(self, index: int, content: str) -> None:
38
+ """Insert a new line at the specified index"""
39
+ if 0 <= index <= len(self.lines):
40
+ self.lines.insert(index, content)
41
+ self.modified = True
42
+
43
+ def delete_line(self, index: int) -> None:
44
+ """Delete the line at the specified index"""
45
+ if 0 <= index < len(self.lines):
46
+ del self.lines[index]
47
+ # Ensure buffer always has at least one line
48
+ if not self.lines:
49
+ self.lines = [""]
50
+ self.modified = True
51
+
52
+ def get_content(self) -> str:
53
+ """Get the entire buffer content as a string"""
54
+ return "\n".join(self.lines)
55
+
56
+ def set_content(self, content: str) -> None:
57
+ """Set the entire buffer content"""
58
+ if content:
59
+ self.lines = content.split("\n")
60
+ else:
61
+ self.lines = [""]
62
+ self.modified = True
63
+
64
+ def set_lines(self, lines: List[str]) -> None:
65
+ """Set all lines in the buffer"""
66
+ if lines:
67
+ self.lines = lines.copy()
68
+ else:
69
+ self.lines = [""]
70
+ self.modified = True
71
+
72
+ def is_modified(self) -> bool:
73
+ """Check if the buffer has been modified"""
74
+ return self.modified
75
+
76
+ def mark_as_saved(self) -> None:
77
+ """Mark the buffer as saved (not modified)"""
78
+ self.modified = False
79
+
80
+ def start_selection(self, y: int, x: int) -> None:
81
+ """Start a selection at the specified position"""
82
+ self.selection_start = (y, x)
83
+ self.selection_end = (y, x)
84
+
85
+ def update_selection(self, y: int, x: int) -> None:
86
+ """Update the end point of the current selection"""
87
+ self.selection_end = (y, x)
88
+
89
+ def end_selection(self) -> None:
90
+ """End the current selection"""
91
+ self.selection_start = None
92
+ self.selection_end = None
93
+
94
+ def get_selection(self) -> Tuple[Optional[Tuple[int, int]], Optional[Tuple[int, int]]]:
95
+ """Get the current selection start and end points"""
96
+ return (self.selection_start, self.selection_end)
97
+
98
+ def get_selection_text(self) -> str:
99
+ """Get the text in the current selection"""
100
+ if not self.selection_start or not self.selection_end:
101
+ return ""
102
+
103
+ start_y, start_x = self.selection_start
104
+ end_y, end_x = self.selection_end
105
+
106
+ # Ensure start is before end
107
+ if (start_y > end_y) or (start_y == end_y and start_x > end_x):
108
+ start_y, start_x, end_y, end_x = end_y, end_x, start_y, start_x
109
+
110
+ if start_y == end_y:
111
+ # Single line selection
112
+ return self.lines[start_y][start_x:end_x]
113
+
114
+ # Multi-line selection
115
+ result = [self.lines[start_y][start_x:]]
116
+ for i in range(start_y + 1, end_y):
117
+ result.append(self.lines[i])
118
+ result.append(self.lines[end_y][:end_x])
119
+
120
+ return "\n".join(result)