akitallm 0.1.1__tar.gz → 1.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.1 → akitallm-1.1.0}/PKG-INFO +8 -11
- {akitallm-0.1.1 → akitallm-1.1.0}/README.md +4 -10
- akitallm-1.1.0/akita/__init__.py +1 -0
- akitallm-1.1.0/akita/cli/main.py +346 -0
- akitallm-1.1.0/akita/core/ast_utils.py +77 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akita/core/config.py +12 -2
- akitallm-1.1.0/akita/core/indexing.py +94 -0
- akitallm-1.1.0/akita/core/plugins.py +81 -0
- akitallm-1.1.0/akita/core/providers.py +181 -0
- akitallm-1.1.0/akita/core/trace.py +18 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akita/models/base.py +12 -7
- akitallm-1.1.0/akita/plugins/__init__.py +1 -0
- akitallm-1.1.0/akita/plugins/files.py +34 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akita/reasoning/engine.py +44 -18
- akitallm-1.1.0/akita/reasoning/session.py +15 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akita/tools/base.py +6 -1
- {akitallm-0.1.1 → akitallm-1.1.0}/akita/tools/context.py +54 -9
- akitallm-1.1.0/akita/tools/diff.py +116 -0
- akitallm-1.1.0/akita/tools/git.py +79 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/PKG-INFO +8 -11
- {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/SOURCES.txt +13 -0
- akitallm-1.1.0/akitallm.egg-info/entry_points.txt +5 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/requires.txt +3 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/pyproject.toml +7 -1
- akitallm-1.1.0/tests/test_ast.py +50 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/tests/test_basic.py +1 -1
- akitallm-1.1.0/tests/test_diff.py +68 -0
- akitallm-1.1.0/tests/test_interactive.py +33 -0
- akitallm-1.1.0/tests/test_plugins.py +41 -0
- akitallm-0.1.1/akita/__init__.py +0 -1
- akitallm-0.1.1/akita/cli/main.py +0 -217
- akitallm-0.1.1/akita/tools/diff.py +0 -41
- akitallm-0.1.1/akitallm.egg-info/entry_points.txt +0 -2
- {akitallm-0.1.1 → akitallm-1.1.0}/LICENSE +0 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akita/schemas/review.py +0 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/dependency_links.txt +0 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/top_level.txt +0 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/setup.cfg +0 -0
- {akitallm-0.1.1 → akitallm-1.1.0}/tests/test_review_mock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: akitallm
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: AkitaLLM: An open-source local-first AI system for programming.
|
|
5
5
|
Author: KerubinDev
|
|
6
6
|
License: MIT
|
|
@@ -28,17 +28,11 @@ Requires-Dist: pytest-mock
|
|
|
28
28
|
Requires-Dist: gitpython
|
|
29
29
|
Requires-Dist: tomli-w
|
|
30
30
|
Requires-Dist: tomli
|
|
31
|
+
Requires-Dist: whatthepatch>=1.0.5
|
|
32
|
+
Requires-Dist: tree-sitter>=0.21.3
|
|
33
|
+
Requires-Dist: tree-sitter-python>=0.21.0
|
|
31
34
|
Dynamic: license-file
|
|
32
35
|
|
|
33
|
-
```text
|
|
34
|
-
_ _ _ _ _ _ __ __
|
|
35
|
-
/ \ | | _(_) |_ __ _| | | | | \/ |
|
|
36
|
-
/ _ \ | |/ / | __/ _` | | | | | |\/| |
|
|
37
|
-
/ ___ \ | <| | || (_| | |___| |___| | | |
|
|
38
|
-
/_/ \_\ |_|\_\_|\__\__,_|_____|_____|_| |_|
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
|
|
42
36
|
# AkitaLLM
|
|
43
37
|
### A deterministic, local-first AI orchestrator for software engineers.
|
|
44
38
|
|
|
@@ -132,7 +126,10 @@ akita solve "Improve error handling in the reasoning engine to prevent silent fa
|
|
|
132
126
|
|
|
133
127
|
---
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
### 🔌 Extensibility
|
|
130
|
+
AkitaLLM is built to be extended. You can create your own tools and plugins. Check the [Plugin Development Guide](PLUGINS.md) for more details.
|
|
131
|
+
|
|
132
|
+
## 🤝 Contributing
|
|
136
133
|
|
|
137
134
|
We are looking for engineers, not just coders. If you value robust abstractions, clean code, and predictable systems, your contribution is welcome.
|
|
138
135
|
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
```text
|
|
2
|
-
_ _ _ _ _ _ __ __
|
|
3
|
-
/ \ | | _(_) |_ __ _| | | | | \/ |
|
|
4
|
-
/ _ \ | |/ / | __/ _` | | | | | |\/| |
|
|
5
|
-
/ ___ \ | <| | || (_| | |___| |___| | | |
|
|
6
|
-
/_/ \_\ |_|\_\_|\__\__,_|_____|_____|_| |_|
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
|
|
10
1
|
# AkitaLLM
|
|
11
2
|
### A deterministic, local-first AI orchestrator for software engineers.
|
|
12
3
|
|
|
@@ -100,7 +91,10 @@ akita solve "Improve error handling in the reasoning engine to prevent silent fa
|
|
|
100
91
|
|
|
101
92
|
---
|
|
102
93
|
|
|
103
|
-
|
|
94
|
+
### 🔌 Extensibility
|
|
95
|
+
AkitaLLM is built to be extended. You can create your own tools and plugins. Check the [Plugin Development Guide](PLUGINS.md) for more details.
|
|
96
|
+
|
|
97
|
+
## 🤝 Contributing
|
|
104
98
|
|
|
105
99
|
We are looking for engineers, not just coders. If you value robust abstractions, clean code, and predictable systems, your contribution is welcome.
|
|
106
100
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from akita.reasoning.engine import ReasoningEngine
|
|
6
|
+
from akita.core.indexing import CodeIndexer
|
|
7
|
+
from akita.models.base import get_model
|
|
8
|
+
from akita.core.config import load_config, save_config, reset_config, CONFIG_FILE
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.markdown import Markdown
|
|
11
|
+
from rich.syntax import Syntax
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
from akita.tools.diff import DiffApplier
|
|
14
|
+
from akita.tools.git import GitTool
|
|
15
|
+
from akita.core.providers import detect_provider
|
|
16
|
+
|
|
17
|
+
# Load environment variables from .env file
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="akita",
|
|
22
|
+
help="AkitaLLM: Local-first AI orchestrator for programmers.",
|
|
23
|
+
add_completion=False,
|
|
24
|
+
)
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
@app.callback()
|
|
28
|
+
def main(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run without making any changes.")
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
AkitaLLM orchestrates LLMs to help you code with confidence.
|
|
34
|
+
"""
|
|
35
|
+
# Skip onboarding for the config command itself
|
|
36
|
+
if ctx.invoked_subcommand == "config":
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if dry_run:
|
|
40
|
+
console.print("[bold yellow]⚠️ Running in DRY-RUN mode. No changes will be applied.[/]")
|
|
41
|
+
|
|
42
|
+
# Onboarding check
|
|
43
|
+
if not CONFIG_FILE.exists():
|
|
44
|
+
run_onboarding()
|
|
45
|
+
|
|
46
|
+
def run_onboarding():
|
|
47
|
+
console.print(Panel(
|
|
48
|
+
"[bold cyan]AkitaLLM Configuration[/]\n\n[italic]API-first setup...[/]",
|
|
49
|
+
title="Onboarding"
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
api_key = typer.prompt("🔑 Paste your API Key (or type 'ollama' for local)", hide_input=False)
|
|
53
|
+
|
|
54
|
+
provider = detect_provider(api_key)
|
|
55
|
+
if not provider:
|
|
56
|
+
console.print("[bold red]❌ Could not detect provider from the given key.[/]")
|
|
57
|
+
console.print("Make sure you are using a valid OpenAI (sk-...) or Anthropic (sk-ant-...) key.")
|
|
58
|
+
raise typer.Abort()
|
|
59
|
+
|
|
60
|
+
console.print(f"[bold green]✅ Detected Provider:[/] {provider.name.upper()}")
|
|
61
|
+
|
|
62
|
+
with console.status(f"[bold blue]Consulting {provider.name} API for available models..."):
|
|
63
|
+
try:
|
|
64
|
+
models = provider.list_models(api_key)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
console.print(f"[bold red]❌ Failed to list models:[/] {e}")
|
|
67
|
+
raise typer.Abort()
|
|
68
|
+
|
|
69
|
+
if not models:
|
|
70
|
+
console.print("[bold yellow]⚠️ No models found for this provider.[/]")
|
|
71
|
+
raise typer.Abort()
|
|
72
|
+
|
|
73
|
+
console.print("\n[bold]Select a model:[/]")
|
|
74
|
+
for i, model in enumerate(models):
|
|
75
|
+
name_display = f" ({model.name})" if model.name else ""
|
|
76
|
+
console.print(f"{i+1}) [cyan]{model.id}[/]{name_display}")
|
|
77
|
+
|
|
78
|
+
choice = typer.prompt("\nChoose a model number", type=int, default=1)
|
|
79
|
+
if 1 <= choice <= len(models):
|
|
80
|
+
selected_model = models[choice-1].id
|
|
81
|
+
else:
|
|
82
|
+
console.print("[bold red]Invalid choice.[/]")
|
|
83
|
+
raise typer.Abort()
|
|
84
|
+
|
|
85
|
+
# Determine if we should save the key or use an env ref
|
|
86
|
+
use_env = typer.confirm("Would you like to use an environment variable for the API key? (Recommended)", default=True)
|
|
87
|
+
|
|
88
|
+
final_key_ref = api_key
|
|
89
|
+
if use_env and provider.name != "ollama":
|
|
90
|
+
env_var_name = f"{provider.name.upper()}_API_KEY"
|
|
91
|
+
console.print(f"[dim]Please ensure you set [bold]{env_var_name}[/] in your .env or shell.[/]")
|
|
92
|
+
final_key_ref = f"env:{env_var_name}"
|
|
93
|
+
|
|
94
|
+
config = {
|
|
95
|
+
"model": {
|
|
96
|
+
"provider": provider.name,
|
|
97
|
+
"name": selected_model,
|
|
98
|
+
"api_key": final_key_ref
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
save_config(config)
|
|
103
|
+
console.print(f"\n[bold green]✨ Configuration saved![/]")
|
|
104
|
+
console.print(f"Model: [bold]{selected_model}[/]")
|
|
105
|
+
console.print(f"Key reference: [dim]{final_key_ref}[/]")
|
|
106
|
+
console.print("\n[dim]Configuration stored at ~/.akita/config.toml[/]\n")
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def review(
|
|
110
|
+
path: str = typer.Argument(".", help="Path to review."),
|
|
111
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run in dry-run mode.")
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Review code in the specified path.
|
|
115
|
+
"""
|
|
116
|
+
model = get_model()
|
|
117
|
+
engine = ReasoningEngine(model)
|
|
118
|
+
console.print(Panel(f"[bold blue]Akita[/] is reviewing: [yellow]{path}[/]", title="Review Mode"))
|
|
119
|
+
|
|
120
|
+
if dry_run:
|
|
121
|
+
console.print("[yellow]Dry-run: Context would be built and LLM would be called.[/]")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = engine.run_review(path)
|
|
126
|
+
|
|
127
|
+
# Display Results
|
|
128
|
+
console.print(Panel(result.summary, title="[bold blue]Review Summary[/]"))
|
|
129
|
+
|
|
130
|
+
if result.issues:
|
|
131
|
+
table = Table(title="[bold red]Identified Issues[/]", show_header=True, header_style="bold magenta")
|
|
132
|
+
table.add_column("File")
|
|
133
|
+
table.add_column("Type")
|
|
134
|
+
table.add_column("Description")
|
|
135
|
+
table.add_column("Severity")
|
|
136
|
+
|
|
137
|
+
for issue in result.issues:
|
|
138
|
+
color = "red" if issue.severity == "high" else "yellow" if issue.severity == "medium" else "blue"
|
|
139
|
+
table.add_row(issue.file, issue.type, issue.description, f"[{color}]{issue.severity}[/]")
|
|
140
|
+
|
|
141
|
+
console.print(table)
|
|
142
|
+
else:
|
|
143
|
+
console.print("[bold green]No issues identified! ✨[/]")
|
|
144
|
+
|
|
145
|
+
if result.strengths:
|
|
146
|
+
console.print("\n[bold green]💪 Strengths:[/]")
|
|
147
|
+
for s in result.strengths:
|
|
148
|
+
console.print(f" - {s}")
|
|
149
|
+
|
|
150
|
+
if result.suggestions:
|
|
151
|
+
console.print("\n[bold cyan]💡 Suggestions:[/]")
|
|
152
|
+
for s in result.suggestions:
|
|
153
|
+
console.print(f" - {s}")
|
|
154
|
+
|
|
155
|
+
color = "red" if result.risk_level == "high" else "yellow" if result.risk_level == "medium" else "green"
|
|
156
|
+
console.print(Panel(f"Resulting Risk Level: [{color} bold]{result.risk_level.upper()}[/]", expand=False))
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
console.print(f"[bold red]Review failed:[/] {e}")
|
|
160
|
+
raise typer.Exit(code=1)
|
|
161
|
+
|
|
162
|
+
@app.command()
|
|
163
|
+
def solve(
|
|
164
|
+
query: str,
|
|
165
|
+
interactive: bool = typer.Option(False, "--interactive", "-i", help="Run in interactive mode to refine the solution."),
|
|
166
|
+
trace: bool = typer.Option(False, "--trace", help="Show the internal reasoning trace."),
|
|
167
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run in dry-run mode.")
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Generate and apply a solution for the given query.
|
|
171
|
+
"""
|
|
172
|
+
model = get_model()
|
|
173
|
+
engine = ReasoningEngine(model)
|
|
174
|
+
console.print(Panel(f"[bold blue]Akita[/] is thinking about: [italic]{query}[/]", title="Solve Mode"))
|
|
175
|
+
|
|
176
|
+
current_query = query
|
|
177
|
+
session = None
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
while True:
|
|
181
|
+
diff_output = engine.run_solve(current_query, session=session)
|
|
182
|
+
session = engine.session
|
|
183
|
+
|
|
184
|
+
if trace:
|
|
185
|
+
console.print(Panel(str(engine.trace), title="[bold cyan]Reasoning Trace[/]", border_style="cyan"))
|
|
186
|
+
console.print(Panel("[bold green]Suggested Code Changes (Unified Diff):[/]"))
|
|
187
|
+
syntax = Syntax(diff_output, "diff", theme="monokai", line_numbers=True)
|
|
188
|
+
console.print(syntax)
|
|
189
|
+
|
|
190
|
+
if interactive:
|
|
191
|
+
action = typer.prompt("\n[A]pprove, [R]efine with feedback, or [C]ancel?", default="A").upper()
|
|
192
|
+
if action == "A":
|
|
193
|
+
break
|
|
194
|
+
elif action == "R":
|
|
195
|
+
current_query = typer.prompt("Enter your feedback/refinement")
|
|
196
|
+
continue
|
|
197
|
+
else:
|
|
198
|
+
console.print("[yellow]Operation cancelled.[/]")
|
|
199
|
+
return
|
|
200
|
+
else:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if not dry_run:
|
|
204
|
+
confirm = typer.confirm("\nDo you want to apply these changes?")
|
|
205
|
+
if confirm:
|
|
206
|
+
console.print("[bold yellow]🚀 Applying changes...[/]")
|
|
207
|
+
success = DiffApplier.apply_unified_diff(diff_output)
|
|
208
|
+
if success:
|
|
209
|
+
console.print("[bold green]✅ Changes applied successfully![/]")
|
|
210
|
+
else:
|
|
211
|
+
console.print("[bold red]❌ Failed to apply changes.[/]")
|
|
212
|
+
else:
|
|
213
|
+
console.print("[bold yellow]Changes discarded.[/]")
|
|
214
|
+
except Exception as e:
|
|
215
|
+
console.print(f"[bold red]Solve failed:[/] {e}")
|
|
216
|
+
raise typer.Exit(code=1)
|
|
217
|
+
|
|
218
|
+
@app.command()
|
|
219
|
+
def plan(
|
|
220
|
+
goal: str,
|
|
221
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Run in dry-run mode.")
|
|
222
|
+
):
|
|
223
|
+
"""
|
|
224
|
+
Generate a step-by-step plan for a goal.
|
|
225
|
+
"""
|
|
226
|
+
model = get_model()
|
|
227
|
+
engine = ReasoningEngine(model)
|
|
228
|
+
console.print(Panel(f"[bold blue]Akita[/] is planning: [yellow]{goal}[/]", title="Plan Mode"))
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
plan_output = engine.run_plan(goal)
|
|
232
|
+
console.print(Markdown(plan_output))
|
|
233
|
+
except Exception as e:
|
|
234
|
+
console.print(f"[bold red]Planning failed:[/] {e}")
|
|
235
|
+
raise typer.Exit(code=1)
|
|
236
|
+
|
|
237
|
+
@app.command()
|
|
238
|
+
def clone(
|
|
239
|
+
url: str = typer.Argument(..., help="Git repository URL to clone."),
|
|
240
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Specific branch to clone."),
|
|
241
|
+
depth: Optional[int] = typer.Option(None, "--depth", "-d", help="Create a shallow clone with a history truncated to the specified number of commits.")
|
|
242
|
+
):
|
|
243
|
+
"""
|
|
244
|
+
Clone a remote Git repository into the Akita workspace (~/.akita/repos/).
|
|
245
|
+
"""
|
|
246
|
+
console.print(Panel(f"🌐 [bold blue]Akita[/] is cloning: [yellow]{url}[/]", title="Clone Mode"))
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
with console.status("[bold green]Cloning repository..."):
|
|
250
|
+
local_path = GitTool.clone_repo(url, branch=branch, depth=depth)
|
|
251
|
+
|
|
252
|
+
console.print(f"\n[bold green]✅ Repository cloned successfully![/]")
|
|
253
|
+
console.print(f"📍 Local path: [cyan]{local_path}[/]")
|
|
254
|
+
except FileExistsError as e:
|
|
255
|
+
console.print(f"[bold yellow]⚠️ {e}[/]")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
console.print(f"[bold red]❌ Clone failed:[/] {e}")
|
|
258
|
+
raise typer.Exit(code=1)
|
|
259
|
+
|
|
260
|
+
@app.command()
|
|
261
|
+
def index(
|
|
262
|
+
path: str = typer.Argument(".", help="Path to index for RAG.")
|
|
263
|
+
):
|
|
264
|
+
"""
|
|
265
|
+
Build a local vector index (RAG) for the project.
|
|
266
|
+
"""
|
|
267
|
+
console.print(Panel(f"🔍 [bold blue]Akita[/] is indexing: [yellow]{path}[/]", title="Index Mode"))
|
|
268
|
+
try:
|
|
269
|
+
indexer = CodeIndexer(path)
|
|
270
|
+
with console.status("[bold green]Indexing project files..."):
|
|
271
|
+
indexer.index_project()
|
|
272
|
+
console.print("[bold green]✅ Indexing complete! Semantic search is now active.[/]")
|
|
273
|
+
except Exception as e:
|
|
274
|
+
console.print(f"[bold red]Indexing failed:[/] {e}")
|
|
275
|
+
raise typer.Exit(code=1)
|
|
276
|
+
|
|
277
|
+
@app.command()
|
|
278
|
+
def test():
|
|
279
|
+
"""
|
|
280
|
+
Run automated tests in the project.
|
|
281
|
+
"""
|
|
282
|
+
console.print(Panel("[bold blue]Akita[/] is running tests...", title="Test Mode"))
|
|
283
|
+
from akita.tools.base import ShellTools
|
|
284
|
+
result = ShellTools.execute("pytest")
|
|
285
|
+
if result.success:
|
|
286
|
+
console.print("[bold green]Tests passed![/]")
|
|
287
|
+
console.print(result.output)
|
|
288
|
+
else:
|
|
289
|
+
console.print("[bold red]Tests failed![/]")
|
|
290
|
+
console.print(result.error or result.output)
|
|
291
|
+
|
|
292
|
+
@app.command()
|
|
293
|
+
def docs():
|
|
294
|
+
"""
|
|
295
|
+
Start the local documentation server.
|
|
296
|
+
"""
|
|
297
|
+
import subprocess
|
|
298
|
+
import sys
|
|
299
|
+
|
|
300
|
+
console.print(Panel("[bold blue]Akita[/] Documentation", title="Docs Mode"))
|
|
301
|
+
console.print("[dim]Starting MkDocs server...[/]")
|
|
302
|
+
console.print("[bold green]Open your browser at: http://127.0.0.1:8000[/]")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
subprocess.run([sys.executable, "-m", "mkdocs", "serve"], check=True)
|
|
306
|
+
except FileNotFoundError:
|
|
307
|
+
console.print("[red]MkDocs not found. Install it with: pip install mkdocs-material[/]")
|
|
308
|
+
raise typer.Exit(code=1)
|
|
309
|
+
except KeyboardInterrupt:
|
|
310
|
+
console.print("[yellow]Documentation server stopped.[/]")
|
|
311
|
+
|
|
312
|
+
# Config Command Group
|
|
313
|
+
config_app = typer.Typer(help="Manage AkitaLLM configuration.")
|
|
314
|
+
app.add_typer(config_app, name="config")
|
|
315
|
+
|
|
316
|
+
@config_app.command("model")
|
|
317
|
+
def config_model(
|
|
318
|
+
reset: bool = typer.Option(False, "--reset", help="Reset configuration to defaults.")
|
|
319
|
+
):
|
|
320
|
+
"""
|
|
321
|
+
View or change the model configuration.
|
|
322
|
+
"""
|
|
323
|
+
if reset:
|
|
324
|
+
if typer.confirm("Are you sure you want to delete your configuration?"):
|
|
325
|
+
reset_config()
|
|
326
|
+
console.print("[bold green]✅ Configuration reset. Onboarding will run on next command.[/]")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
config = load_config()
|
|
330
|
+
if not config:
|
|
331
|
+
console.print("[yellow]No configuration found. Running setup...[/]")
|
|
332
|
+
run_onboarding()
|
|
333
|
+
config = load_config()
|
|
334
|
+
|
|
335
|
+
console.print(Panel(
|
|
336
|
+
f"[bold blue]Current Model Configuration[/]\n\n"
|
|
337
|
+
f"Provider: [yellow]{config['model']['provider']}[/]\n"
|
|
338
|
+
f"Name: [yellow]{config['model']['name']}[/]",
|
|
339
|
+
title="Settings"
|
|
340
|
+
))
|
|
341
|
+
|
|
342
|
+
if typer.confirm("Do you want to change these settings?"):
|
|
343
|
+
run_onboarding()
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
app()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import tree_sitter_python as tspython
|
|
2
|
+
from tree_sitter import Language, Parser
|
|
3
|
+
import pathlib
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
class ASTParser:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.language = Language(tspython.language())
|
|
9
|
+
self.parser = Parser(self.language)
|
|
10
|
+
|
|
11
|
+
def parse_file(self, file_path: str) -> Optional[Any]:
|
|
12
|
+
path = pathlib.Path(file_path)
|
|
13
|
+
if not path.exists():
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
with open(path, "rb") as f:
|
|
17
|
+
content = f.read()
|
|
18
|
+
|
|
19
|
+
return self.parser.parse(content)
|
|
20
|
+
|
|
21
|
+
def get_definitions(self, file_path: str) -> List[Dict[str, Any]]:
|
|
22
|
+
"""Extract classes and functions with their line ranges using recursive traversal."""
|
|
23
|
+
tree = self.parse_file(file_path)
|
|
24
|
+
if not tree:
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
with open(file_path, "rb") as f:
|
|
28
|
+
content = f.read()
|
|
29
|
+
|
|
30
|
+
definitions = []
|
|
31
|
+
|
|
32
|
+
def explore(node):
|
|
33
|
+
# Check for definitions
|
|
34
|
+
if node.type in ["class_definition", "function_definition", "decorated_definition"]:
|
|
35
|
+
# Find name
|
|
36
|
+
name_node = node.child_by_field_name("name")
|
|
37
|
+
if not name_node and node.type == "decorated_definition":
|
|
38
|
+
# For decorated definitions, the name is in the class/function child
|
|
39
|
+
inner = node.child_by_field_name("definition")
|
|
40
|
+
if inner:
|
|
41
|
+
name_node = inner.child_by_field_name("name")
|
|
42
|
+
|
|
43
|
+
name = content[name_node.start_byte:name_node.end_byte].decode("utf-8") if name_node else "anonymous"
|
|
44
|
+
|
|
45
|
+
# Docstring extraction
|
|
46
|
+
docstring = None
|
|
47
|
+
body = node.child_by_field_name("body")
|
|
48
|
+
if body and body.children:
|
|
49
|
+
for stmt in body.children:
|
|
50
|
+
if stmt.type == "expression_statement":
|
|
51
|
+
child = stmt.children[0]
|
|
52
|
+
if child.type == "string":
|
|
53
|
+
docstring = content[child.start_byte:child.end_byte].decode("utf-8").strip('"\' \n')
|
|
54
|
+
break # Only first statement
|
|
55
|
+
|
|
56
|
+
definitions.append({
|
|
57
|
+
"name": name,
|
|
58
|
+
"type": "class" if "class" in node.type else "function",
|
|
59
|
+
"start_line": node.start_point[0] + 1,
|
|
60
|
+
"end_line": node.end_point[0] + 1,
|
|
61
|
+
"docstring": docstring
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Always explore children regardless of current node type
|
|
65
|
+
for child in node.children:
|
|
66
|
+
explore(child)
|
|
67
|
+
|
|
68
|
+
explore(tree.root_node)
|
|
69
|
+
return definitions
|
|
70
|
+
|
|
71
|
+
def get_source_segment(self, file_path: str, start_line: int, end_line: int) -> str:
|
|
72
|
+
"""Extract a segment of code from a file by line numbers."""
|
|
73
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
74
|
+
lines = f.readlines()
|
|
75
|
+
|
|
76
|
+
# Lines are 1-indexed in our definitions, but 0-indexed in the list
|
|
77
|
+
return "".join(lines[start_line-1 : end_line])
|
|
@@ -43,9 +43,19 @@ def reset_config():
|
|
|
43
43
|
if CONFIG_FILE.exists():
|
|
44
44
|
CONFIG_FILE.unlink()
|
|
45
45
|
|
|
46
|
+
def resolve_config_value(value: Any) -> Any:
|
|
47
|
+
"""
|
|
48
|
+
Resolves values like 'env:VAR_NAME' to their environment variable content.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(value, str) and value.startswith("env:"):
|
|
51
|
+
env_var = value[4:]
|
|
52
|
+
return os.getenv(env_var, value)
|
|
53
|
+
return value
|
|
54
|
+
|
|
46
55
|
def get_config_value(section: str, key: str, default: Any = None) -> Any:
|
|
47
|
-
"""Get a specific value from the config."""
|
|
56
|
+
"""Get a specific value from the config and resolve env refs."""
|
|
48
57
|
config = load_config()
|
|
49
58
|
if not config:
|
|
50
59
|
return default
|
|
51
|
-
|
|
60
|
+
val = config.get(section, {}).get(key, default)
|
|
61
|
+
return resolve_config_value(val)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
from akita.core.ast_utils import ASTParser
|
|
7
|
+
|
|
8
|
+
class CodeIndexer:
|
|
9
|
+
"""
|
|
10
|
+
A lightweight, zero-dependency semantic-keyword indexer.
|
|
11
|
+
Uses basic TF-IDF principles and AST-aware keyword weighting.
|
|
12
|
+
Works perfectly even in restricted environments like Python 3.14.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, project_path: str):
|
|
15
|
+
self.project_path = Path(project_path)
|
|
16
|
+
self.index_file = self.project_path / ".akita" / "index.json"
|
|
17
|
+
self.ast_parser = ASTParser()
|
|
18
|
+
self.data: List[Dict[str, Any]] = []
|
|
19
|
+
self.load_index()
|
|
20
|
+
|
|
21
|
+
def load_index(self):
|
|
22
|
+
if self.index_file.exists():
|
|
23
|
+
try:
|
|
24
|
+
with open(self.index_file, "r", encoding="utf-8") as f:
|
|
25
|
+
self.data = json.load(f)
|
|
26
|
+
except Exception:
|
|
27
|
+
self.data = []
|
|
28
|
+
|
|
29
|
+
def save_index(self):
|
|
30
|
+
self.index_file.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
with open(self.index_file, "w", encoding="utf-8") as f:
|
|
32
|
+
json.dump(self.data, f, indent=2)
|
|
33
|
+
|
|
34
|
+
def index_project(self):
|
|
35
|
+
"""Index all Python files in the project."""
|
|
36
|
+
self.data = []
|
|
37
|
+
for root, _, files in os.walk(self.project_path):
|
|
38
|
+
if ".akita" in root or ".git" in root or "__pycache__" in root:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
for file in files:
|
|
42
|
+
if file.endswith(".py"):
|
|
43
|
+
full_path = Path(root) / file
|
|
44
|
+
rel_path = full_path.relative_to(self.project_path)
|
|
45
|
+
self._index_file(full_path, str(rel_path))
|
|
46
|
+
self.save_index()
|
|
47
|
+
|
|
48
|
+
def _index_file(self, file_path: Path, rel_path: str):
|
|
49
|
+
try:
|
|
50
|
+
definitions = self.ast_parser.get_definitions(str(file_path))
|
|
51
|
+
for d in definitions:
|
|
52
|
+
source = self.ast_parser.get_source_segment(
|
|
53
|
+
str(file_path), d["start_line"], d["end_line"]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Create a searchable representation (keyword rich)
|
|
57
|
+
# We normalize case and extract meaningful words
|
|
58
|
+
search_blob = f"{d['name']} {d['type']} {d['docstring'] or ''} {source}"
|
|
59
|
+
keywords = set(re.findall(r'\w+', search_blob.lower()))
|
|
60
|
+
|
|
61
|
+
self.data.append({
|
|
62
|
+
"path": rel_path,
|
|
63
|
+
"name": d["name"],
|
|
64
|
+
"type": d["type"],
|
|
65
|
+
"start_line": d["start_line"],
|
|
66
|
+
"end_line": d["end_line"],
|
|
67
|
+
"keywords": list(keywords),
|
|
68
|
+
"content": source[:500] # Store snippet preview
|
|
69
|
+
})
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def search(self, query: str, n_results: int = 5) -> List[Dict[str, Any]]:
|
|
74
|
+
"""Search using Jaccard Similarity on keywords (Lite Contextual Search)."""
|
|
75
|
+
query_keywords = set(re.findall(r'\w+', query.lower()))
|
|
76
|
+
if not query_keywords:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
scores = []
|
|
80
|
+
for item in self.data:
|
|
81
|
+
item_keywords = set(item["keywords"])
|
|
82
|
+
intersection = query_keywords.intersection(item_keywords)
|
|
83
|
+
# Simple intersection count as score, weighted by name match
|
|
84
|
+
score = len(intersection)
|
|
85
|
+
if any(qk in item["name"].lower() for qk in query_keywords):
|
|
86
|
+
score += 5 # Boost explicit name matches
|
|
87
|
+
|
|
88
|
+
if score > 0:
|
|
89
|
+
scores.append((score, item))
|
|
90
|
+
|
|
91
|
+
# Sort by score descending
|
|
92
|
+
scores.sort(key=lambda x: x[0], reverse=True)
|
|
93
|
+
|
|
94
|
+
return [s[1] for s in scores[:n_results]]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import inspect
|
|
5
|
+
import pkgutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Dict, Any, Type, Optional
|
|
8
|
+
|
|
9
|
+
class AkitaPlugin(abc.ABC):
|
|
10
|
+
"""Base class for all AkitaLLM plugins."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def name(self) -> str:
|
|
15
|
+
"""Unique name of the plugin."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def description(self) -> str:
|
|
21
|
+
"""Brief description of what the plugin does."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abc.abstractmethod
|
|
25
|
+
def get_tools(self) -> List[Dict[str, Any]]:
|
|
26
|
+
"""Return a list of tools (functions) provided by this plugin."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class PluginManager:
|
|
30
|
+
def __init__(self, internal_plugins_path: Optional[str] = None):
|
|
31
|
+
self.plugins: Dict[str, AkitaPlugin] = {}
|
|
32
|
+
self.internal_path = internal_plugins_path or str(Path(__file__).parent.parent / "plugins")
|
|
33
|
+
|
|
34
|
+
def discover_all(self):
|
|
35
|
+
"""Discover both internal and external plugins."""
|
|
36
|
+
self._discover_internal()
|
|
37
|
+
self._discover_external()
|
|
38
|
+
|
|
39
|
+
def _discover_internal(self):
|
|
40
|
+
"""Load plugins from the akita/plugins directory."""
|
|
41
|
+
path = Path(self.internal_path)
|
|
42
|
+
if not path.exists():
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
for loader, module_name, is_pkg in pkgutil.iter_modules([str(path)]):
|
|
46
|
+
full_module_name = f"akita.plugins.{module_name}"
|
|
47
|
+
try:
|
|
48
|
+
module = importlib.import_module(full_module_name)
|
|
49
|
+
self._load_from_module(module)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Error loading internal plugin {module_name}: {e}")
|
|
52
|
+
|
|
53
|
+
def _discover_external(self):
|
|
54
|
+
"""Load plugins registered via entry_points (akitallm.plugins)."""
|
|
55
|
+
try:
|
|
56
|
+
# Python 3.10+
|
|
57
|
+
eps = importlib.metadata.entry_points(group='akitallm.plugins')
|
|
58
|
+
for entry_point in eps:
|
|
59
|
+
try:
|
|
60
|
+
plugin_class = entry_point.load()
|
|
61
|
+
if inspect.isclass(plugin_class) and issubclass(plugin_class, AkitaPlugin):
|
|
62
|
+
instance = plugin_class()
|
|
63
|
+
self.plugins[instance.name] = instance
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"Error loading external plugin {entry_point.name}: {e}")
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def _load_from_module(self, module):
|
|
70
|
+
"""Extract AkitaPlugin classes from a module and instantiate them."""
|
|
71
|
+
for name, obj in inspect.getmembers(module):
|
|
72
|
+
if inspect.isclass(obj) and issubclass(obj, AkitaPlugin) and obj is not AkitaPlugin:
|
|
73
|
+
instance = obj()
|
|
74
|
+
self.plugins[instance.name] = instance
|
|
75
|
+
|
|
76
|
+
def get_all_tools(self) -> List[Dict[str, Any]]:
|
|
77
|
+
"""Collect all tools from all loaded plugins."""
|
|
78
|
+
all_tools = []
|
|
79
|
+
for plugin in self.plugins.values():
|
|
80
|
+
all_tools.extend(plugin.get_tools())
|
|
81
|
+
return all_tools
|