fix(windows): add pywin32 dependency for LadybugDB (#627) (#778)

* fix(windows): add pywin32 dependency and improve error handling (#627)

Windows users were experiencing ModuleNotFoundError: No module named 'pywintypes'
when running subtasks, because pywin32 is required by real_ladybug but was missing
from requirements.txt.

Changes:
- apps/backend/requirements.txt: Add pywin32>=306 for Windows Python 3.12+
- apps/backend/core/dependency_validator.py: NEW - Validate platform-specific deps
- apps/backend/cli/utils.py: Integrate dependency validation in validate_environment()
- apps/backend/integrations/graphiti/queries_pkg/client.py: Improve Windows error logging
- tests/test_github_pr_review.py: Fix deprecated asyncio.get_event_loop().run_until_complete()
  pattern, convert to async/await with @pytest.mark.asyncio

Fixes #627

* fix: address PR feedback from code review

- Use pathlib Path operator for proper Windows path separators
- Use sys.prefix for venv path detection (works with conda, poetry, etc.)
- Add hasattr check for ImportError.name for more robust pywin32 detection
- Add Python version check (3.12+) to match requirements.txt constraint
- Remove unnecessary pass statement

Co-authored-by: gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com>

* fix: correct misleading comment about conda support

The comment incorrectly stated sys.prefix works for conda, but
conda on Windows uses 'conda activate <env>' rather than
Scripts/activate path.

* chore: add config.json to .gitignore

- Add /config.json to .gitignore to prevent accidental commits
- Config files may contain sensitive settings and should not be tracked

---------

Co-authored-by: gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Andy
2026-01-07 14:01:44 +01:00
committed by GitHub
parent 01decaeb26
commit fbaf2e7ab4
6 changed files with 87 additions and 19 deletions
+1
View File
@@ -14,6 +14,7 @@ Desktop.ini
.env
.env.*
!.env.example
/config.json
*.pem
*.key
*.crt
+4
View File
@@ -15,6 +15,7 @@ if str(_PARENT_DIR) not in sys.path:
sys.path.insert(0, str(_PARENT_DIR))
from core.auth import get_auth_token, get_auth_token_source
from core.dependency_validator import validate_platform_dependencies
def import_dotenv():
@@ -154,6 +155,9 @@ def validate_environment(spec_dir: Path) -> bool:
Returns:
True if valid, False otherwise (with error messages printed)
"""
# Validate platform-specific dependencies first (exits if missing)
validate_platform_dependencies()
valid = True
# Check for OAuth token (API keys are not supported)
+50
View File
@@ -0,0 +1,50 @@
"""
Dependency Validator
====================
Validates platform-specific dependencies are installed before running agents.
"""
import sys
from pathlib import Path
def validate_platform_dependencies() -> None:
"""
Validate that platform-specific dependencies are installed.
Raises:
SystemExit: If required platform-specific dependencies are missing,
with helpful installation instructions.
"""
# Check Windows-specific dependencies
if sys.platform == "win32" and sys.version_info >= (3, 12):
try:
import pywintypes # noqa: F401
except ImportError:
_exit_with_pywin32_error()
def _exit_with_pywin32_error() -> None:
"""Exit with helpful error message for missing pywin32."""
# Use sys.prefix to detect the virtual environment path
# This works for venv and poetry environments
venv_activate = Path(sys.prefix) / "Scripts" / "activate"
sys.exit(
"Error: Required Windows dependency 'pywin32' is not installed.\n"
"\n"
"Auto Claude requires pywin32 on Windows for LadybugDB/Graphiti memory integration.\n"
"\n"
"To fix this:\n"
"1. Activate your virtual environment:\n"
f" {venv_activate}\n"
"\n"
"2. Install pywin32:\n"
" pip install pywin32>=306\n"
"\n"
" Or reinstall all dependencies:\n"
" pip install -r requirements.txt\n"
"\n"
f"Current Python: {sys.executable}\n"
)
@@ -34,8 +34,25 @@ def _apply_ladybug_monkeypatch() -> bool:
sys.modules["kuzu"] = real_ladybug
logger.info("Applied LadybugDB monkeypatch (kuzu -> real_ladybug)")
return True
except ImportError:
pass
except ImportError as e:
logger.debug(f"LadybugDB import failed: {e}")
# On Windows with Python 3.12+, provide more specific error details
# (pywin32 is only required for Python 3.12+ per requirements.txt)
if sys.platform == "win32" and sys.version_info >= (3, 12):
# Check if it's the pywin32 error using both name attribute and string match
# for robustness across Python versions
is_pywin32_error = (
(hasattr(e, "name") and e.name in ("pywintypes", "pywin32", "win32api"))
or "pywintypes" in str(e)
or "pywin32" in str(e)
)
if is_pywin32_error:
logger.error(
"LadybugDB requires pywin32 on Windows. "
"Install with: pip install pywin32>=306"
)
else:
logger.debug(f"Windows-specific import issue: {e}")
# Fall back to native kuzu
try:
+4
View File
@@ -10,6 +10,10 @@ tomli>=2.0.0; python_version < "3.11"
real_ladybug>=0.13.0; python_version >= "3.12"
graphiti-core>=0.5.0; python_version >= "3.12"
# Windows-specific dependency for LadybugDB/Graphiti
# pywin32 provides Windows system bindings required by real_ladybug
pywin32>=306; sys_platform == "win32" and python_version >= "3.12"
# Google AI (optional - for Gemini LLM and embeddings)
google-generativeai>=0.8.0
+9 -17
View File
@@ -101,13 +101,11 @@ def mock_bot_detector(tmp_path):
class TestPRReviewResult:
"""Test PRReviewResult model."""
def test_save_and_load(self, temp_github_dir, sample_review_result):
@pytest.mark.asyncio
async def test_save_and_load(self, temp_github_dir, sample_review_result):
"""Test saving and loading review result."""
# Save
import asyncio
asyncio.get_event_loop().run_until_complete(
sample_review_result.save(temp_github_dir)
)
await sample_review_result.save(temp_github_dir)
# Verify file exists
review_file = temp_github_dir / "pr" / f"review_{sample_review_result.pr_number}.json"
@@ -337,14 +335,11 @@ class TestBotDetectionIntegration:
class TestOrchestratorSkipLogic:
"""Test orchestrator behavior when bot detection skips."""
def test_skip_returns_existing_review(self, temp_github_dir, sample_review_result):
@pytest.mark.asyncio
async def test_skip_returns_existing_review(self, temp_github_dir, sample_review_result):
"""Test that skipping 'Already reviewed' returns existing review."""
import asyncio
# Save existing review
asyncio.get_event_loop().run_until_complete(
sample_review_result.save(temp_github_dir)
)
await sample_review_result.save(temp_github_dir)
# Simulate the orchestrator logic for "Already reviewed" skip
skip_reason = "Already reviewed commit abc123"
@@ -466,19 +461,16 @@ class TestPostedFindingsTracking:
assert sample_review_result.has_posted_findings is True
assert len(sample_review_result.posted_finding_ids) == 1
def test_posted_findings_serialization(self, temp_github_dir, sample_review_result):
@pytest.mark.asyncio
async def test_posted_findings_serialization(self, temp_github_dir, sample_review_result):
"""Test that posted findings are serialized correctly."""
import asyncio
# Set posted findings
sample_review_result.has_posted_findings = True
sample_review_result.posted_finding_ids = ["finding-001"]
sample_review_result.posted_at = "2025-01-01T10:00:00"
# Save
asyncio.get_event_loop().run_until_complete(
sample_review_result.save(temp_github_dir)
)
await sample_review_result.save(temp_github_dir)
# Load and verify
loaded = PRReviewResult.load(temp_github_dir, sample_review_result.pr_number)