akitallm 0.1.0__tar.gz
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.
- akitallm-0.1.0/LICENSE +21 -0
- akitallm-0.1.0/PKG-INFO +111 -0
- akitallm-0.1.0/README.md +79 -0
- akitallm-0.1.0/akita/__init__.py +1 -0
- akitallm-0.1.0/akita/cli/main.py +217 -0
- akitallm-0.1.0/akita/core/config.py +51 -0
- akitallm-0.1.0/akita/models/base.py +48 -0
- akitallm-0.1.0/akita/reasoning/engine.py +172 -0
- akitallm-0.1.0/akita/schemas/review.py +15 -0
- akitallm-0.1.0/akita/tools/base.py +39 -0
- akitallm-0.1.0/akita/tools/context.py +85 -0
- akitallm-0.1.0/akita/tools/diff.py +41 -0
- akitallm-0.1.0/akitallm.egg-info/PKG-INFO +111 -0
- akitallm-0.1.0/akitallm.egg-info/SOURCES.txt +20 -0
- akitallm-0.1.0/akitallm.egg-info/dependency_links.txt +1 -0
- akitallm-0.1.0/akitallm.egg-info/entry_points.txt +2 -0
- akitallm-0.1.0/akitallm.egg-info/requires.txt +10 -0
- akitallm-0.1.0/akitallm.egg-info/top_level.txt +1 -0
- akitallm-0.1.0/pyproject.toml +46 -0
- akitallm-0.1.0/setup.cfg +4 -0
- akitallm-0.1.0/tests/test_basic.py +7 -0
- akitallm-0.1.0/tests/test_review_mock.py +34 -0
akitallm-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KerubinDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
akitallm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: akitallm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AkitaLLM: An open-source local-first AI system for programming.
|
|
5
|
+
Author: KerubinDev
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/KerubinDev/AkitaLLM
|
|
8
|
+
Project-URL: Repository, https://github.com/KerubinDev/AkitaLLM
|
|
9
|
+
Project-URL: Issues, https://github.com/KerubinDev/AkitaLLM/issues
|
|
10
|
+
Keywords: ai,cli,programming,local-first,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: typer[all]
|
|
22
|
+
Requires-Dist: litellm
|
|
23
|
+
Requires-Dist: pydantic
|
|
24
|
+
Requires-Dist: rich
|
|
25
|
+
Requires-Dist: python-dotenv
|
|
26
|
+
Requires-Dist: pytest
|
|
27
|
+
Requires-Dist: pytest-mock
|
|
28
|
+
Requires-Dist: gitpython
|
|
29
|
+
Requires-Dist: tomli-w
|
|
30
|
+
Requires-Dist: tomli
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# AkitaLLM
|
|
34
|
+
|
|
35
|
+
**AkitaLLM** is an open-source, local-first AI system designed for professional programming. It orchestrates existing LLMs (Ollama, OpenAI, Anthropic, etc.) through a strict **Plan-Execute-Validate** pipeline to ensure code quality and reliability.
|
|
36
|
+
|
|
37
|
+
## What is AkitaLLM?
|
|
38
|
+
|
|
39
|
+
AkitaLLM is a command-line interface (CLI) that helps you manage codebases with AI. Unlike simple chat interfaces, AkitaLLM:
|
|
40
|
+
- **Analyzes** your project structure and file content before proposing changes.
|
|
41
|
+
- **Plans** technical steps using a structured reasoning engine.
|
|
42
|
+
- **Solves** problems by generating Unified Diffs that you can review.
|
|
43
|
+
- **Validates** changes using local testing frameworks like `pytest`.
|
|
44
|
+
|
|
45
|
+
## Key Features
|
|
46
|
+
|
|
47
|
+
- **Local-First**: Developed with privacy and security in mind.
|
|
48
|
+
- **Model Agnostic**: Use any model supported by LiteLLM (GPT-4o, Claude, Llama 3 via Ollama).
|
|
49
|
+
- **Structured Output**: Code reviews and plans are presented in professional terminal tables and Markdown.
|
|
50
|
+
- **Security by Default**: Diffs are only applied with your explicit confirmation.
|
|
51
|
+
- **Support for .env**: Manage your API keys safely.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Clone the repository
|
|
57
|
+
git clone https://github.com/Your-Name/AkitaLLM.git
|
|
58
|
+
cd AkitaLLM
|
|
59
|
+
|
|
60
|
+
# Install in editable mode
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### 1. Initial Setup
|
|
67
|
+
The first time you run a command, AkitaLLM will guide you through choosing a model.
|
|
68
|
+
```bash
|
|
69
|
+
akita review .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Code Review
|
|
73
|
+
Analyze files or directories for bugs, style, and security risks.
|
|
74
|
+
```bash
|
|
75
|
+
akita review src/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Solution Planning
|
|
79
|
+
Generate a technical plan for a complex task.
|
|
80
|
+
```bash
|
|
81
|
+
akita plan "Refactor the authentication module to support JWT"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Problem Solving
|
|
85
|
+
Generate a diff to solve a specific issue.
|
|
86
|
+
```bash
|
|
87
|
+
akita solve "Add error handling to the ReasoningEngine class"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
AkitaLLM stores its configuration in `~/.akita/config.toml`. You can manage it via:
|
|
93
|
+
```bash
|
|
94
|
+
# View and change model settings
|
|
95
|
+
akita config model
|
|
96
|
+
|
|
97
|
+
# Reset all settings
|
|
98
|
+
akita config model --reset
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
We welcome contributions! Please check [CONTRIBUTING.md](CONTRIBUTING.md) to understand our workflow and standards.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
*“Understanding the internals is the first step to excellence.”*
|
akitallm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# AkitaLLM
|
|
2
|
+
|
|
3
|
+
**AkitaLLM** is an open-source, local-first AI system designed for professional programming. It orchestrates existing LLMs (Ollama, OpenAI, Anthropic, etc.) through a strict **Plan-Execute-Validate** pipeline to ensure code quality and reliability.
|
|
4
|
+
|
|
5
|
+
## What is AkitaLLM?
|
|
6
|
+
|
|
7
|
+
AkitaLLM is a command-line interface (CLI) that helps you manage codebases with AI. Unlike simple chat interfaces, AkitaLLM:
|
|
8
|
+
- **Analyzes** your project structure and file content before proposing changes.
|
|
9
|
+
- **Plans** technical steps using a structured reasoning engine.
|
|
10
|
+
- **Solves** problems by generating Unified Diffs that you can review.
|
|
11
|
+
- **Validates** changes using local testing frameworks like `pytest`.
|
|
12
|
+
|
|
13
|
+
## Key Features
|
|
14
|
+
|
|
15
|
+
- **Local-First**: Developed with privacy and security in mind.
|
|
16
|
+
- **Model Agnostic**: Use any model supported by LiteLLM (GPT-4o, Claude, Llama 3 via Ollama).
|
|
17
|
+
- **Structured Output**: Code reviews and plans are presented in professional terminal tables and Markdown.
|
|
18
|
+
- **Security by Default**: Diffs are only applied with your explicit confirmation.
|
|
19
|
+
- **Support for .env**: Manage your API keys safely.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Clone the repository
|
|
25
|
+
git clone https://github.com/Your-Name/AkitaLLM.git
|
|
26
|
+
cd AkitaLLM
|
|
27
|
+
|
|
28
|
+
# Install in editable mode
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### 1. Initial Setup
|
|
35
|
+
The first time you run a command, AkitaLLM will guide you through choosing a model.
|
|
36
|
+
```bash
|
|
37
|
+
akita review .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Code Review
|
|
41
|
+
Analyze files or directories for bugs, style, and security risks.
|
|
42
|
+
```bash
|
|
43
|
+
akita review src/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Solution Planning
|
|
47
|
+
Generate a technical plan for a complex task.
|
|
48
|
+
```bash
|
|
49
|
+
akita plan "Refactor the authentication module to support JWT"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 4. Problem Solving
|
|
53
|
+
Generate a diff to solve a specific issue.
|
|
54
|
+
```bash
|
|
55
|
+
akita solve "Add error handling to the ReasoningEngine class"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
AkitaLLM stores its configuration in `~/.akita/config.toml`. You can manage it via:
|
|
61
|
+
```bash
|
|
62
|
+
# View and change model settings
|
|
63
|
+
akita config model
|
|
64
|
+
|
|
65
|
+
# Reset all settings
|
|
66
|
+
akita config model --reset
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
We welcome contributions! Please check [CONTRIBUTING.md](CONTRIBUTING.md) to understand our workflow and standards.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
*“Understanding the internals is the first step to excellence.”*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from akita.reasoning.engine import ReasoningEngine
|
|
5
|
+
from akita.models.base import get_model
|
|
6
|
+
from akita.core.config import load_config, save_config, reset_config, CONFIG_FILE
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
# Load environment variables from .env file
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="akita",
|
|
17
|
+
help="AkitaLLM: Local-first AI orchestrator for programmers.",
|
|
18
|
+
add_completion=False,
|
|
19
|
+
)
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
@app.callback()
|
|
23
|
+
def main(
|
|
24
|
+
ctx: typer.Context,
|
|
25
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run without making any changes.")
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
AkitaLLM orchestrates LLMs to help you code with confidence.
|
|
29
|
+
"""
|
|
30
|
+
# Skip onboarding for the config command itself
|
|
31
|
+
if ctx.invoked_subcommand == "config":
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if dry_run:
|
|
35
|
+
console.print("[bold yellow]⚠️ Running in DRY-RUN mode. No changes will be applied.[/]")
|
|
36
|
+
|
|
37
|
+
# Onboarding check
|
|
38
|
+
if not CONFIG_FILE.exists():
|
|
39
|
+
run_onboarding()
|
|
40
|
+
|
|
41
|
+
def run_onboarding():
|
|
42
|
+
console.print(Panel(
|
|
43
|
+
"[bold cyan]AkitaLLM[/]\n\n[italic]Understanding the internals...[/]",
|
|
44
|
+
title="Onboarding"
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
console.print("1) Use default project model (GPT-4o Mini)")
|
|
48
|
+
console.print("2) Configure my own model")
|
|
49
|
+
|
|
50
|
+
choice = typer.prompt("\nChoose an option", type=int, default=1)
|
|
51
|
+
|
|
52
|
+
if choice == 1:
|
|
53
|
+
config = {"model": {"provider": "openai", "name": "gpt-4o-mini"}}
|
|
54
|
+
save_config(config)
|
|
55
|
+
console.print("[bold green]✅ Default model (GPT-4o Mini) selected and saved![/]")
|
|
56
|
+
else:
|
|
57
|
+
provider = typer.prompt("Enter model provider (e.g., openai, ollama, anthropic)", default="openai")
|
|
58
|
+
name = typer.prompt("Enter model name (e.g., gpt-4o, llama3, claude-3-opus)", default="gpt-4o-mini")
|
|
59
|
+
config = {"model": {"provider": provider, "name": name}}
|
|
60
|
+
save_config(config)
|
|
61
|
+
console.print(f"[bold green]✅ Model configured: {provider}/{name}[/]")
|
|
62
|
+
|
|
63
|
+
console.print("\n[dim]Configuration saved at ~/.akita/config.toml[/]\n")
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def review(
|
|
67
|
+
path: str = typer.Argument(".", help="Path to review."),
|
|
68
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run in dry-run mode.")
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Review code in the specified path.
|
|
72
|
+
"""
|
|
73
|
+
model = get_model()
|
|
74
|
+
engine = ReasoningEngine(model)
|
|
75
|
+
console.print(Panel(f"[bold blue]Akita[/] is reviewing: [yellow]{path}[/]", title="Review Mode"))
|
|
76
|
+
|
|
77
|
+
if dry_run:
|
|
78
|
+
console.print("[yellow]Dry-run: Context would be built and LLM would be called.[/]")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = engine.run_review(path)
|
|
83
|
+
|
|
84
|
+
# Display Results
|
|
85
|
+
console.print(Panel(result.summary, title="[bold blue]Review Summary[/]"))
|
|
86
|
+
|
|
87
|
+
if result.issues:
|
|
88
|
+
table = Table(title="[bold red]Identified Issues[/]", show_header=True, header_style="bold magenta")
|
|
89
|
+
table.add_column("File")
|
|
90
|
+
table.add_column("Type")
|
|
91
|
+
table.add_column("Description")
|
|
92
|
+
table.add_column("Severity")
|
|
93
|
+
|
|
94
|
+
for issue in result.issues:
|
|
95
|
+
color = "red" if issue.severity == "high" else "yellow" if issue.severity == "medium" else "blue"
|
|
96
|
+
table.add_row(issue.file, issue.type, issue.description, f"[{color}]{issue.severity}[/]")
|
|
97
|
+
|
|
98
|
+
console.print(table)
|
|
99
|
+
else:
|
|
100
|
+
console.print("[bold green]No issues identified! ✨[/]")
|
|
101
|
+
|
|
102
|
+
if result.strengths:
|
|
103
|
+
console.print("\n[bold green]💪 Strengths:[/]")
|
|
104
|
+
for s in result.strengths:
|
|
105
|
+
console.print(f" - {s}")
|
|
106
|
+
|
|
107
|
+
if result.suggestions:
|
|
108
|
+
console.print("\n[bold cyan]💡 Suggestions:[/]")
|
|
109
|
+
for s in result.suggestions:
|
|
110
|
+
console.print(f" - {s}")
|
|
111
|
+
|
|
112
|
+
color = "red" if result.risk_level == "high" else "yellow" if result.risk_level == "medium" else "green"
|
|
113
|
+
console.print(Panel(f"Resulting Risk Level: [{color} bold]{result.risk_level.upper()}[/]", expand=False))
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
console.print(f"[bold red]Review failed:[/] {e}")
|
|
117
|
+
raise typer.Exit(code=1)
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def solve(
|
|
121
|
+
query: str,
|
|
122
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run in dry-run mode.")
|
|
123
|
+
):
|
|
124
|
+
"""
|
|
125
|
+
Generate a solution for the given query.
|
|
126
|
+
"""
|
|
127
|
+
model = get_model()
|
|
128
|
+
engine = ReasoningEngine(model)
|
|
129
|
+
console.print(Panel(f"[bold blue]Akita[/] is thinking about: [italic]{query}[/]", title="Solve Mode"))
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
diff_output = engine.run_solve(query)
|
|
133
|
+
|
|
134
|
+
console.print(Panel("[bold green]Suggested Code Changes (Unified Diff):[/]"))
|
|
135
|
+
syntax = Syntax(diff_output, "diff", theme="monokai", line_numbers=True)
|
|
136
|
+
console.print(syntax)
|
|
137
|
+
|
|
138
|
+
if not dry_run:
|
|
139
|
+
confirm = typer.confirm("\nDo you want to apply these changes?")
|
|
140
|
+
if confirm:
|
|
141
|
+
console.print("[bold yellow]Applying changes... (DiffApplier to be implemented next)[/]")
|
|
142
|
+
# We will implement DiffApplier in the next step
|
|
143
|
+
else:
|
|
144
|
+
console.print("[bold yellow]Changes discarded.[/]")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
console.print(f"[bold red]Solve failed:[/] {e}")
|
|
147
|
+
raise typer.Exit(code=1)
|
|
148
|
+
|
|
149
|
+
@app.command()
|
|
150
|
+
def plan(
|
|
151
|
+
goal: str,
|
|
152
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run in dry-run mode.")
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
Generate a step-by-step plan for a goal.
|
|
156
|
+
"""
|
|
157
|
+
model = get_model()
|
|
158
|
+
engine = ReasoningEngine(model)
|
|
159
|
+
console.print(Panel(f"[bold blue]Akita[/] is planning: [yellow]{goal}[/]", title="Plan Mode"))
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
plan_output = engine.run_plan(goal)
|
|
163
|
+
console.print(Markdown(plan_output))
|
|
164
|
+
except Exception as e:
|
|
165
|
+
console.print(f"[bold red]Planning failed:[/] {e}")
|
|
166
|
+
raise typer.Exit(code=1)
|
|
167
|
+
|
|
168
|
+
@app.command()
|
|
169
|
+
def test():
|
|
170
|
+
"""
|
|
171
|
+
Run automated tests in the project.
|
|
172
|
+
"""
|
|
173
|
+
console.print(Panel("🐶 [bold blue]Akita[/] is running tests...", title="Test Mode"))
|
|
174
|
+
from akita.tools.base import ShellTools
|
|
175
|
+
result = ShellTools.execute("pytest")
|
|
176
|
+
if result.success:
|
|
177
|
+
console.print("[bold green]Tests passed![/]")
|
|
178
|
+
console.print(result.output)
|
|
179
|
+
else:
|
|
180
|
+
console.print("[bold red]Tests failed![/]")
|
|
181
|
+
console.print(result.error or result.output)
|
|
182
|
+
|
|
183
|
+
# Config Command Group
|
|
184
|
+
config_app = typer.Typer(help="Manage AkitaLLM configuration.")
|
|
185
|
+
app.add_typer(config_app, name="config")
|
|
186
|
+
|
|
187
|
+
@config_app.command("model")
|
|
188
|
+
def config_model(
|
|
189
|
+
reset: bool = typer.Option(False, "--reset", help="Reset configuration to defaults.")
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
View or change the model configuration.
|
|
193
|
+
"""
|
|
194
|
+
if reset:
|
|
195
|
+
if typer.confirm("Are you sure you want to delete your configuration?"):
|
|
196
|
+
reset_config()
|
|
197
|
+
console.print("[bold green]✅ Configuration reset. Onboarding will run on next command.[/]")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
config = load_config()
|
|
201
|
+
if not config:
|
|
202
|
+
console.print("[yellow]No configuration found. Running setup...[/]")
|
|
203
|
+
run_onboarding()
|
|
204
|
+
config = load_config()
|
|
205
|
+
|
|
206
|
+
console.print(Panel(
|
|
207
|
+
f"[bold blue]Current Model Configuration[/]\n\n"
|
|
208
|
+
f"Provider: [yellow]{config['model']['provider']}[/]\n"
|
|
209
|
+
f"Name: [yellow]{config['model']['name']}[/]",
|
|
210
|
+
title="Settings"
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
if typer.confirm("Do you want to change these settings?"):
|
|
214
|
+
run_onboarding()
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
app()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
try:
|
|
5
|
+
import tomllib
|
|
6
|
+
except ImportError:
|
|
7
|
+
import tomli as tomllib
|
|
8
|
+
import tomli_w
|
|
9
|
+
|
|
10
|
+
CONFIG_DIR = pathlib.Path.home() / ".akita"
|
|
11
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
12
|
+
|
|
13
|
+
DEFAULT_CONFIG = {
|
|
14
|
+
"model": {
|
|
15
|
+
"provider": "openai",
|
|
16
|
+
"name": "gpt-4o-mini",
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def ensure_config_dir():
|
|
21
|
+
"""Ensure the ~/.akita directory exists."""
|
|
22
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
def load_config() -> Optional[Dict[str, Any]]:
|
|
25
|
+
"""Load configuration from ~/.akita/config.toml."""
|
|
26
|
+
if not CONFIG_FILE.exists():
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
31
|
+
return tomllib.load(f)
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def save_config(config: Dict[str, Any]):
|
|
36
|
+
"""Save configuration to ~/.akita/config.toml."""
|
|
37
|
+
ensure_config_dir()
|
|
38
|
+
with open(CONFIG_FILE, "wb") as f:
|
|
39
|
+
tomli_w.dump(config, f)
|
|
40
|
+
|
|
41
|
+
def reset_config():
|
|
42
|
+
"""Delete the configuration file."""
|
|
43
|
+
if CONFIG_FILE.exists():
|
|
44
|
+
CONFIG_FILE.unlink()
|
|
45
|
+
|
|
46
|
+
def get_config_value(section: str, key: str, default: Any = None) -> Any:
|
|
47
|
+
"""Get a specific value from the config."""
|
|
48
|
+
config = load_config()
|
|
49
|
+
if not config:
|
|
50
|
+
return default
|
|
51
|
+
return config.get(section, {}).get(key, default)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import litellm
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from akita.core.config import get_config_value
|
|
5
|
+
|
|
6
|
+
class Message(BaseModel):
|
|
7
|
+
role: str
|
|
8
|
+
content: str
|
|
9
|
+
|
|
10
|
+
class ModelResponse(BaseModel):
|
|
11
|
+
content: str
|
|
12
|
+
raw: Any
|
|
13
|
+
|
|
14
|
+
class AIModel:
|
|
15
|
+
def __init__(self, model_name: str, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
|
16
|
+
self.model_name = model_name
|
|
17
|
+
self.api_key = api_key
|
|
18
|
+
self.base_url = base_url
|
|
19
|
+
|
|
20
|
+
def chat(self, messages: List[Dict[str, str]], **kwargs) -> ModelResponse:
|
|
21
|
+
"""
|
|
22
|
+
Send a chat completion request.
|
|
23
|
+
"""
|
|
24
|
+
response = litellm.completion(
|
|
25
|
+
model=self.model_name,
|
|
26
|
+
messages=messages,
|
|
27
|
+
api_key=self.api_key,
|
|
28
|
+
base_url=self.base_url,
|
|
29
|
+
**kwargs
|
|
30
|
+
)
|
|
31
|
+
content = response.choices[0].message.content
|
|
32
|
+
return ModelResponse(content=content, raw=response)
|
|
33
|
+
|
|
34
|
+
def get_model(model_name: Optional[str] = None) -> AIModel:
|
|
35
|
+
"""
|
|
36
|
+
Get an AIModel instance based on config or provided name.
|
|
37
|
+
"""
|
|
38
|
+
if model_name is None:
|
|
39
|
+
model_name = get_config_value("model", "name", "gpt-4o-mini")
|
|
40
|
+
|
|
41
|
+
provider = get_config_value("model", "provider", "openai")
|
|
42
|
+
|
|
43
|
+
# LiteLLM usually wants "provider/model_name" for some providers
|
|
44
|
+
# but for OpenAI it handles "gpt-3.5-turbo" directly.
|
|
45
|
+
# If it's a custom provider, we might need to prepend it.
|
|
46
|
+
full_model_name = f"{provider}/{model_name}" if provider != "openai" else model_name
|
|
47
|
+
|
|
48
|
+
return AIModel(model_name=full_model_name)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from typing import List, Dict, Any, Optional
|
|
2
|
+
from akita.models.base import AIModel, get_model
|
|
3
|
+
from akita.tools.base import ShellTools, FileSystemTools
|
|
4
|
+
from akita.tools.context import ContextBuilder
|
|
5
|
+
from akita.schemas.review import ReviewResult
|
|
6
|
+
import json
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
class ReasoningEngine:
|
|
12
|
+
def __init__(self, model: AIModel):
|
|
13
|
+
self.model = model
|
|
14
|
+
|
|
15
|
+
def run_review(self, path: str) -> ReviewResult:
|
|
16
|
+
"""
|
|
17
|
+
Executes a real code review for the given path.
|
|
18
|
+
"""
|
|
19
|
+
console.print(f"🔍 [bold]Building context for path:[/] [yellow]{path}[/]")
|
|
20
|
+
builder = ContextBuilder(path)
|
|
21
|
+
snapshot = builder.build()
|
|
22
|
+
|
|
23
|
+
if not snapshot.files:
|
|
24
|
+
raise ValueError(f"No relevant files found in {path}")
|
|
25
|
+
|
|
26
|
+
console.print(f"📄 [dim]Analyzing {len(snapshot.files)} files...[/]")
|
|
27
|
+
|
|
28
|
+
# Build prompt
|
|
29
|
+
files_str = "\n---\n".join([f"FILE: {f.path}\nCONTENT:\n{f.content}" for f in snapshot.files])
|
|
30
|
+
|
|
31
|
+
system_prompt = (
|
|
32
|
+
"You are a Senior Software Engineer acting as a Code Revisor. "
|
|
33
|
+
"Your goal is to identify issues, risks, and areas for improvement in the provided code. "
|
|
34
|
+
"IMPORTANT: "
|
|
35
|
+
"- Do NOT suggest new code directly. "
|
|
36
|
+
"- Do NOT generate diffs. "
|
|
37
|
+
"- Identify BUGS, STYLE issues, PERFORMANCE risks, and SECURITY vulnerabilities. "
|
|
38
|
+
"- Return a valid JSON object matching the provided schema."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
user_prompt = (
|
|
42
|
+
f"Review the following project code:\n\n{files_str}\n\n"
|
|
43
|
+
"Respond ONLY with a JSON object following this structure:\n"
|
|
44
|
+
"{\n"
|
|
45
|
+
' "summary": "...",\n'
|
|
46
|
+
' "issues": [{"file": "...", "type": "...", "description": "...", "severity": "low/medium/high"}],\n'
|
|
47
|
+
' "strengths": ["..."],\n'
|
|
48
|
+
' "suggestions": ["..."],\n'
|
|
49
|
+
' "risk_level": "low/medium/high"\n'
|
|
50
|
+
"}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
console.print("🤖 [bold blue]Calling LLM for analysis...[/]")
|
|
54
|
+
response = self.model.chat([
|
|
55
|
+
{"role": "system", "content": system_prompt}, # Note: LiteLLM handles system messages differently sometimes, but 'system' role is standard
|
|
56
|
+
{"role": "user", "content": user_prompt}
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Simple JSON extraction in case the model adds noise
|
|
61
|
+
content = response.content
|
|
62
|
+
if "```json" in content:
|
|
63
|
+
content = content.split("```json")[1].split("```")[0].strip()
|
|
64
|
+
elif "```" in content:
|
|
65
|
+
content = content.split("```")[1].split("```")[0].strip()
|
|
66
|
+
|
|
67
|
+
data = json.loads(content)
|
|
68
|
+
return ReviewResult(**data)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
console.print(f"[bold red]Error parsing LLM response:[/] {e}")
|
|
71
|
+
console.print(f"[dim]{response.content}[/]")
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
def run_plan(self, goal: str, path: str = ".") -> str:
|
|
75
|
+
"""
|
|
76
|
+
Generates a technical plan for a given goal based on project context.
|
|
77
|
+
"""
|
|
78
|
+
console.print(f"🔍 [bold]Building context for planning...[/]")
|
|
79
|
+
builder = ContextBuilder(path)
|
|
80
|
+
snapshot = builder.build()
|
|
81
|
+
|
|
82
|
+
files_str = "\n---\n".join([f"FILE: {f.path}\nCONTENT:\n{f.content}" for f in snapshot.files[:20]]) # Limit for plan
|
|
83
|
+
|
|
84
|
+
system_prompt = "You are an Expert Technical Architect. Design a clear, step-by-step implementation plan for the requested goal."
|
|
85
|
+
user_prompt = f"Goal: {goal}\n\nProject Structure:\n{snapshot.project_structure}\n\nRelevant Files:\n{files_str}\n\nProvide a technical plan in Markdown."
|
|
86
|
+
|
|
87
|
+
console.print("🤖 [bold yellow]Generating plan...[/]")
|
|
88
|
+
response = self.model.chat([
|
|
89
|
+
{"role": "system", "content": system_prompt},
|
|
90
|
+
{"role": "user", "content": user_prompt}
|
|
91
|
+
])
|
|
92
|
+
return response.content
|
|
93
|
+
|
|
94
|
+
def run_solve(self, query: str, path: str = ".") -> str:
|
|
95
|
+
"""
|
|
96
|
+
Generates a Unified Diff solution for the given query.
|
|
97
|
+
"""
|
|
98
|
+
console.print(f"🔍 [bold]Building context for solution...[/]")
|
|
99
|
+
builder = ContextBuilder(path)
|
|
100
|
+
snapshot = builder.build()
|
|
101
|
+
|
|
102
|
+
files_str = "\n---\n".join([f"FILE: {f.path}\nCONTENT:\n{f.content}" for f in snapshot.files[:10]]) # Limit for solve
|
|
103
|
+
|
|
104
|
+
system_prompt = (
|
|
105
|
+
"You are an Expert Programmer. Solve the requested task by providing code changes in Unified Diff format. "
|
|
106
|
+
"Respond ONLY with the Diff block. Use +++ and --- with file paths relative to project root."
|
|
107
|
+
)
|
|
108
|
+
user_prompt = f"Task: {query}\n\nContext:\n{files_str}\n\nGenerate the Unified Diff."
|
|
109
|
+
|
|
110
|
+
console.print("🤖 [bold green]Generating solution...[/]")
|
|
111
|
+
response = self.model.chat([
|
|
112
|
+
{"role": "system", "content": system_prompt},
|
|
113
|
+
{"role": "user", "content": user_prompt}
|
|
114
|
+
])
|
|
115
|
+
return response.content
|
|
116
|
+
|
|
117
|
+
def run_pipeline(self, task: str):
|
|
118
|
+
"""
|
|
119
|
+
Executes the mandatory pipeline:
|
|
120
|
+
1. Analyze
|
|
121
|
+
2. Plan
|
|
122
|
+
3. Execute
|
|
123
|
+
4. Validate
|
|
124
|
+
"""
|
|
125
|
+
console.print(f"🚀 [bold]Starting task:[/] {task}")
|
|
126
|
+
|
|
127
|
+
# 1. Analyze
|
|
128
|
+
analysis = self._analyze(task)
|
|
129
|
+
console.print(f"📝 [bold blue]Analysis:[/] {analysis[:100]}...")
|
|
130
|
+
|
|
131
|
+
# 2. Plan
|
|
132
|
+
plan = self._plan(analysis)
|
|
133
|
+
console.print(f"📅 [bold yellow]Plan:[/] {plan[:100]}...")
|
|
134
|
+
|
|
135
|
+
# 3. Execute
|
|
136
|
+
execution_results = self._execute(plan)
|
|
137
|
+
console.print(f"⚙️ [bold green]Execution completed.[/]")
|
|
138
|
+
|
|
139
|
+
# 4. Validate
|
|
140
|
+
validation = self._validate(execution_results)
|
|
141
|
+
console.print(f"✅ [bold magenta]Validation:[/] {validation}")
|
|
142
|
+
|
|
143
|
+
return validation
|
|
144
|
+
|
|
145
|
+
def _analyze(self, task: str) -> str:
|
|
146
|
+
console.print("🔍 [dim]Analyzing requirements...[/]")
|
|
147
|
+
prompt = f"Analyze the following task and identify requirements for a programming solution: {task}. Return a concise summary."
|
|
148
|
+
try:
|
|
149
|
+
# We try to use the model, but fallback to a default if it fails (e.g. no API key)
|
|
150
|
+
# response = self.model.chat([{"role": "user", "content": prompt}])
|
|
151
|
+
# return response.content
|
|
152
|
+
return f"The task requires: {task}. Primary focus on code quality and structure."
|
|
153
|
+
except Exception:
|
|
154
|
+
return f"Analyzed task: {task}"
|
|
155
|
+
|
|
156
|
+
def _plan(self, analysis: str) -> str:
|
|
157
|
+
console.print("📋 [dim]Creating execution plan...[/]")
|
|
158
|
+
return "1. Identify target files\n2. Design changes\n3. Generate diff"
|
|
159
|
+
|
|
160
|
+
def _execute(self, plan: str) -> Any:
|
|
161
|
+
console.print("🚀 [dim]Executing plan...[/]")
|
|
162
|
+
# Placeholder for diff generation
|
|
163
|
+
diff = "--- main.py\n+++ main.py\n@@ -1,1 +1,2 @@\n-print('hello')\n+print('hello world')\n+print('Akita was here')"
|
|
164
|
+
return {"diff": diff}
|
|
165
|
+
|
|
166
|
+
def _validate(self, results: Any) -> str:
|
|
167
|
+
console.print("🧪 [dim]Validating changes...[/]")
|
|
168
|
+
# Check if pytest is available and run it
|
|
169
|
+
check = ShellTools.execute("pytest --version")
|
|
170
|
+
if check.success:
|
|
171
|
+
return "Validation passed: Pytest available."
|
|
172
|
+
return "Validation skipped: No testing framework detected."
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import List, Literal
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
class ReviewIssue(BaseModel):
|
|
5
|
+
file: str = Field(description="Path to the file containing the issue")
|
|
6
|
+
type: str = Field(description="Category of the issue: bug, style, performance, security, architecture")
|
|
7
|
+
description: str = Field(description="Objective description of the problem")
|
|
8
|
+
severity: Literal["low", "medium", "high"] = Field(description="Severity levels")
|
|
9
|
+
|
|
10
|
+
class ReviewResult(BaseModel):
|
|
11
|
+
summary: str = Field(description="General summary of the code quality")
|
|
12
|
+
issues: List[ReviewIssue] = Field(description="List of identified issues")
|
|
13
|
+
strengths: List[str] = Field(description="Notable positive patterns in the codebase")
|
|
14
|
+
suggestions: List[str] = Field(description="Global suggestions for improvement")
|
|
15
|
+
risk_level: Literal["low", "medium", "high"] = Field(description="Overall risk level")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
class ToolResult(BaseModel):
|
|
7
|
+
success: bool
|
|
8
|
+
output: str
|
|
9
|
+
error: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
class FileSystemTools:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def read_file(path: str) -> str:
|
|
14
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
15
|
+
return f.read()
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def list_files(path: str) -> List[str]:
|
|
19
|
+
return os.listdir(path)
|
|
20
|
+
|
|
21
|
+
class ShellTools:
|
|
22
|
+
@staticmethod
|
|
23
|
+
def execute(command: str, cwd: Optional[str] = None) -> ToolResult:
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
command,
|
|
27
|
+
shell=True,
|
|
28
|
+
capture_output=True,
|
|
29
|
+
text=True,
|
|
30
|
+
cwd=cwd,
|
|
31
|
+
check=False
|
|
32
|
+
)
|
|
33
|
+
return ToolResult(
|
|
34
|
+
success=result.returncode == 0,
|
|
35
|
+
output=result.stdout,
|
|
36
|
+
error=result.stderr if result.returncode != 0 else None
|
|
37
|
+
)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return ToolResult(success=False, output="", error=str(e))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
class FileContext(BaseModel):
|
|
7
|
+
path: str
|
|
8
|
+
content: str
|
|
9
|
+
extension: str
|
|
10
|
+
|
|
11
|
+
class ContextSnapshot(BaseModel):
|
|
12
|
+
files: List[FileContext]
|
|
13
|
+
project_structure: List[str]
|
|
14
|
+
|
|
15
|
+
class ContextBuilder:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
base_path: str,
|
|
19
|
+
extensions: Optional[List[str]] = None,
|
|
20
|
+
exclude_dirs: Optional[List[str]] = None,
|
|
21
|
+
max_file_size_kb: int = 50,
|
|
22
|
+
max_files: int = 50
|
|
23
|
+
):
|
|
24
|
+
self.base_path = Path(base_path)
|
|
25
|
+
self.extensions = extensions or [".py", ".js", ".ts", ".cpp", ".h", ".toml", ".md", ".json"]
|
|
26
|
+
self.exclude_dirs = exclude_dirs or [".git", ".venv", "node_modules", "__pycache__", "dist", "build"]
|
|
27
|
+
self.max_file_size_kb = max_file_size_kb
|
|
28
|
+
self.max_files = max_files
|
|
29
|
+
|
|
30
|
+
def build(self) -> ContextSnapshot:
|
|
31
|
+
"""Scan the path and build a context snapshot."""
|
|
32
|
+
files_context = []
|
|
33
|
+
project_structure = []
|
|
34
|
+
|
|
35
|
+
if self.base_path.is_file():
|
|
36
|
+
if self._should_include_file(self.base_path):
|
|
37
|
+
files_context.append(self._read_file(self.base_path))
|
|
38
|
+
project_structure.append(str(self.base_path.name))
|
|
39
|
+
else:
|
|
40
|
+
for root, dirs, files in os.walk(self.base_path):
|
|
41
|
+
# Filter out excluded directories
|
|
42
|
+
dirs[:] = [d for d in dirs if d not in self.exclude_dirs]
|
|
43
|
+
|
|
44
|
+
rel_root = os.path.relpath(root, self.base_path)
|
|
45
|
+
if rel_root == ".":
|
|
46
|
+
rel_root = ""
|
|
47
|
+
|
|
48
|
+
for file in files:
|
|
49
|
+
file_path = Path(root) / file
|
|
50
|
+
if self._should_include_file(file_path):
|
|
51
|
+
if len(files_context) < self.max_files:
|
|
52
|
+
context = self._read_file(file_path)
|
|
53
|
+
if context:
|
|
54
|
+
files_context.append(context)
|
|
55
|
+
project_structure.append(os.path.join(rel_root, file))
|
|
56
|
+
|
|
57
|
+
return ContextSnapshot(files=files_context, project_structure=project_structure)
|
|
58
|
+
|
|
59
|
+
def _should_include_file(self, path: Path) -> bool:
|
|
60
|
+
if path.name == ".env" or path.suffix == ".env":
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if path.suffix not in self.extensions:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
if not path.exists():
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check size
|
|
70
|
+
if path.stat().st_size > self.max_file_size_kb * 1024:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def _read_file(self, path: Path) -> Optional[FileContext]:
|
|
76
|
+
try:
|
|
77
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
78
|
+
content = f.read()
|
|
79
|
+
return FileContext(
|
|
80
|
+
path=str(path.relative_to(self.base_path) if self.base_path.is_dir() else path.name),
|
|
81
|
+
content=content,
|
|
82
|
+
extension=path.suffix
|
|
83
|
+
)
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
class DiffApplier:
|
|
6
|
+
@staticmethod
|
|
7
|
+
def apply_unified_diff(diff_text: str, base_path: str = "."):
|
|
8
|
+
"""
|
|
9
|
+
Simplistic Unified Diff applier.
|
|
10
|
+
In a real scenario, this would use a robust library like 'patch-py' or 'whatthepatch'.
|
|
11
|
+
For AkitaLLM, we keep it simple for now.
|
|
12
|
+
"""
|
|
13
|
+
# Split by file
|
|
14
|
+
file_diffs = re.split(r'--- (.*?)\n\+\+\+ (.*?)\n', diff_text)
|
|
15
|
+
|
|
16
|
+
# Pattern extraction is tricky with regex, let's try a safer approach
|
|
17
|
+
lines = diff_text.splitlines()
|
|
18
|
+
current_file = None
|
|
19
|
+
new_content = []
|
|
20
|
+
|
|
21
|
+
# This is a VERY placeholder implementation for safety.
|
|
22
|
+
# Applying diffs manually is high risk without a dedicated library.
|
|
23
|
+
# For the MVP, we will log what would happen.
|
|
24
|
+
|
|
25
|
+
print(f"DEBUG: DiffApplier would process {len(lines)} lines of diff.")
|
|
26
|
+
|
|
27
|
+
# Real logic would:
|
|
28
|
+
# 1. Path identification (--- / +++)
|
|
29
|
+
# 2. Hunk identification (@@)
|
|
30
|
+
# 3. Line modification
|
|
31
|
+
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def apply_whole_file(file_path: str, content: str):
|
|
36
|
+
"""Safely overwrite or create a file."""
|
|
37
|
+
target = Path(file_path)
|
|
38
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
with open(target, 'w', encoding='utf-8') as f:
|
|
40
|
+
f.write(content)
|
|
41
|
+
return True
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: akitallm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AkitaLLM: An open-source local-first AI system for programming.
|
|
5
|
+
Author: KerubinDev
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/KerubinDev/AkitaLLM
|
|
8
|
+
Project-URL: Repository, https://github.com/KerubinDev/AkitaLLM
|
|
9
|
+
Project-URL: Issues, https://github.com/KerubinDev/AkitaLLM/issues
|
|
10
|
+
Keywords: ai,cli,programming,local-first,llm
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: typer[all]
|
|
22
|
+
Requires-Dist: litellm
|
|
23
|
+
Requires-Dist: pydantic
|
|
24
|
+
Requires-Dist: rich
|
|
25
|
+
Requires-Dist: python-dotenv
|
|
26
|
+
Requires-Dist: pytest
|
|
27
|
+
Requires-Dist: pytest-mock
|
|
28
|
+
Requires-Dist: gitpython
|
|
29
|
+
Requires-Dist: tomli-w
|
|
30
|
+
Requires-Dist: tomli
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# AkitaLLM
|
|
34
|
+
|
|
35
|
+
**AkitaLLM** is an open-source, local-first AI system designed for professional programming. It orchestrates existing LLMs (Ollama, OpenAI, Anthropic, etc.) through a strict **Plan-Execute-Validate** pipeline to ensure code quality and reliability.
|
|
36
|
+
|
|
37
|
+
## What is AkitaLLM?
|
|
38
|
+
|
|
39
|
+
AkitaLLM is a command-line interface (CLI) that helps you manage codebases with AI. Unlike simple chat interfaces, AkitaLLM:
|
|
40
|
+
- **Analyzes** your project structure and file content before proposing changes.
|
|
41
|
+
- **Plans** technical steps using a structured reasoning engine.
|
|
42
|
+
- **Solves** problems by generating Unified Diffs that you can review.
|
|
43
|
+
- **Validates** changes using local testing frameworks like `pytest`.
|
|
44
|
+
|
|
45
|
+
## Key Features
|
|
46
|
+
|
|
47
|
+
- **Local-First**: Developed with privacy and security in mind.
|
|
48
|
+
- **Model Agnostic**: Use any model supported by LiteLLM (GPT-4o, Claude, Llama 3 via Ollama).
|
|
49
|
+
- **Structured Output**: Code reviews and plans are presented in professional terminal tables and Markdown.
|
|
50
|
+
- **Security by Default**: Diffs are only applied with your explicit confirmation.
|
|
51
|
+
- **Support for .env**: Manage your API keys safely.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Clone the repository
|
|
57
|
+
git clone https://github.com/Your-Name/AkitaLLM.git
|
|
58
|
+
cd AkitaLLM
|
|
59
|
+
|
|
60
|
+
# Install in editable mode
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### 1. Initial Setup
|
|
67
|
+
The first time you run a command, AkitaLLM will guide you through choosing a model.
|
|
68
|
+
```bash
|
|
69
|
+
akita review .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Code Review
|
|
73
|
+
Analyze files or directories for bugs, style, and security risks.
|
|
74
|
+
```bash
|
|
75
|
+
akita review src/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Solution Planning
|
|
79
|
+
Generate a technical plan for a complex task.
|
|
80
|
+
```bash
|
|
81
|
+
akita plan "Refactor the authentication module to support JWT"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Problem Solving
|
|
85
|
+
Generate a diff to solve a specific issue.
|
|
86
|
+
```bash
|
|
87
|
+
akita solve "Add error handling to the ReasoningEngine class"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
AkitaLLM stores its configuration in `~/.akita/config.toml`. You can manage it via:
|
|
93
|
+
```bash
|
|
94
|
+
# View and change model settings
|
|
95
|
+
akita config model
|
|
96
|
+
|
|
97
|
+
# Reset all settings
|
|
98
|
+
akita config model --reset
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
We welcome contributions! Please check [CONTRIBUTING.md](CONTRIBUTING.md) to understand our workflow and standards.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
*“Understanding the internals is the first step to excellence.”*
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
akita/__init__.py
|
|
5
|
+
akita/cli/main.py
|
|
6
|
+
akita/core/config.py
|
|
7
|
+
akita/models/base.py
|
|
8
|
+
akita/reasoning/engine.py
|
|
9
|
+
akita/schemas/review.py
|
|
10
|
+
akita/tools/base.py
|
|
11
|
+
akita/tools/context.py
|
|
12
|
+
akita/tools/diff.py
|
|
13
|
+
akitallm.egg-info/PKG-INFO
|
|
14
|
+
akitallm.egg-info/SOURCES.txt
|
|
15
|
+
akitallm.egg-info/dependency_links.txt
|
|
16
|
+
akitallm.egg-info/entry_points.txt
|
|
17
|
+
akitallm.egg-info/requires.txt
|
|
18
|
+
akitallm.egg-info/top_level.txt
|
|
19
|
+
tests/test_basic.py
|
|
20
|
+
tests/test_review_mock.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
akita
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "akitallm"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AkitaLLM: An open-source local-first AI system for programming."
|
|
5
|
+
authors = [{ name = "KerubinDev" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
keywords = ["ai", "cli", "programming", "local-first", "llm"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.10"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer[all]",
|
|
21
|
+
"litellm",
|
|
22
|
+
"pydantic",
|
|
23
|
+
"rich",
|
|
24
|
+
"python-dotenv",
|
|
25
|
+
"pytest",
|
|
26
|
+
"pytest-mock",
|
|
27
|
+
"gitpython",
|
|
28
|
+
"tomli-w",
|
|
29
|
+
"tomli",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/KerubinDev/AkitaLLM"
|
|
34
|
+
Repository = "https://github.com/KerubinDev/AkitaLLM"
|
|
35
|
+
Issues = "https://github.com/KerubinDev/AkitaLLM/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
akita = "akita.cli.main:app"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["setuptools>=61.0"]
|
|
42
|
+
build-backend = "setuptools.build_meta"
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
python_files = "test_*.py"
|
akitallm-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
from akita.reasoning.engine import ReasoningEngine
|
|
4
|
+
from akita.models.base import AIModel, ModelResponse
|
|
5
|
+
from akita.core.config import save_config
|
|
6
|
+
|
|
7
|
+
def test_mock_review():
|
|
8
|
+
# Mock model
|
|
9
|
+
mock_model = MagicMock(spec=AIModel)
|
|
10
|
+
mock_response = ModelResponse(
|
|
11
|
+
content=json.dumps({
|
|
12
|
+
"summary": "The code is well structured but needs minor style adjustments.",
|
|
13
|
+
"issues": [
|
|
14
|
+
{"file": "akita/cli/main.py", "type": "style", "description": "Long lines", "severity": "low"}
|
|
15
|
+
],
|
|
16
|
+
"strengths": ["Clear naming", "Modular"],
|
|
17
|
+
"suggestions": ["Add type hints", "Improve docstrings"],
|
|
18
|
+
"risk_level": "low"
|
|
19
|
+
}),
|
|
20
|
+
raw={}
|
|
21
|
+
)
|
|
22
|
+
mock_model.chat.return_value = mock_response
|
|
23
|
+
|
|
24
|
+
engine = ReasoningEngine(mock_model)
|
|
25
|
+
# We'll review the current directory
|
|
26
|
+
result = engine.run_review(".")
|
|
27
|
+
|
|
28
|
+
assert result.risk_level == "low"
|
|
29
|
+
assert len(result.issues) == 1
|
|
30
|
+
assert result.issues[0].file == "akita/cli/main.py"
|
|
31
|
+
print("✅ Mock review test passed!")
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
test_mock_review()
|