ace-git-copilot 0.1.8__tar.gz → 0.2.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.
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/PKG-INFO +26 -7
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/README.md +23 -6
- ace_git_copilot-0.2.0/ace/__init__.py +1 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/llm_factory.py +49 -1
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/cli.py +67 -9
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/core/config.py +34 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ui/display.py +1 -1
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/pyproject.toml +3 -1
- ace_git_copilot-0.2.0/tests/test_llm_factory.py +173 -0
- ace_git_copilot-0.1.8/ace/__init__.py +0 -1
- ace_git_copilot-0.1.8/tests/test_llm_factory.py +0 -51
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/.env.example +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/.github/workflows/tests.yml +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/.gitignore +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/__main__.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/changelog_generator.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/code_reviewer.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/commit_generator.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/conflict_resolver.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/gitignore_generator.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/history_analyzer.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/intent_parser.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/pr_drafter.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/changelog.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/commit.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/conflict.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/explain.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/ignore.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/intent.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/pr.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/review.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/search.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ai/prompts/undo.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/core/context.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/core/git_ops.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/core/safety.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ui/banner.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ui/dashboard.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ui/prompts.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/ui/themes.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/utils/conflict_parser.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/utils/diff_parser.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/ace/utils/json_utils.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/conftest.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_changelog_generator.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_code_reviewer.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_conflict_resolver.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_diff_trimmer.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_git_ops.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_help.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_history_analyzer.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_ignore.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_intent_parser.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_pr_drafter.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_safety.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_search.py +0 -0
- {ace_git_copilot-0.1.8 → ace_git_copilot-0.2.0}/tests/test_undo.py +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ace-git-copilot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: AI-powered Git copilot — talk to Git in plain English
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: click>=8.0
|
|
7
7
|
Requires-Dist: gitpython>=3.1
|
|
8
|
+
Requires-Dist: langchain-anthropic>=0.1.0
|
|
8
9
|
Requires-Dist: langchain-nvidia-ai-endpoints>=0.3
|
|
9
10
|
Requires-Dist: langchain-ollama>=0.3
|
|
11
|
+
Requires-Dist: langchain-openai>=0.1.0
|
|
10
12
|
Requires-Dist: langchain>=0.3
|
|
11
13
|
Requires-Dist: python-dotenv>=1.0
|
|
12
14
|
Requires-Dist: rich>=13.0
|
|
@@ -94,16 +96,33 @@ Run the built-in configuration wizard to select your AI model provider:
|
|
|
94
96
|
```bash
|
|
95
97
|
ace setup
|
|
96
98
|
```
|
|
97
|
-
Ace saves your configuration file to `~/.ace/config.toml`. It supports:
|
|
99
|
+
Ace saves your configuration file to `~/.ace/config.toml`. It supports the following AI model providers:
|
|
98
100
|
|
|
99
|
-
### 1.
|
|
100
|
-
Uses cloud-hosted high-performance models.
|
|
101
|
-
*
|
|
101
|
+
### 1. NVIDIA NIM API (Cloud)
|
|
102
|
+
Uses cloud-hosted high-performance models.
|
|
103
|
+
* You will need an NVIDIA developer API key. Get one for free at [NVIDIA build](https://build.nvidia.com/).
|
|
104
|
+
* Default model: `meta/llama-3.3-70b-instruct`
|
|
102
105
|
|
|
103
|
-
### 2. Local Models
|
|
106
|
+
### 2. Ollama (Local Models)
|
|
104
107
|
For a 100% private, offline, and free experience.
|
|
105
108
|
* Ensure [Ollama](https://ollama.com/) is installed and running on your system.
|
|
106
|
-
*
|
|
109
|
+
* Default model: `qwen2.5-coder:7b`
|
|
110
|
+
* If the selected model is not downloaded yet, Ace will automatically pull it for you during setup.
|
|
111
|
+
|
|
112
|
+
### 3. OpenAI
|
|
113
|
+
Uses OpenAI GPT models.
|
|
114
|
+
* You will need an OpenAI API key.
|
|
115
|
+
* Default model: `gpt-4o-mini`
|
|
116
|
+
|
|
117
|
+
### 4. Anthropic
|
|
118
|
+
Uses Anthropic Claude models.
|
|
119
|
+
* You will need an Anthropic API key.
|
|
120
|
+
* Default model: `claude-3-5-sonnet-latest`
|
|
121
|
+
|
|
122
|
+
### 5. Custom OpenAI-Compatible
|
|
123
|
+
Allows using any custom endpoint that supports the OpenAI API schema (e.g. Groq, OpenRouter, Together AI).
|
|
124
|
+
* You will need the provider's API key and custom API base URL (e.g. `https://api.groq.com/openai/v1`).
|
|
125
|
+
* Default model: `custom-model`
|
|
107
126
|
|
|
108
127
|
---
|
|
109
128
|
|
|
@@ -74,16 +74,33 @@ Run the built-in configuration wizard to select your AI model provider:
|
|
|
74
74
|
```bash
|
|
75
75
|
ace setup
|
|
76
76
|
```
|
|
77
|
-
Ace saves your configuration file to `~/.ace/config.toml`. It supports:
|
|
77
|
+
Ace saves your configuration file to `~/.ace/config.toml`. It supports the following AI model providers:
|
|
78
78
|
|
|
79
|
-
### 1.
|
|
80
|
-
Uses cloud-hosted high-performance models.
|
|
81
|
-
*
|
|
79
|
+
### 1. NVIDIA NIM API (Cloud)
|
|
80
|
+
Uses cloud-hosted high-performance models.
|
|
81
|
+
* You will need an NVIDIA developer API key. Get one for free at [NVIDIA build](https://build.nvidia.com/).
|
|
82
|
+
* Default model: `meta/llama-3.3-70b-instruct`
|
|
82
83
|
|
|
83
|
-
### 2. Local Models
|
|
84
|
+
### 2. Ollama (Local Models)
|
|
84
85
|
For a 100% private, offline, and free experience.
|
|
85
86
|
* Ensure [Ollama](https://ollama.com/) is installed and running on your system.
|
|
86
|
-
*
|
|
87
|
+
* Default model: `qwen2.5-coder:7b`
|
|
88
|
+
* If the selected model is not downloaded yet, Ace will automatically pull it for you during setup.
|
|
89
|
+
|
|
90
|
+
### 3. OpenAI
|
|
91
|
+
Uses OpenAI GPT models.
|
|
92
|
+
* You will need an OpenAI API key.
|
|
93
|
+
* Default model: `gpt-4o-mini`
|
|
94
|
+
|
|
95
|
+
### 4. Anthropic
|
|
96
|
+
Uses Anthropic Claude models.
|
|
97
|
+
* You will need an Anthropic API key.
|
|
98
|
+
* Default model: `claude-3-5-sonnet-latest`
|
|
99
|
+
|
|
100
|
+
### 5. Custom OpenAI-Compatible
|
|
101
|
+
Allows using any custom endpoint that supports the OpenAI API schema (e.g. Groq, OpenRouter, Together AI).
|
|
102
|
+
* You will need the provider's API key and custom API base URL (e.g. `https://api.groq.com/openai/v1`).
|
|
103
|
+
* Default model: `custom-model`
|
|
87
104
|
|
|
88
105
|
---
|
|
89
106
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -110,7 +110,55 @@ def get_llm(offline_override: bool = False) -> BaseChatModel:
|
|
|
110
110
|
num_predict=2048,
|
|
111
111
|
)
|
|
112
112
|
|
|
113
|
+
elif provider == "openai":
|
|
114
|
+
api_key = config.ai.openai_api_key or os.getenv("OPENAI_API_KEY")
|
|
115
|
+
if not api_key:
|
|
116
|
+
raise LLMConfigurationError(
|
|
117
|
+
"OpenAI API Key not found. Please set the OPENAI_API_KEY environment variable "
|
|
118
|
+
"or configure it using 'ace setup'."
|
|
119
|
+
)
|
|
120
|
+
from langchain_openai import ChatOpenAI
|
|
121
|
+
return ChatOpenAI(
|
|
122
|
+
model=config.ai.openai_model or "gpt-4o-mini",
|
|
123
|
+
api_key=api_key,
|
|
124
|
+
temperature=0.0,
|
|
125
|
+
max_tokens=2048,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
elif provider == "anthropic":
|
|
129
|
+
api_key = config.ai.anthropic_api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
130
|
+
if not api_key:
|
|
131
|
+
raise LLMConfigurationError(
|
|
132
|
+
"Anthropic API Key not found. Please set the ANTHROPIC_API_KEY environment variable "
|
|
133
|
+
"or configure it using 'ace setup'."
|
|
134
|
+
)
|
|
135
|
+
from langchain_anthropic import ChatAnthropic
|
|
136
|
+
return ChatAnthropic(
|
|
137
|
+
model=config.ai.anthropic_model or "claude-3-5-sonnet-latest",
|
|
138
|
+
api_key=api_key,
|
|
139
|
+
temperature=0.0,
|
|
140
|
+
max_tokens=2048,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
elif provider == "custom":
|
|
144
|
+
api_key = config.ai.custom_api_key or os.getenv("CUSTOM_API_KEY")
|
|
145
|
+
base_url = config.ai.custom_api_base or os.getenv("CUSTOM_API_BASE")
|
|
146
|
+
model_name = config.ai.custom_model or os.getenv("CUSTOM_MODEL")
|
|
147
|
+
if not base_url:
|
|
148
|
+
raise LLMConfigurationError(
|
|
149
|
+
"Custom API Base URL not found. Please set the CUSTOM_API_BASE environment variable "
|
|
150
|
+
"or configure it using 'ace setup'."
|
|
151
|
+
)
|
|
152
|
+
from langchain_openai import ChatOpenAI
|
|
153
|
+
return ChatOpenAI(
|
|
154
|
+
model=model_name or "custom-model",
|
|
155
|
+
api_key=api_key or "no-key",
|
|
156
|
+
base_url=base_url,
|
|
157
|
+
temperature=0.0,
|
|
158
|
+
max_tokens=2048,
|
|
159
|
+
)
|
|
160
|
+
|
|
113
161
|
else:
|
|
114
162
|
raise LLMConfigurationError(
|
|
115
|
-
f"Unsupported AI provider: '{provider}'. Supported providers are
|
|
163
|
+
f"Unsupported AI provider: '{provider}'. Supported providers are: nvidia, ollama, openai, anthropic, custom."
|
|
116
164
|
)
|
|
@@ -404,18 +404,42 @@ def setup_cmd():
|
|
|
404
404
|
pass
|
|
405
405
|
|
|
406
406
|
console.print("[bold orange3]Welcome to Ace AI Git Copilot Setup![/bold orange3] 🚀\n")
|
|
407
|
+
console.print("Configure your preferences and AI provider step-by-step.\n")
|
|
407
408
|
|
|
408
409
|
config = get_config()
|
|
409
410
|
|
|
410
|
-
|
|
411
411
|
# Provider select
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
console.print("[bold]Select your AI Provider:[/bold]")
|
|
413
|
+
console.print(" [bold cyan]1[/bold cyan] -> NVIDIA API Endpoints (Cloud)")
|
|
414
|
+
console.print(" [bold cyan]2[/bold cyan] -> Ollama (Local Models)")
|
|
415
|
+
console.print(" [bold cyan]3[/bold cyan] -> OpenAI (GPT-4o, etc.)")
|
|
416
|
+
console.print(" [bold cyan]4[/bold cyan] -> Anthropic (Claude)")
|
|
417
|
+
console.print(" [bold cyan]5[/bold cyan] -> Custom OpenAI-Compatible (Groq, OpenRouter, etc.)")
|
|
418
|
+
console.print("")
|
|
419
|
+
|
|
420
|
+
provider_map = {
|
|
421
|
+
"1": "nvidia",
|
|
422
|
+
"2": "ollama",
|
|
423
|
+
"3": "openai",
|
|
424
|
+
"4": "anthropic",
|
|
425
|
+
"5": "custom",
|
|
426
|
+
}
|
|
427
|
+
provider_reverse_map = {v: k for k, v in provider_map.items()}
|
|
428
|
+
default_choice = provider_reverse_map.get(config.ai.provider, "1")
|
|
429
|
+
|
|
430
|
+
choice = typer.prompt("Enter choice (1-5)", default=default_choice)
|
|
431
|
+
choice_clean = choice.strip().lower()
|
|
432
|
+
|
|
433
|
+
if choice_clean in provider_map:
|
|
434
|
+
provider = provider_map[choice_clean]
|
|
435
|
+
elif choice_clean in provider_reverse_map:
|
|
436
|
+
provider = choice_clean
|
|
437
|
+
else:
|
|
438
|
+
print_warning("Invalid choice. Defaulting to NVIDIA.")
|
|
416
439
|
provider = "nvidia"
|
|
417
440
|
|
|
418
441
|
config.ai.provider = provider
|
|
442
|
+
console.print(f"Selected Provider: [bold cyan]{provider.upper()}[/bold cyan]\n")
|
|
419
443
|
|
|
420
444
|
# NVIDIA setup
|
|
421
445
|
if provider == "nvidia":
|
|
@@ -430,7 +454,31 @@ def setup_cmd():
|
|
|
430
454
|
config.ai.ollama_url = ollama_url
|
|
431
455
|
ollama_model = typer.prompt("Ollama model name", default=config.ai.ollama_model)
|
|
432
456
|
config.ai.ollama_model = ollama_model
|
|
457
|
+
|
|
458
|
+
# OpenAI setup
|
|
459
|
+
elif provider == "openai":
|
|
460
|
+
openai_key = typer.prompt("Enter your OpenAI API Key", default=config.ai.openai_api_key, hide_input=True)
|
|
461
|
+
config.ai.openai_api_key = openai_key
|
|
462
|
+
openai_model = typer.prompt("OpenAI LLM Model name", default=config.ai.openai_model)
|
|
463
|
+
config.ai.openai_model = openai_model
|
|
464
|
+
|
|
465
|
+
# Anthropic setup
|
|
466
|
+
elif provider == "anthropic":
|
|
467
|
+
anthropic_key = typer.prompt("Enter your Anthropic API Key", default=config.ai.anthropic_api_key, hide_input=True)
|
|
468
|
+
config.ai.anthropic_api_key = anthropic_key
|
|
469
|
+
anthropic_model = typer.prompt("Anthropic LLM Model name", default=config.ai.anthropic_model)
|
|
470
|
+
config.ai.anthropic_model = anthropic_model
|
|
471
|
+
|
|
472
|
+
# Custom setup
|
|
473
|
+
elif provider == "custom":
|
|
474
|
+
custom_base = typer.prompt("Custom API Base URL (e.g., https://api.groq.com/openai/v1)", default=config.ai.custom_api_base)
|
|
475
|
+
config.ai.custom_api_base = custom_base
|
|
476
|
+
custom_key = typer.prompt("Enter your Custom API Key", default=config.ai.custom_api_key, hide_input=True)
|
|
477
|
+
config.ai.custom_api_key = custom_key
|
|
478
|
+
custom_model = typer.prompt("Custom LLM Model name", default=config.ai.custom_model)
|
|
479
|
+
config.ai.custom_model = custom_model
|
|
433
480
|
|
|
481
|
+
console.print("\n[bold]Configure Commit Preferences:[/bold]")
|
|
434
482
|
# Commit pref setup
|
|
435
483
|
commit_format = typer.prompt("Default commit format (conventional, simple, detailed)", default=config.commit.format)
|
|
436
484
|
if commit_format.lower().strip() in ("conventional", "simple", "detailed"):
|
|
@@ -438,6 +486,9 @@ def setup_cmd():
|
|
|
438
486
|
|
|
439
487
|
sign_commits = confirm("Should Ace sign commits by default (GPG/SSH)?", default=config.commit.sign)
|
|
440
488
|
config.commit.sign = sign_commits
|
|
489
|
+
|
|
490
|
+
use_emoji = confirm("Should Ace use emojis in commit messages by default?", default=config.commit.emoji)
|
|
491
|
+
config.commit.emoji = use_emoji
|
|
441
492
|
|
|
442
493
|
# Save config
|
|
443
494
|
try:
|
|
@@ -455,16 +506,23 @@ def config_cmd():
|
|
|
455
506
|
table.add_column("Setting")
|
|
456
507
|
table.add_column("Value")
|
|
457
508
|
|
|
458
|
-
# Mask API key
|
|
459
|
-
|
|
460
|
-
|
|
509
|
+
# Mask API key helper
|
|
510
|
+
def mask_key(k: str) -> str:
|
|
511
|
+
return k[:8] + "..." if k else "Not set"
|
|
461
512
|
|
|
462
513
|
# Add items
|
|
463
514
|
table.add_row("AI", "Provider", config.ai.provider)
|
|
464
|
-
table.add_row("AI", "NVIDIA API Key",
|
|
515
|
+
table.add_row("AI", "NVIDIA API Key", mask_key(config.ai.nvidia_api_key))
|
|
465
516
|
table.add_row("AI", "NVIDIA Model", config.ai.nvidia_model)
|
|
466
517
|
table.add_row("AI", "Ollama URL", config.ai.ollama_url)
|
|
467
518
|
table.add_row("AI", "Ollama Model", config.ai.ollama_model)
|
|
519
|
+
table.add_row("AI", "OpenAI API Key", mask_key(config.ai.openai_api_key))
|
|
520
|
+
table.add_row("AI", "OpenAI Model", config.ai.openai_model)
|
|
521
|
+
table.add_row("AI", "Anthropic API Key", mask_key(config.ai.anthropic_api_key))
|
|
522
|
+
table.add_row("AI", "Anthropic Model", config.ai.anthropic_model)
|
|
523
|
+
table.add_row("AI", "Custom API Base URL", config.ai.custom_api_base or "Not set")
|
|
524
|
+
table.add_row("AI", "Custom API Key", mask_key(config.ai.custom_api_key))
|
|
525
|
+
table.add_row("AI", "Custom Model", config.ai.custom_model or "Not set")
|
|
468
526
|
|
|
469
527
|
table.add_row("Commit", "Default Format", config.commit.format)
|
|
470
528
|
table.add_row("Commit", "Sign Commits", str(config.commit.sign))
|
|
@@ -13,6 +13,13 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
13
13
|
"nvidia_model": "meta/llama-3.3-70b-instruct",
|
|
14
14
|
"ollama_model": "qwen2.5-coder:7b",
|
|
15
15
|
"ollama_url": "http://localhost:11434",
|
|
16
|
+
"openai_api_key": "",
|
|
17
|
+
"openai_model": "gpt-4o-mini",
|
|
18
|
+
"anthropic_api_key": "",
|
|
19
|
+
"anthropic_model": "claude-3-5-sonnet-latest",
|
|
20
|
+
"custom_api_key": "",
|
|
21
|
+
"custom_api_base": "",
|
|
22
|
+
"custom_model": "",
|
|
16
23
|
},
|
|
17
24
|
"commit": {
|
|
18
25
|
"format": "conventional",
|
|
@@ -117,6 +124,33 @@ def get_config() -> Config:
|
|
|
117
124
|
if env_ollama_url:
|
|
118
125
|
data["ai"]["ollama_url"] = env_ollama_url
|
|
119
126
|
|
|
127
|
+
# OpenAI API Key & Model
|
|
128
|
+
env_openai_key = os.getenv("OPENAI_API_KEY")
|
|
129
|
+
if env_openai_key:
|
|
130
|
+
data["ai"]["openai_api_key"] = env_openai_key
|
|
131
|
+
env_openai_model = os.getenv("OPENAI_MODEL")
|
|
132
|
+
if env_openai_model:
|
|
133
|
+
data["ai"]["openai_model"] = env_openai_model
|
|
134
|
+
|
|
135
|
+
# Anthropic API Key & Model
|
|
136
|
+
env_anthropic_key = os.getenv("ANTHROPIC_API_KEY")
|
|
137
|
+
if env_anthropic_key:
|
|
138
|
+
data["ai"]["anthropic_api_key"] = env_anthropic_key
|
|
139
|
+
env_anthropic_model = os.getenv("ANTHROPIC_MODEL")
|
|
140
|
+
if env_anthropic_model:
|
|
141
|
+
data["ai"]["anthropic_model"] = env_anthropic_model
|
|
142
|
+
|
|
143
|
+
# Custom API Key, Base, & Model
|
|
144
|
+
env_custom_key = os.getenv("CUSTOM_API_KEY")
|
|
145
|
+
if env_custom_key:
|
|
146
|
+
data["ai"]["custom_api_key"] = env_custom_key
|
|
147
|
+
env_custom_base = os.getenv("CUSTOM_API_BASE")
|
|
148
|
+
if env_custom_base:
|
|
149
|
+
data["ai"]["custom_api_base"] = env_custom_base
|
|
150
|
+
env_custom_model = os.getenv("CUSTOM_MODEL")
|
|
151
|
+
if env_custom_model:
|
|
152
|
+
data["ai"]["custom_model"] = env_custom_model
|
|
153
|
+
|
|
120
154
|
return Config(data)
|
|
121
155
|
|
|
122
156
|
def save_config(config: Config) -> None:
|
|
@@ -60,7 +60,7 @@ def show_plan(commands: List[str], explanations: List[str]) -> None:
|
|
|
60
60
|
for i, (cmd, exp) in enumerate(zip(commands, explanations), 1):
|
|
61
61
|
content.append(f"\n {i}. ", style="ai")
|
|
62
62
|
content.append(f"{cmd}\n", style="bold white")
|
|
63
|
-
content.append(f"
|
|
63
|
+
content.append(f" -> {exp}\n", style="dim italic")
|
|
64
64
|
|
|
65
65
|
panel = Panel(
|
|
66
66
|
content,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ace-git-copilot"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "AI-powered Git copilot — talk to Git in plain English"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -12,6 +12,8 @@ dependencies = [
|
|
|
12
12
|
"langchain>=0.3",
|
|
13
13
|
"langchain-nvidia-ai-endpoints>=0.3",
|
|
14
14
|
"langchain-ollama>=0.3",
|
|
15
|
+
"langchain-openai>=0.1.0",
|
|
16
|
+
"langchain-anthropic>=0.1.0",
|
|
15
17
|
"python-dotenv>=1.0",
|
|
16
18
|
"toml>=0.10.2",
|
|
17
19
|
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
from ace.ai.llm_factory import ensure_ollama_model, _checked_ollama_models, get_llm
|
|
4
|
+
|
|
5
|
+
def test_ensure_ollama_model_cached():
|
|
6
|
+
# Setup cache key
|
|
7
|
+
_checked_ollama_models.add(("http://localhost:11434", "test-model"))
|
|
8
|
+
|
|
9
|
+
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
10
|
+
ensure_ollama_model("http://localhost:11434", "test-model")
|
|
11
|
+
mock_urlopen.assert_not_called()
|
|
12
|
+
|
|
13
|
+
@patch("urllib.request.urlopen")
|
|
14
|
+
def test_ensure_ollama_model_exists(mock_urlopen):
|
|
15
|
+
# Reset cache
|
|
16
|
+
_checked_ollama_models.clear()
|
|
17
|
+
|
|
18
|
+
# Mock tags response
|
|
19
|
+
mock_resp = MagicMock()
|
|
20
|
+
mock_resp.read.return_value = json.dumps({
|
|
21
|
+
"models": [{"name": "llama3:latest"}]
|
|
22
|
+
}).encode("utf-8")
|
|
23
|
+
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
24
|
+
|
|
25
|
+
ensure_ollama_model("http://localhost:11434", "llama3")
|
|
26
|
+
assert ("http://localhost:11434", "llama3") in _checked_ollama_models
|
|
27
|
+
|
|
28
|
+
@patch("urllib.request.urlopen")
|
|
29
|
+
@patch("ace.ui.prompts.confirm", return_value=True)
|
|
30
|
+
def test_ensure_ollama_model_pull(mock_confirm, mock_urlopen):
|
|
31
|
+
_checked_ollama_models.clear()
|
|
32
|
+
|
|
33
|
+
# Mock tags response (not exists) and then pull response
|
|
34
|
+
mock_tags_resp = MagicMock()
|
|
35
|
+
mock_tags_resp.read.return_value = json.dumps({
|
|
36
|
+
"models": [{"name": "other-model:latest"}]
|
|
37
|
+
}).encode("utf-8")
|
|
38
|
+
|
|
39
|
+
mock_pull_resp = MagicMock()
|
|
40
|
+
mock_pull_resp.read.return_value = json.dumps({
|
|
41
|
+
"status": "success"
|
|
42
|
+
}).encode("utf-8")
|
|
43
|
+
|
|
44
|
+
# urlopen will be called twice: first for tags, second for pull
|
|
45
|
+
mock_urlopen.return_value.__enter__.side_effect = [mock_tags_resp, mock_pull_resp]
|
|
46
|
+
|
|
47
|
+
ensure_ollama_model("http://localhost:11434", "llama3")
|
|
48
|
+
|
|
49
|
+
# Check both calls
|
|
50
|
+
assert mock_urlopen.call_count == 2
|
|
51
|
+
assert mock_confirm.called
|
|
52
|
+
|
|
53
|
+
def test_get_llm_nvidia():
|
|
54
|
+
import pytest
|
|
55
|
+
from ace.core.config import Config
|
|
56
|
+
mock_config = Config({
|
|
57
|
+
"ai": {
|
|
58
|
+
"provider": "nvidia",
|
|
59
|
+
"nvidia_api_key": "nv-api-key-test",
|
|
60
|
+
"nvidia_model": "meta/llama-3.3-70b-instruct",
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config), \
|
|
64
|
+
patch("langchain_nvidia_ai_endpoints.ChatNVIDIA") as mock_chat:
|
|
65
|
+
get_llm()
|
|
66
|
+
mock_chat.assert_called_once_with(
|
|
67
|
+
model="meta/llama-3.3-70b-instruct",
|
|
68
|
+
api_key="nv-api-key-test",
|
|
69
|
+
base_url="https://integrate.api.nvidia.com/v1",
|
|
70
|
+
temperature=0.0,
|
|
71
|
+
max_tokens=2048,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def test_get_llm_openai():
|
|
75
|
+
from ace.core.config import Config
|
|
76
|
+
mock_config = Config({
|
|
77
|
+
"ai": {
|
|
78
|
+
"provider": "openai",
|
|
79
|
+
"openai_api_key": "openai-key-test",
|
|
80
|
+
"openai_model": "gpt-4o-mini",
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config), \
|
|
84
|
+
patch("langchain_openai.ChatOpenAI") as mock_chat:
|
|
85
|
+
get_llm()
|
|
86
|
+
mock_chat.assert_called_once_with(
|
|
87
|
+
model="gpt-4o-mini",
|
|
88
|
+
api_key="openai-key-test",
|
|
89
|
+
temperature=0.0,
|
|
90
|
+
max_tokens=2048,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def test_get_llm_anthropic():
|
|
94
|
+
from ace.core.config import Config
|
|
95
|
+
mock_config = Config({
|
|
96
|
+
"ai": {
|
|
97
|
+
"provider": "anthropic",
|
|
98
|
+
"anthropic_api_key": "anthropic-key-test",
|
|
99
|
+
"anthropic_model": "claude-3-5-sonnet-latest",
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config), \
|
|
103
|
+
patch("langchain_anthropic.ChatAnthropic") as mock_chat:
|
|
104
|
+
get_llm()
|
|
105
|
+
mock_chat.assert_called_once_with(
|
|
106
|
+
model="claude-3-5-sonnet-latest",
|
|
107
|
+
api_key="anthropic-key-test",
|
|
108
|
+
temperature=0.0,
|
|
109
|
+
max_tokens=2048,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def test_get_llm_custom():
|
|
113
|
+
from ace.core.config import Config
|
|
114
|
+
mock_config = Config({
|
|
115
|
+
"ai": {
|
|
116
|
+
"provider": "custom",
|
|
117
|
+
"custom_api_key": "custom-key-test",
|
|
118
|
+
"custom_api_base": "https://custom-url.com/v1",
|
|
119
|
+
"custom_model": "my-custom-model",
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config), \
|
|
123
|
+
patch("langchain_openai.ChatOpenAI") as mock_chat:
|
|
124
|
+
get_llm()
|
|
125
|
+
mock_chat.assert_called_once_with(
|
|
126
|
+
model="my-custom-model",
|
|
127
|
+
api_key="custom-key-test",
|
|
128
|
+
base_url="https://custom-url.com/v1",
|
|
129
|
+
temperature=0.0,
|
|
130
|
+
max_tokens=2048,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def test_get_llm_missing_keys():
|
|
134
|
+
import pytest
|
|
135
|
+
from ace.core.config import Config
|
|
136
|
+
from ace.ai.llm_factory import LLMConfigurationError
|
|
137
|
+
|
|
138
|
+
# Test openai missing key
|
|
139
|
+
mock_config = Config({
|
|
140
|
+
"ai": {
|
|
141
|
+
"provider": "openai",
|
|
142
|
+
"openai_api_key": "",
|
|
143
|
+
"openai_model": "gpt-4o-mini",
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config):
|
|
147
|
+
with pytest.raises(LLMConfigurationError, match="OpenAI API Key not found"):
|
|
148
|
+
get_llm()
|
|
149
|
+
|
|
150
|
+
# Test anthropic missing key
|
|
151
|
+
mock_config = Config({
|
|
152
|
+
"ai": {
|
|
153
|
+
"provider": "anthropic",
|
|
154
|
+
"anthropic_api_key": "",
|
|
155
|
+
"anthropic_model": "claude-3-5-sonnet-latest",
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config):
|
|
159
|
+
with pytest.raises(LLMConfigurationError, match="Anthropic API Key not found"):
|
|
160
|
+
get_llm()
|
|
161
|
+
|
|
162
|
+
# Test custom missing base url
|
|
163
|
+
mock_config = Config({
|
|
164
|
+
"ai": {
|
|
165
|
+
"provider": "custom",
|
|
166
|
+
"custom_api_key": "some-key",
|
|
167
|
+
"custom_api_base": "",
|
|
168
|
+
"custom_model": "custom-model",
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
with patch("ace.ai.llm_factory.get_config", return_value=mock_config):
|
|
172
|
+
with pytest.raises(LLMConfigurationError, match="Custom API Base URL not found"):
|
|
173
|
+
get_llm()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from unittest.mock import MagicMock, patch
|
|
3
|
-
from ace.ai.llm_factory import ensure_ollama_model, _checked_ollama_models
|
|
4
|
-
|
|
5
|
-
def test_ensure_ollama_model_cached():
|
|
6
|
-
# Setup cache key
|
|
7
|
-
_checked_ollama_models.add(("http://localhost:11434", "test-model"))
|
|
8
|
-
|
|
9
|
-
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
10
|
-
ensure_ollama_model("http://localhost:11434", "test-model")
|
|
11
|
-
mock_urlopen.assert_not_called()
|
|
12
|
-
|
|
13
|
-
@patch("urllib.request.urlopen")
|
|
14
|
-
def test_ensure_ollama_model_exists(mock_urlopen):
|
|
15
|
-
# Reset cache
|
|
16
|
-
_checked_ollama_models.clear()
|
|
17
|
-
|
|
18
|
-
# Mock tags response
|
|
19
|
-
mock_resp = MagicMock()
|
|
20
|
-
mock_resp.read.return_value = json.dumps({
|
|
21
|
-
"models": [{"name": "llama3:latest"}]
|
|
22
|
-
}).encode("utf-8")
|
|
23
|
-
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
24
|
-
|
|
25
|
-
ensure_ollama_model("http://localhost:11434", "llama3")
|
|
26
|
-
assert ("http://localhost:11434", "llama3") in _checked_ollama_models
|
|
27
|
-
|
|
28
|
-
@patch("urllib.request.urlopen")
|
|
29
|
-
@patch("ace.ui.prompts.confirm", return_value=True)
|
|
30
|
-
def test_ensure_ollama_model_pull(mock_confirm, mock_urlopen):
|
|
31
|
-
_checked_ollama_models.clear()
|
|
32
|
-
|
|
33
|
-
# Mock tags response (not exists) and then pull response
|
|
34
|
-
mock_tags_resp = MagicMock()
|
|
35
|
-
mock_tags_resp.read.return_value = json.dumps({
|
|
36
|
-
"models": [{"name": "other-model:latest"}]
|
|
37
|
-
}).encode("utf-8")
|
|
38
|
-
|
|
39
|
-
mock_pull_resp = MagicMock()
|
|
40
|
-
mock_pull_resp.read.return_value = json.dumps({
|
|
41
|
-
"status": "success"
|
|
42
|
-
}).encode("utf-8")
|
|
43
|
-
|
|
44
|
-
# urlopen will be called twice: first for tags, second for pull
|
|
45
|
-
mock_urlopen.return_value.__enter__.side_effect = [mock_tags_resp, mock_pull_resp]
|
|
46
|
-
|
|
47
|
-
ensure_ollama_model("http://localhost:11434", "llama3")
|
|
48
|
-
|
|
49
|
-
# Check both calls
|
|
50
|
-
assert mock_urlopen.call_count == 2
|
|
51
|
-
assert mock_confirm.called
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|