Refactor GitHub automation tool:

- Updated WorkItemFieldExtractor to be more generic and removed Azure DevOps specific references.
- Removed the EnhancedContentBuilders class as it was specific to Azure DevOps.
- Deleted work_item_processor.py as it was no longer needed.
- Introduced workflow.py to manage GitHub workflow items (issues and pull requests) with improved structure and functionality.
- Enhanced logging and error handling across the new workflow management system.
This commit is contained in:
b-tsammmons
2025-11-11 22:46:02 -10:00
parent d6da461d10
commit 0f41a3e750
15 changed files with 2003 additions and 1446 deletions
+210 -11
View File
@@ -2709,7 +2709,7 @@ class LocalGitManager:
self.logger.log(f"❌ Error saving diff to file: {str(e)}")
def create_ai_provider(provider_name: str, api_key: str, logger: Logger) -> Optional[AIProvider]:
def create_ai_provider(provider_name: str, api_key: str, logger: Logger, ollama_url: str = None, ollama_model: str = None) -> Optional[AIProvider]:
"""Factory function to create AI provider instances"""
if provider_name.lower() == 'claude':
return ClaudeProvider(api_key, logger)
@@ -2717,6 +2717,9 @@ def create_ai_provider(provider_name: str, api_key: str, logger: Logger) -> Opti
return ChatGPTProvider(api_key, logger)
elif provider_name.lower() in ['github-copilot', 'copilot', 'github_copilot']:
return GitHubCopilotProvider(api_key, logger)
elif provider_name.lower() == 'ollama':
# For Ollama, api_key is optional (can be empty string)
return OllamaProvider(api_key or "", logger, ollama_url, ollama_model)
else:
logger.log(f"⚠️ Unknown AI provider: {provider_name}")
return None
@@ -2927,6 +2930,198 @@ def validate_ai_provider_setup(config: dict, parent_window=None) -> bool:
return False
class OllamaProvider(AIProvider):
"""Ollama AI provider for self-hosted models"""
def __init__(self, api_key: str, logger: Logger, ollama_url: str = None, model: str = None):
super().__init__(api_key, logger)
self.ollama_url = ollama_url or "http://localhost:11434"
self.model = model or "llama2"
# Normalize URL
if not self.ollama_url.startswith('http'):
self.ollama_url = f"http://{self.ollama_url}"
def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]:
"""Make targeted changes using Ollama"""
# Step 1: Try direct string replacement first
if old_text and old_text.strip() in file_content:
self.logger.log("✅ Making direct string replacement (reference text found exactly)")
updated_content = file_content.replace(old_text.strip(), new_text.strip())
if updated_content != file_content:
original_lines = file_content.split('\n')
updated_lines = updated_content.split('\n')
import difflib
diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm=''))
changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')])
self.logger.log(f"✅ Direct replacement successful ({changed_lines} lines changed)")
return updated_content
# Step 2: Use Ollama to generate full document with targeted changes
self.logger.log(f"📝 Using Ollama ({self.model}) to modify the document...")
return self._generate_updated_document(file_content, old_text, new_text, file_path, custom_instructions)
def _generate_updated_document(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]:
"""Generate updated document content using Ollama"""
try:
import requests
# Build custom instructions text
if custom_instructions and custom_instructions.strip():
custom_instructions_text = f"""
**Additional Custom Instructions:**
{custom_instructions.strip()}
"""
else:
custom_instructions_text = ""
# Handle case where new_text is empty or just guidance
if new_text and new_text.strip() and not new_text.strip().lower().startswith('<blank'):
# We have specific replacement text
guidance_text = f"""
**Reference text to find:**
```
{old_text}
```
**Replace with this specific content:**
```
{new_text}
```
Please find the reference text and replace it with the suggested content."""
else:
# new_text is empty or just guidance - use old_text as instructions
guidance_text = f"""
**Task Instructions:**
{old_text}
**Note:** No specific replacement text provided. Use the task instructions above to determine what changes to make to improve the document. Add appropriate content based on the instructions."""
prompt = f"""**Instructions:**
Task: Update the documentation file with the changes requested.
Steps to complete:
1. Review the current file content below
2. Follow the guidance provided to determine what changes to make
3. Make appropriate improvements while maintaining existing formatting
4. Return the complete updated file content
> [!IMPORTANT]
> OUTPUT REQUIREMENTS:
> - Return ONLY the complete file content - no explanatory text, dialog, or commentary
> - Do NOT add any text before or after the file content
> - Do NOT wrap output in markdown code blocks (```), just return the raw content
> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...]
> - Every single line of the original document must be present in your response
> - Preserve all markdown formatting, links, and code blocks exactly
> - Only make changes that fulfill the specified request
{custom_instructions_text}
**Current File Content:**
```
{file_content}
```
{guidance_text}
Return the complete updated file content now (NO explanatory text):"""
# Prepare request headers
headers = {
"Content-Type": "application/json"
}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
# Prepare request payload
payload = {
"model": self.model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.3, # Lower temperature for more consistent output
"num_predict": -1, # Generate as many tokens as needed
}
}
# Make request to Ollama
self.logger.log(f"🔄 Sending request to Ollama at {self.ollama_url}...")
response = requests.post(
f"{self.ollama_url}/api/generate",
json=payload,
headers=headers,
timeout=300 # 5 minute timeout for large documents
)
response.raise_for_status()
result = response.json()
updated_content = result.get("response", "").strip()
if not updated_content:
self.logger.log("❌ Ollama returned empty response")
return None
# Clean up response
updated_content = self._clean_ai_response(updated_content)
# Validate that we got the full document back
original_line_count = len(file_content.split('\n'))
updated_line_count = len(updated_content.split('\n'))
if updated_line_count < original_line_count * 0.5: # Less than 50% of original lines
self.logger.log(f"⚠️ Warning: Updated document seems truncated ({updated_line_count} vs {original_line_count} lines)")
self.logger.log("❌ AI may have truncated the document - using fallback")
return None
self.logger.log(f"✅ Successfully generated updated document ({updated_line_count} lines)")
return updated_content
except requests.exceptions.ConnectionError:
self.logger.log(f"❌ Could not connect to Ollama server at {self.ollama_url}")
self.logger.log(" Make sure Ollama is running and the URL is correct")
return None
except requests.exceptions.Timeout:
self.logger.log("❌ Request to Ollama server timed out")
return None
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
self.logger.log("❌ Authentication failed - check your Ollama API key")
elif e.response.status_code == 404:
self.logger.log(f"❌ Model '{self.model}' not found on Ollama server")
self.logger.log(f" Use 'ollama pull {self.model}' to download it")
else:
self.logger.log(f"❌ HTTP error from Ollama: {e}")
return None
except Exception as e:
self.logger.log(f"❌ Error calling Ollama: {str(e)}")
import traceback
traceback.print_exc()
return None
def _clean_ai_response(self, response: str) -> str:
"""Clean up AI response by removing markdown code blocks and explanatory text"""
# Remove markdown code blocks if present
if response.startswith('```'):
lines = response.split('\n')
# Remove first line if it's a code fence
if lines[0].startswith('```'):
lines = lines[1:]
# Remove last line if it's a code fence
if lines and lines[-1].strip() == '```':
lines = lines[:-1]
response = '\n'.join(lines)
return response.strip()
# AI Providers availability flag - now always True since they're included
AI_PROVIDERS_AVAILABLE = True
@@ -2952,18 +3147,18 @@ class AIManager:
def check_ai_module_availability(self, provider_name: str) -> Tuple[bool, List[str]]:
"""Check if AI provider modules are available and return missing packages
Args:
provider_name: 'chatgpt', 'claude', 'anthropic', or 'github-copilot'
provider_name: 'chatgpt', 'claude', 'anthropic', 'github-copilot', or 'ollama'
Returns:
tuple: (all_available, missing_packages)
"""
missing_packages = []
# Common packages needed for AI providers
required_common = ['GitPython']
# Provider-specific packages
if provider_name.lower() == 'chatgpt':
required_packages = required_common + ['openai']
@@ -2971,9 +3166,11 @@ class AIManager:
required_packages = required_common + ['anthropic']
elif provider_name.lower() in ['github-copilot', 'copilot', 'github_copilot']:
required_packages = required_common + ['requests']
elif provider_name.lower() == 'ollama':
required_packages = required_common + ['requests']
else:
return True, [] # Unknown provider, assume no check needed
for package in required_packages:
try:
if package == 'GitPython':
@@ -2982,9 +3179,11 @@ class AIManager:
import openai
elif package == 'anthropic':
import anthropic
elif package == 'requests':
import requests
except ImportError:
missing_packages.append(package)
all_available = len(missing_packages) == 0
return all_available, missing_packages
@@ -3159,14 +3358,14 @@ class AIManager:
if self.logger:
self.logger.log(f"Error in AI modules check: {str(e)}")
def create_ai_provider(self, provider_name: str, api_key: str):
def create_ai_provider(self, provider_name: str, api_key: str, ollama_url: str = None, ollama_model: str = None):
"""Create an AI provider instance"""
if not AI_PROVIDERS_AVAILABLE:
return None
try:
ai_logger = Logger(self.log)
return create_ai_provider(provider_name, api_key, ai_logger)
return create_ai_provider(provider_name, api_key, ai_logger, ollama_url, ollama_model)
except Exception as e:
self.log(f"Error creating AI provider: {e}")
return None