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.
Files changed (39) hide show
  1. {akitallm-0.1.1 → akitallm-1.1.0}/PKG-INFO +8 -11
  2. {akitallm-0.1.1 → akitallm-1.1.0}/README.md +4 -10
  3. akitallm-1.1.0/akita/__init__.py +1 -0
  4. akitallm-1.1.0/akita/cli/main.py +346 -0
  5. akitallm-1.1.0/akita/core/ast_utils.py +77 -0
  6. {akitallm-0.1.1 → akitallm-1.1.0}/akita/core/config.py +12 -2
  7. akitallm-1.1.0/akita/core/indexing.py +94 -0
  8. akitallm-1.1.0/akita/core/plugins.py +81 -0
  9. akitallm-1.1.0/akita/core/providers.py +181 -0
  10. akitallm-1.1.0/akita/core/trace.py +18 -0
  11. {akitallm-0.1.1 → akitallm-1.1.0}/akita/models/base.py +12 -7
  12. akitallm-1.1.0/akita/plugins/__init__.py +1 -0
  13. akitallm-1.1.0/akita/plugins/files.py +34 -0
  14. {akitallm-0.1.1 → akitallm-1.1.0}/akita/reasoning/engine.py +44 -18
  15. akitallm-1.1.0/akita/reasoning/session.py +15 -0
  16. {akitallm-0.1.1 → akitallm-1.1.0}/akita/tools/base.py +6 -1
  17. {akitallm-0.1.1 → akitallm-1.1.0}/akita/tools/context.py +54 -9
  18. akitallm-1.1.0/akita/tools/diff.py +116 -0
  19. akitallm-1.1.0/akita/tools/git.py +79 -0
  20. {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/PKG-INFO +8 -11
  21. {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/SOURCES.txt +13 -0
  22. akitallm-1.1.0/akitallm.egg-info/entry_points.txt +5 -0
  23. {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/requires.txt +3 -0
  24. {akitallm-0.1.1 → akitallm-1.1.0}/pyproject.toml +7 -1
  25. akitallm-1.1.0/tests/test_ast.py +50 -0
  26. {akitallm-0.1.1 → akitallm-1.1.0}/tests/test_basic.py +1 -1
  27. akitallm-1.1.0/tests/test_diff.py +68 -0
  28. akitallm-1.1.0/tests/test_interactive.py +33 -0
  29. akitallm-1.1.0/tests/test_plugins.py +41 -0
  30. akitallm-0.1.1/akita/__init__.py +0 -1
  31. akitallm-0.1.1/akita/cli/main.py +0 -217
  32. akitallm-0.1.1/akita/tools/diff.py +0 -41
  33. akitallm-0.1.1/akitallm.egg-info/entry_points.txt +0 -2
  34. {akitallm-0.1.1 → akitallm-1.1.0}/LICENSE +0 -0
  35. {akitallm-0.1.1 → akitallm-1.1.0}/akita/schemas/review.py +0 -0
  36. {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/dependency_links.txt +0 -0
  37. {akitallm-0.1.1 → akitallm-1.1.0}/akitallm.egg-info/top_level.txt +0 -0
  38. {akitallm-0.1.1 → akitallm-1.1.0}/setup.cfg +0 -0
  39. {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: 0.1.1
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
- ## Contributing
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
- ## Contributing
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
- return config.get(section, {}).get(key, default)
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