ace-git-copilot 0.1.9__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.
Files changed (57) hide show
  1. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/PKG-INFO +26 -7
  2. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/README.md +23 -6
  3. ace_git_copilot-0.2.0/ace/__init__.py +1 -0
  4. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/llm_factory.py +49 -1
  5. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/cli.py +67 -9
  6. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/core/config.py +34 -0
  7. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/pyproject.toml +3 -1
  8. ace_git_copilot-0.2.0/tests/test_llm_factory.py +173 -0
  9. ace_git_copilot-0.1.9/ace/__init__.py +0 -1
  10. ace_git_copilot-0.1.9/tests/test_llm_factory.py +0 -51
  11. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/.env.example +0 -0
  12. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/.github/workflows/tests.yml +0 -0
  13. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/.gitignore +0 -0
  14. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/__main__.py +0 -0
  15. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/changelog_generator.py +0 -0
  16. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/code_reviewer.py +0 -0
  17. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/commit_generator.py +0 -0
  18. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/conflict_resolver.py +0 -0
  19. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/gitignore_generator.py +0 -0
  20. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/history_analyzer.py +0 -0
  21. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/intent_parser.py +0 -0
  22. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/pr_drafter.py +0 -0
  23. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/changelog.py +0 -0
  24. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/commit.py +0 -0
  25. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/conflict.py +0 -0
  26. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/explain.py +0 -0
  27. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/ignore.py +0 -0
  28. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/intent.py +0 -0
  29. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/pr.py +0 -0
  30. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/review.py +0 -0
  31. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/search.py +0 -0
  32. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ai/prompts/undo.py +0 -0
  33. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/core/context.py +0 -0
  34. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/core/git_ops.py +0 -0
  35. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/core/safety.py +0 -0
  36. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ui/banner.py +0 -0
  37. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ui/dashboard.py +0 -0
  38. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ui/display.py +0 -0
  39. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ui/prompts.py +0 -0
  40. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/ui/themes.py +0 -0
  41. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/utils/conflict_parser.py +0 -0
  42. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/utils/diff_parser.py +0 -0
  43. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/ace/utils/json_utils.py +0 -0
  44. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/conftest.py +0 -0
  45. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_changelog_generator.py +0 -0
  46. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_code_reviewer.py +0 -0
  47. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_conflict_resolver.py +0 -0
  48. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_diff_trimmer.py +0 -0
  49. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_git_ops.py +0 -0
  50. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_help.py +0 -0
  51. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_history_analyzer.py +0 -0
  52. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_ignore.py +0 -0
  53. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_intent_parser.py +0 -0
  54. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_pr_drafter.py +0 -0
  55. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_safety.py +0 -0
  56. {ace_git_copilot-0.1.9 → ace_git_copilot-0.2.0}/tests/test_search.py +0 -0
  57. {ace_git_copilot-0.1.9 → 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.1.9
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. Cloud Models (NVIDIA NIM API)
100
- Uses cloud-hosted high-performance models.
101
- * To use this, you will need an NVIDIA developer API key. Get one for free at [NVIDIA build](https://build.nvidia.com/).
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 (Ollama)
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
- * You can select models like `qwen2.5-coder`, `llama3.1`, or `mistral`. If the selected model is not downloaded yet, Ace will automatically pull it for you during setup.
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. Cloud Models (NVIDIA NIM API)
80
- Uses cloud-hosted high-performance models.
81
- * To use this, you will need an NVIDIA developer API key. Get one for free at [NVIDIA build](https://build.nvidia.com/).
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 (Ollama)
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
- * You can select models like `qwen2.5-coder`, `llama3.1`, or `mistral`. If the selected model is not downloaded yet, Ace will automatically pull it for you during setup.
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 'nvidia' and 'ollama'."
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
- provider = typer.prompt("Select AI Provider (nvidia or ollama)", default=config.ai.provider)
413
- provider = provider.lower().strip()
414
- if provider not in ("nvidia", "ollama"):
415
- print_error("Invalid provider. Defaulting to 'nvidia'.")
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
- nv_key = config.ai.nvidia_api_key
460
- masked_key = nv_key[:8] + "..." if nv_key else "Not set"
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", masked_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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ace-git-copilot"
3
- version = "0.1.9"
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