diff --git a/README.md b/README.md index d063a20..3392c5a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ and performing detailed code reviews. What makes codai stand out is its deep understanding of the entire context of your project, enabling it to analyze your code base and suggest improvements or new code based on your context. This AI-powered tool supports multiple LLM models, including GPT-4o, GPT-4, Ollama, and more.** +![](./assets/codai-demo.gif) + + We use **two** main methods to manage context: **RAG** (Retrieval-Augmented Generation) and **Summarize Full Context of Code**. Each method has its own benefits and is chosen depending on the specific needs of the request. Below is a description of each method. diff --git a/assets/codai-demo.gif b/assets/codai-demo.gif new file mode 100644 index 0000000..f1c4945 Binary files /dev/null and b/assets/codai-demo.gif differ diff --git a/cmd/code.go b/cmd/code.go index 398df12..1c82aee 100644 --- a/cmd/code.go +++ b/cmd/code.go @@ -182,8 +182,6 @@ startLoop: // Label for the start loop finalPrompt, userInputPrompt := rootDependencies.Analyzer.GeneratePrompt(fullContextCodes, rootDependencies.ChatHistory.GetHistory(), userInput, requestedContext) // Step 7: Send the relevant code and user input to the AI API - var b = finalPrompt + userInputPrompt - fmt.Println(b) responseChan := rootDependencies.CurrentProvider.ChatCompletionRequest(ctx, userInputPrompt, finalPrompt) // Iterate over response channel to handle streamed data or errors. diff --git a/code_analyzer/analyzer.go b/code_analyzer/analyzer.go index 69094b4..e276ebb 100644 --- a/code_analyzer/analyzer.go +++ b/code_analyzer/analyzer.go @@ -244,42 +244,74 @@ func (analyzer *CodeAnalyzer) TryGetInCompletedCodeBlocK(relativePaths string) ( return requestedContext, nil } -// ExtractCodeChanges extracts code changes from the given text. -func (analyzer *CodeAnalyzer) ExtractCodeChanges(text string) []models.CodeChange { - if text == "" { - return nil - } +func (analyzer *CodeAnalyzer) ExtractCodeChanges(diff string) []models.CodeChange { + filePathPattern := regexp.MustCompile(`(?i)(?:\d+\.\s*|File:\s*)(\S+\.[a-zA-Z0-9]+)`) - // Regex patterns for file paths and code blocks - filePathPattern := regexp.MustCompile("(?i)(?:\\d+\\.\\s*|File:\\s*)[`']?([^\\s*`']+?\\.[a-zA-Z0-9]+)[`']?\\b") - // Capture entire diff blocks, assuming they are enclosed in ```diff ... ``` - codeBlockPattern := regexp.MustCompile("(?s)```[a-zA-Z0-9]*\\s*(.*?)\\s*```") + lines := strings.Split(diff, "\n") + var fileChanges []models.CodeChange - // Find all file path matches and code block matches - filePathMatches := filePathPattern.FindAllStringSubmatch(text, -1) - codeMatches := codeBlockPattern.FindAllStringSubmatch(text, -1) + var currentFilePath string + var currentCodeBlock []string + insideCodeBlock := false - // Ensure pairs are processed up to the minimum count of matches - minLength := len(filePathMatches) - if len(codeMatches) < minLength { - minLength = len(codeMatches) - } + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + + // Detect a new file path + if !insideCodeBlock && filePathPattern.MatchString(trimmedLine) { + // Add the previous file's change if there was one + if currentFilePath != "" && len(currentCodeBlock) > 0 { + fileChanges = append(fileChanges, models.CodeChange{ + RelativePath: currentFilePath, + Code: strings.Join(currentCodeBlock, "\n"), + }) + currentCodeBlock = nil + } + + // Capture the new file path + matches := filePathPattern.FindStringSubmatch(trimmedLine) + currentFilePath = matches[1] + continue + } - // Initialize code changes - var codeChanges []models.CodeChange - for i := 0; i < minLength; i++ { - relativePath := strings.TrimSpace(filePathMatches[i][1]) - code := strings.TrimSpace(codeMatches[i][1]) + // Start of a code block + if strings.HasPrefix(trimmedLine, "```") { + if !insideCodeBlock { + // Start a code block only if a file path is defined + if currentFilePath != "" { + insideCodeBlock = true + } + continue + } else { + // End the code block + insideCodeBlock = false + if currentFilePath != "" && len(currentCodeBlock) > 0 { + fileChanges = append(fileChanges, models.CodeChange{ + RelativePath: currentFilePath, + Code: strings.Join(currentCodeBlock, "\n"), + }) + currentCodeBlock = nil + currentFilePath = "" + } + continue + } + } - // Capture the relative path and associated diff content - codeChange := models.CodeChange{ - RelativePath: relativePath, - Code: code, + // Collect lines inside a code block + if insideCodeBlock { + currentCodeBlock = append(currentCodeBlock, line) } - codeChanges = append(codeChanges, codeChange) } - return codeChanges + // Add the last collected code block if any + if currentFilePath != "" && len(currentCodeBlock) > 0 { + fileChanges = append(fileChanges, models.CodeChange{ + RelativePath: currentFilePath, + Code: strings.Join(currentCodeBlock, "\n"), + }) + } + + return fileChanges } func (analyzer *CodeAnalyzer) ApplyChanges(relativePath, diff string) error { diff --git a/code_analyzer/analyzer_test.go b/code_analyzer/analyzer_test.go index 0e6c3db..c3a35cf 100644 --- a/code_analyzer/analyzer_test.go +++ b/code_analyzer/analyzer_test.go @@ -206,6 +206,28 @@ func TestApplyChanges_DeletedFile(t *testing.T) { assert.Equal(t, content, string(savedContent)) } +// / Test for ExtractCodeChanges with standard input +func TestExtractCodeChangesComplexText(t *testing.T) { + setup(t) + text := "Sure, I can help you create a simple Pacman game using Python. We'll use the `pygame` library for this purpose. If you don't have `pygame` installed, you can install it using `pip install pygame`.\n\nHere is a basic implementation of a Pacman game:\n\nFile: pacman_game.py\n```python\nimport pygame\nimport random\n\n# Initialize pygame\npygame.init()\n\n# Screen dimensions\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 600\n\n# Colors\nBLACK = (0, 0, 0)\nWHITE = (255, 255, 255)\nYELLOW = (255, 255, 0)\nRED = (255, 0, 0)\n\n# Pacman settings\nPACMAN_SIZE = 50\nPACMAN_SPEED = 5\n\n# Ghost settings\nGHOST_SIZE = 50\nGHOST_SPEED = 3\n\n# Create the screen\nscreen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))\npygame.display.set_caption(\"Pacman Game\")\n\n# Load images\npacman_image = pygame.image.load(\"pacman.png\")\npacman_image = pygame.transform.scale(pacman_image, (PACMAN_SIZE, PACMAN_SIZE))\n\nghost_image = pygame.image.load(\"ghost.png\")\nghost_image = pygame.transform.scale(ghost_image, (GHOST_SIZE, GHOST_SIZE))\n\n# Pacman class\nclass Pacman:\n def __init__(self):\n self.x = SCREEN_WIDTH // 2\n self.y = SCREEN_HEIGHT // 2\n self.speed = PACMAN_SPEED\n self.image = pacman_image\n\n def move(self, dx, dy):\n self.x += dx * self.speed\n self.y += dy * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - PACMAN_SIZE:\n self.x = SCREEN_WIDTH - PACMAN_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - PACMAN_SIZE:\n self.y = SCREEN_HEIGHT - PACMAN_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Ghost class\nclass Ghost:\n def __init__(self):\n self.x = random.randint(0, SCREEN_WIDTH - GHOST_SIZE)\n self.y = random.randint(0, SCREEN_HEIGHT - GHOST_SIZE)\n self.speed = GHOST_SPEED\n self.image = ghost_image\n\n def move(self):\n self.x += random.choice([-1, 1]) * self.speed\n self.y += random.choice([-1, 1]) * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - GHOST_SIZE:\n self.x = SCREEN_WIDTH - GHOST_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - GHOST_SIZE:\n self.y = SCREEN_HEIGHT - GHOST_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Main game loop\ndef main():\n clock = pygame.time.Clock()\n pacman = Pacman()\n ghosts = [Ghost() for _ in range(4)]\n\n running = True\n while running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False\n\n keys = pygame.key.get_pressed()\n dx = dy = 0\n if keys[pygame.K_LEFT]:\n dx = -1\n if keys[pygame.K_RIGHT]:\n dx = 1\n if keys[pygame.K_UP]:\n dy = -1\n if keys[pygame.K_DOWN]:\n dy = 1\n\n pacman.move(dx, dy)\n\n for ghost in ghosts:\n ghost.move()\n\n screen.fill(BLACK)\n pacman.draw()\n for ghost in ghosts:\n ghost.draw()\n\n pygame.display.flip()\n clock.tick(30)\n\n pygame.quit()\n\nif __name__ == \"__main__\":\n main()\n```\n\nThis code creates a simple Pacman game where Pacman can move around the screen using the arrow keys, and there are ghosts that move randomly. You need to have `pacman.png` and `ghost.png` images in the same directory as the script for it to work.\n\nTo run the game, save the code in a file named `pacman_game.py` and execute it with Python:\n\n```sh\npython pacman_game.py\n```\n\nEnjoy your game!" + + codeChanges := analyzer.ExtractCodeChanges(text) + + assert.Len(t, codeChanges, 1) + assert.Equal(t, "pacman_game.py", codeChanges[0].RelativePath) + assert.Equal(t, "import pygame\nimport random\n\n# Initialize pygame\npygame.init()\n\n# Screen dimensions\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 600\n\n# Colors\nBLACK = (0, 0, 0)\nWHITE = (255, 255, 255)\nYELLOW = (255, 255, 0)\nRED = (255, 0, 0)\n\n# Pacman settings\nPACMAN_SIZE = 50\nPACMAN_SPEED = 5\n\n# Ghost settings\nGHOST_SIZE = 50\nGHOST_SPEED = 3\n\n# Create the screen\nscreen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))\npygame.display.set_caption(\"Pacman Game\")\n\n# Load images\npacman_image = pygame.image.load(\"pacman.png\")\npacman_image = pygame.transform.scale(pacman_image, (PACMAN_SIZE, PACMAN_SIZE))\n\nghost_image = pygame.image.load(\"ghost.png\")\nghost_image = pygame.transform.scale(ghost_image, (GHOST_SIZE, GHOST_SIZE))\n\n# Pacman class\nclass Pacman:\n def __init__(self):\n self.x = SCREEN_WIDTH // 2\n self.y = SCREEN_HEIGHT // 2\n self.speed = PACMAN_SPEED\n self.image = pacman_image\n\n def move(self, dx, dy):\n self.x += dx * self.speed\n self.y += dy * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - PACMAN_SIZE:\n self.x = SCREEN_WIDTH - PACMAN_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - PACMAN_SIZE:\n self.y = SCREEN_HEIGHT - PACMAN_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Ghost class\nclass Ghost:\n def __init__(self):\n self.x = random.randint(0, SCREEN_WIDTH - GHOST_SIZE)\n self.y = random.randint(0, SCREEN_HEIGHT - GHOST_SIZE)\n self.speed = GHOST_SPEED\n self.image = ghost_image\n\n def move(self):\n self.x += random.choice([-1, 1]) * self.speed\n self.y += random.choice([-1, 1]) * self.speed\n\n # Boundary check\n if self.x < 0:\n self.x = 0\n elif self.x > SCREEN_WIDTH - GHOST_SIZE:\n self.x = SCREEN_WIDTH - GHOST_SIZE\n\n if self.y < 0:\n self.y = 0\n elif self.y > SCREEN_HEIGHT - GHOST_SIZE:\n self.y = SCREEN_HEIGHT - GHOST_SIZE\n\n def draw(self):\n screen.blit(self.image, (self.x, self.y))\n\n# Main game loop\ndef main():\n clock = pygame.time.Clock()\n pacman = Pacman()\n ghosts = [Ghost() for _ in range(4)]\n\n running = True\n while running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False\n\n keys = pygame.key.get_pressed()\n dx = dy = 0\n if keys[pygame.K_LEFT]:\n dx = -1\n if keys[pygame.K_RIGHT]:\n dx = 1\n if keys[pygame.K_UP]:\n dy = -1\n if keys[pygame.K_DOWN]:\n dy = 1\n\n pacman.move(dx, dy)\n\n for ghost in ghosts:\n ghost.move()\n\n screen.fill(BLACK)\n pacman.draw()\n for ghost in ghosts:\n ghost.draw()\n\n pygame.display.flip()\n clock.tick(30)\n\n pygame.quit()\n\nif __name__ == \"__main__\":\n main()", codeChanges[0].Code) +} + +// / Test for ExtractCodeChanges with standard input +func TestExtractCodeChangesComplexText2(t *testing.T) { + setup(t) + text := "Sure, I can help you create a simple \"Dino Game\" using Python and the Pygame library. This game will be similar to the Chrome Dino game where a dinosaur jumps over obstacles.\n\nFirst, ensure you have Pygame installed. You can install it using pip:\n```sh\npip install pygame\n```\n\nNow, let's create the game. We will need a few files:\n1. `main.py` - The main game logic.\n2. `dino.py` - The Dino character logic.\n3. `obstacle.py` - The obstacle logic.\n4. `settings.py` - Game settings and configurations.\n\n### File: `settings.py`\n```python\n# Game settings and configurations\n\nSCREEN_WIDTH = 800\nSCREEN_HEIGHT = 400\nFPS = 30\n\n# Colors\nWHITE = (255, 255, 255)\nBLACK = (0, 0, 0)\n\n# Dino settings\nDINO_WIDTH = 40\nDINO_HEIGHT = 60\nDINO_COLOR = (0, 255, 0)\nDINO_JUMP_HEIGHT = 10\n\n# Obstacle settings\nOBSTACLE_WIDTH = 20\nOBSTACLE_HEIGHT = 40\nOBSTACLE_COLOR = (255, 0, 0)\nOBSTACLE_SPEED = 5\n```\n\n### File: `dino.py`\n```python\nimport pygame\nfrom settings import DINO_WIDTH, DINO_HEIGHT, DINO_COLOR, DINO_JUMP_HEIGHT\n\nclass Dino:\n def __init__(self, x, y):\n self.rect = pygame.Rect(x, y, DINO_WIDTH, DINO_HEIGHT)\n self.color = DINO_COLOR\n self.is_jumping = False\n self.jump_speed = DINO_JUMP_HEIGHT\n self.gravity = 1\n\n def update(self):\n if self.is_jumping:\n self.rect.y -= self.jump_speed\n self.jump_speed -= self.gravity\n if self.jump_speed < -DINO_JUMP_HEIGHT:\n self.is_jumping = False\n self.jump_speed = DINO_JUMP_HEIGHT\n\n def draw(self, screen):\n pygame.draw.rect(screen, self.color, self.rect)\n\n def jump(self):\n if not self.is_jumping:\n self.is_jumping = True\n```\n\n### File: `obstacle.py`\n```python\nimport pygame\nfrom settings import OBSTACLE_WIDTH, OBSTACLE_HEIGHT, OBSTACLE_COLOR, OBSTACLE_SPEED\n\nclass Obstacle:\n def __init__(self, x, y):\n self.rect = pygame.Rect(x, y, OBSTACLE_WIDTH, OBSTACLE_HEIGHT)\n self.color = OBSTACLE_COLOR\n self.speed = OBSTACLE_SPEED\n\n def update(self):\n self.rect.x -= self.speed\n\n def draw(self, screen):\n pygame.draw.rect(screen, self.color, self.rect)\n```\n\n### File: `main.py`\n```python\nimport pygame\nimport random\nfrom settings import SCREEN_WIDTH, SCREEN_HEIGHT, FPS, WHITE\nfrom dino.py import Dino\nfrom obstacle.py import Obstacle\n\ndef main():\n pygame.init()\n screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))\n pygame.display.set_caption(\"Dino Game\")\n clock = pygame.time.Clock()\n\n dino = Dino(50, SCREEN_HEIGHT - 60)\n obstacles = []\n\n running = True\n while running:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n running = False\n if event.type == pygame.KEYDOWN:\n if event.key == pygame.K_SPACE:\n dino.jump()\n\n screen.fill(WHITE)\n\n dino.update()\n dino.draw(screen)\n\n if random.randint(1, 100) < 2:\n obstacles.append(Obstacle(SCREEN_WIDTH, SCREEN_HEIGHT - 40))\n\n for obstacle in obstacles[:]:\n obstacle.update()\n obstacle.draw(screen)\n if obstacle.rect.x < 0:\n obstacles.remove(obstacle)\n if dino.rect.colliderect(obstacle.rect):\n running = False\n\n pygame.display.flip()\n clock.tick(FPS)\n\n pygame.quit()\n\nif __name__ == \"__main__\":\n main()\n```\n\nThis code sets up a basic Dino game where the dinosaur can jump over obstacles. The game will end if the dinosaur collides with an obstacle. You can expand and improve this game by adding more features, such as scoring, different types of obstacles, and animations." + + codeChanges := analyzer.ExtractCodeChanges(text) + + assert.Len(t, codeChanges, 4) +} + // / Test for ExtractCodeChanges with standard input func TestExtractCodeChanges(t *testing.T) { setup(t) diff --git a/embed_data/prompts/rag_context_prompt.tmpl b/embed_data/prompts/rag_context_prompt.tmpl index 1a90c87..3b1aae1 100644 --- a/embed_data/prompts/rag_context_prompt.tmpl +++ b/embed_data/prompts/rag_context_prompt.tmpl @@ -21,7 +21,9 @@ - Prefix **"-"** for removed lines. - **Unchanged lines** should **not** be prefixed; they should remain as they are. - **Last line**: End of the markdown highlighted **Code BLOCK**. - - If you provide a **diff** always give me full code and **do not summarize that** only for **modify part** and I want also **all of unchanged line**. + - If you provide a **diff** always give me **full code** and **do not summarize that** only for **modify part**, always I want **full code**. + - Always add **relative path** and **file name** **top** of each **Code BLOCK**. + ## Code BLOCK Format: - Every code modification **must follow** this pattern strictly, ensuring that added, removed, and unchanged lines are marked correctly with **"+"**, **"-"**, or skipped for unchanged lines and keep in mind **give me all of unchanged line and dont summarize them.** diff --git a/embed_data/prompts/summarize_full_context_prompt.tmpl b/embed_data/prompts/summarize_full_context_prompt.tmpl index 3f2423f..bdd27c5 100644 --- a/embed_data/prompts/summarize_full_context_prompt.tmpl +++ b/embed_data/prompts/summarize_full_context_prompt.tmpl @@ -42,7 +42,8 @@ - Prefix **"-"** for removed lines. - **Unchanged lines** should **not** be prefixed; they should remain as they are. - **Last line**: End of the markdown highlighted **Code BLOCK**. - - If you provide a **diff** always give me full code and **do not summarize that** only for **modify part** and I want also **all of unchanged line**. + - If you provide a **diff** always give me **full code** and **do not summarize that** only for **modify part**, always I want **full code**. + - Always add **relative path** and **file name** **top** of each **Code BLOCK**. ## Code BLOCK Format: - Every code modification **must follow** this pattern strictly, ensuring that added, removed, and unchanged lines are marked correctly with **"+"**, **"-"**, or skipped for unchanged lines and keep in mind **give me all of unchanged line and dont summarize them.**