-
-
Notifications
You must be signed in to change notification settings - Fork 49
Feat: Semantic Version Conflict Resolver #154 #350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9bbace0
071f61a
a891d4b
a1b9b2f
ecd4ea8
648b091
9195439
710410c
dbd7031
d54fb69
d72984a
60dda9b
4e5fcac
880f98f
fbd46a7
8dd3ff8
00389f1
058b56d
25e378f
cb37d66
261d0d7
63213f2
39f797d
9935086
e7ebde1
0e50765
08bdf4a
b8cc32c
b56b512
a52eaec
ea4f2e5
e64c079
2024934
1443137
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,16 @@ | ||
| import argparse | ||
| import logging | ||
| import os | ||
| import re | ||
| import shutil | ||
| import sys | ||
| import time | ||
| from datetime import datetime | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| import aiofiles | ||
|
|
||
| from cortex.api_key_detector import auto_detect_api_key, setup_api_key | ||
| from cortex.ask import AskHandler | ||
| from cortex.branding import VERSION, console, cx_header, cx_print, show_banner | ||
|
|
@@ -601,6 +605,125 @@ def _sandbox_exec(self, sandbox, args: argparse.Namespace) -> int: | |
|
|
||
| # --- End Sandbox Commands --- | ||
|
|
||
| async def _update_manifest(self, package_name: str, version_constraint: str) -> None: | ||
| """Update the requirements.txt file with the resolved version. | ||
|
|
||
| Args: | ||
| package_name: The name of the package to update. | ||
| version_constraint: The version string to apply (e.g., "1.2.0"). | ||
|
|
||
| Returns: | ||
| None | ||
| """ | ||
| manifest_path = "requirements.txt" | ||
| if not os.path.exists(manifest_path): | ||
| cx_print(f"Advisory: Manual update required for {package_name}.", "warning") | ||
| return | ||
|
|
||
| try: | ||
| # Use aiofiles for asynchronous non-blocking read | ||
| async with aiofiles.open(manifest_path) as f: | ||
| lines = await f.readlines() | ||
|
|
||
| new_lines = [] | ||
| updated = False | ||
| for line in lines: | ||
| if not line.strip() or line.startswith("#"): | ||
| new_lines.append(line) | ||
| continue | ||
|
|
||
| parts = re.split(r"[=<>~!]", line) | ||
| current_pkg_name = parts[0].strip() | ||
|
|
||
| if current_pkg_name == package_name: | ||
| new_lines.append(f"{package_name}=={version_constraint}\n") | ||
| updated = True | ||
| else: | ||
| new_lines.append(line) | ||
|
|
||
| if not updated: | ||
| new_lines.append(f"{package_name}=={version_constraint}\n") | ||
|
|
||
| # Use aiofiles for asynchronous non-blocking write | ||
| async with aiofiles.open(manifest_path, mode="w") as f: | ||
| await f.writelines(new_lines) | ||
|
|
||
| status_msg = f"Updated {manifest_path}" if updated else f"Added to {manifest_path}" | ||
| cx_print(f"✓ {status_msg} with {package_name}", "success") | ||
|
|
||
| except Exception as file_err: | ||
| self._print_error(f"Could not update manifest: {file_err}") | ||
|
|
||
| async def resolve(self, args: argparse.Namespace) -> int: | ||
| """Handle the dependency resolution command asynchronously. | ||
|
|
||
| Args: | ||
| args: Parsed command-line arguments containing package and version data. | ||
|
|
||
| Returns: | ||
| int: 0 for successful resolution, 1 for failure. | ||
| """ | ||
| from rich.prompt import Prompt | ||
|
|
||
| from cortex.resolver import DependencyResolver | ||
|
|
||
| try: | ||
| conflict_data = { | ||
| "package_a": {"name": args.package, "requires": args.version}, | ||
| "package_b": {"name": args.package_b, "requires": args.version_b}, | ||
| "dependency": args.dependency, | ||
| } | ||
|
|
||
| cx_header("AI Conflict Analysis") | ||
| cx_print(f"Analyzing conflicts for [bold]{args.dependency}[/bold]...", "thinking") | ||
|
|
||
| # Initialize resolver with detected credentials | ||
| api_key = self._get_api_key() | ||
| provider = self._get_provider() | ||
| resolver = DependencyResolver(api_key=api_key, provider=provider) | ||
|
|
||
| # Trigger AI/Deterministic analysis | ||
| results = await resolver.resolve(conflict_data) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Awaiting a synchronous method will cause a runtime error. Based on the relevant code snippet from 🐛 Proposed fix - call synchronously- # Trigger AI/Deterministic analysis
- results = await resolver.resolve(conflict_data)
+ # Trigger AI/Deterministic analysis (synchronous call)
+ results = resolver.resolve(conflict_data)Alternatively, if you intend to keep the CLI method async for non-blocking file I/O, wrap the synchronous call in an executor: import asyncio
loop = asyncio.get_event_loop()
results = await loop.run_in_executor(None, resolver.resolve, conflict_data)🤖 Prompt for AI Agents |
||
|
|
||
| if not results or results[0].get("type") == "Error": | ||
| error_msg = results[0].get("action") if results else "Unknown error" | ||
| self._print_error(f"Resolution failed: {error_msg}") | ||
| return 1 | ||
|
|
||
| # Display strategies to the user | ||
| for s in results: | ||
| s_type = s.get("type", "Unknown") | ||
| color = "green" if s_type == "Recommended" else "yellow" | ||
| console.print(f"\n[{color}]Strategy {s['id']} ({s_type}):[/{color}]") | ||
| console.print(f" [bold]Action:[/bold] {s['action']}") | ||
| console.print(f" [bold]Risk:[/bold] {s['risk']}") | ||
|
|
||
| # Prompt user for selection | ||
| choices = [str(s.get("id")) for s in results] | ||
| choice = Prompt.ask("\nSelect strategy to apply", choices=choices, default=choices[0]) | ||
| selected = next((s for s in results if str(s.get("id")) == choice), None) | ||
|
|
||
| if selected: | ||
| cx_print(f"Applying strategy {choice}...", "info") | ||
| action = selected.get("action", "") | ||
|
|
||
| # Parse the action string (e.g., "Use django 4.2.0") | ||
| match = re.search(r"Use\s+(\S+)\s+(.+)", action) | ||
| if match: | ||
| package_name = match.group(1) | ||
| # Strip syntax symbols for clean manifest writing | ||
| version_constraint = match.group(2).strip("^~ ") | ||
|
|
||
| # Call the helper method to perform safe, async file I/O | ||
| await self._update_manifest(package_name, version_constraint) | ||
|
|
||
| self._print_success("✓ Conflict resolved successfully") | ||
|
Comment on lines
+706
to
+720
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Success message shown even if action parsing fails. If the action string doesn't match the expected pattern 🔧 Proposed fix if selected:
cx_print(f"Applying strategy {choice}...", "info")
action = selected.get("action", "")
# Parse the action string (e.g., "Use django 4.2.0")
match = re.search(r"Use\s+(\S+)\s+(.+)", action)
if match:
package_name = match.group(1)
# Strip syntax symbols for clean manifest writing
version_constraint = match.group(2).strip("^~ ")
# Call the helper method to perform safe, async file I/O
await self._update_manifest(package_name, version_constraint)
+ self._print_success("✓ Conflict resolved successfully")
+ else:
+ cx_print(
+ f"Could not parse action: '{action}'. Manual update may be required.",
+ "warning",
+ )
- self._print_success("✓ Conflict resolved successfully")
return 0🤖 Prompt for AI Agents |
||
| return 0 | ||
|
|
||
| except Exception as e: | ||
| self._print_error(f"Resolution process failed: {e}") | ||
| return 1 | ||
|
|
||
| def ask(self, question: str) -> int: | ||
| """Answer a natural language question about the system.""" | ||
| api_key = self._get_api_key() | ||
|
|
@@ -2185,6 +2308,33 @@ def main(): | |
| rollback_parser.add_argument("id", help="Installation ID") | ||
| rollback_parser.add_argument("--dry-run", action="store_true") | ||
|
|
||
| # Dependencies command | ||
| deps_parser = subparsers.add_parser("deps", help="Manage project dependencies") | ||
| deps_subs = deps_parser.add_subparsers(dest="deps_action") | ||
|
|
||
| resolve_parser = deps_subs.add_parser( | ||
| "resolve", | ||
| help="AI-powered version conflict resolution", | ||
| description=""" | ||
| Resolve semantic version conflicts between packages using AI. | ||
| Examples: | ||
| # Basic usage | ||
| cortex deps resolve --package django --version ">=3.0.0" \\ | ||
| --package-b admin --version-b "<4.0.0" \\ | ||
| --dependency django | ||
| # Breaking change detection | ||
| cortex deps resolve --package api --version "^2.0.0" \\ | ||
| --package-b legacy --version-b "~1.9.0" \\ | ||
| --dependency requests | ||
| """, | ||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||
| ) | ||
| resolve_parser.add_argument("--package", required=True, help="Name of package A") | ||
| resolve_parser.add_argument("--version", required=True, help="Constraint for package A") | ||
| resolve_parser.add_argument("--package-b", required=True, help="Name of package B") | ||
| resolve_parser.add_argument("--version-b", required=True, help="Constraint for package B") | ||
| resolve_parser.add_argument("--dependency", required=True, help="Conflicting dependency") | ||
|
|
||
| # --- New Notify Command --- | ||
| notify_parser = subparsers.add_parser("notify", help="Manage desktop notifications") | ||
| notify_subs = notify_parser.add_subparsers(dest="notify_action", help="Notify actions") | ||
|
|
@@ -2517,6 +2667,14 @@ def main(): | |
| return cli.history(limit=args.limit, status=args.status, show_id=args.show_id) | ||
| elif args.command == "rollback": | ||
| return cli.rollback(args.id, dry_run=args.dry_run) | ||
| elif args.command == "deps": | ||
| if args.deps_action == "resolve": | ||
| import asyncio | ||
|
|
||
| return asyncio.run(cli.resolve(args)) | ||
| deps_parser.print_help() | ||
| return 1 | ||
|
|
||
| # Handle the new notify command | ||
| elif args.command == "notify": | ||
| return cli.notify(args) | ||
|
|
||
Kesavaraja67 marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,192 @@ | ||||||||||||||
| """ | ||||||||||||||
| Semantic Version Conflict Resolver Module. | ||||||||||||||
| Handles dependency version conflicts using AI-driven intelligent analysis. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| import json | ||||||||||||||
| import logging | ||||||||||||||
| import re | ||||||||||||||
| from typing import Any | ||||||||||||||
|
|
||||||||||||||
| import semantic_version as sv | ||||||||||||||
|
|
||||||||||||||
| from cortex.ask import AskHandler | ||||||||||||||
|
|
||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class DependencyResolver: | ||||||||||||||
| """AI-powered semantic version conflict resolver. | ||||||||||||||
| Analyzes dependency trees and suggests upgrade/downgrade paths using | ||||||||||||||
| deterministic logic and AI reasoning. | ||||||||||||||
| """ | ||||||||||||||
|
|
||||||||||||||
| def __init__(self, api_key: str | None = None, provider: str = "ollama"): | ||||||||||||||
| """Initialize the resolver with the AskHandler for reasoning. | ||||||||||||||
| Args: | ||||||||||||||
| api_key: API key for the AI provider. Defaults to "ollama" for local mode. | ||||||||||||||
| provider: The AI service provider to use (e.g., "openai", "claude"). | ||||||||||||||
| """ | ||||||||||||||
| self.handler = AskHandler( | ||||||||||||||
| api_key=api_key or "ollama", | ||||||||||||||
| provider=provider, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| def resolve(self, conflict_data: dict[str, Any]) -> list[dict[str, Any]]: | ||||||||||||||
| """Resolve version conflicts using deterministic analysis and AI. | ||||||||||||||
| Args: | ||||||||||||||
| conflict_data: Dictionary containing 'package_a', 'package_b', | ||||||||||||||
| and the 'dependency' name. | ||||||||||||||
| Returns: | ||||||||||||||
| List of strategy dictionaries with resolution actions and risk levels. | ||||||||||||||
| Raises: | ||||||||||||||
| KeyError: If required keys are missing from conflict_data. | ||||||||||||||
| """ | ||||||||||||||
| required_keys = ["package_a", "package_b", "dependency"] | ||||||||||||||
| for key in required_keys: | ||||||||||||||
| if key not in conflict_data: | ||||||||||||||
| raise KeyError(f"Missing required key: {key}") | ||||||||||||||
|
|
||||||||||||||
| # 1. Deterministic resolution first (Reliable & Fast) | ||||||||||||||
| strategies = self._deterministic_resolution(conflict_data) | ||||||||||||||
|
|
||||||||||||||
| # CRITICAL FIX: If we have a mathematical match, RETURN IMMEDIATELY. | ||||||||||||||
| # Do not proceed to AI logic. | ||||||||||||||
| if strategies: | ||||||||||||||
| return strategies | ||||||||||||||
|
|
||||||||||||||
| # 2. AI Reasoning fallback | ||||||||||||||
| prompt = self._build_prompt(conflict_data) | ||||||||||||||
| try: | ||||||||||||||
| response = self.handler.ask(prompt) | ||||||||||||||
|
|
||||||||||||||
| # Safety check for unit tests | ||||||||||||||
| if not isinstance(response, str): | ||||||||||||||
| return [ | ||||||||||||||
| { | ||||||||||||||
| "id": 1, | ||||||||||||||
| "type": "Manual", | ||||||||||||||
| "action": f"Check {conflict_data['dependency']} compatibility manually.", | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix PEP 8 line length violations. Lines 74, 87, and 98 exceed the PEP 8 recommended maximum of 79 characters. These lines contain similar f-string constructions that should be refactored for compliance. 🔧 Proposed fix {
"id": 1,
"type": "Manual",
- "action": f"Check {conflict_data['dependency']} compatibility manually.",
+ "action": (
+ f"Check {conflict_data['dependency']} "
+ "compatibility manually."
+ ),
"risk": "High",
}Apply similar refactoring to lines 87 and 98. As per coding guidelines, PEP 8 style compliance is required. Also applies to: 87-87, 98-98 |
||||||||||||||
| "risk": "High", | ||||||||||||||
| } | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| ai_strategies = self._parse_ai_response(response) | ||||||||||||||
| return ( | ||||||||||||||
| ai_strategies | ||||||||||||||
| if ai_strategies | ||||||||||||||
| else [ | ||||||||||||||
| { | ||||||||||||||
| "id": 1, | ||||||||||||||
| "type": "Manual", | ||||||||||||||
| "action": f"Check {conflict_data['dependency']} compatibility manually.", | ||||||||||||||
| "risk": "High", | ||||||||||||||
| } | ||||||||||||||
| ] | ||||||||||||||
| ) | ||||||||||||||
| except Exception as e: | ||||||||||||||
| logger.error(f"AI Resolution failed: {e}") | ||||||||||||||
| return [ | ||||||||||||||
| { | ||||||||||||||
| "id": 1, | ||||||||||||||
| "type": "Manual", | ||||||||||||||
| "action": f"Check {conflict_data['dependency']} compatibility manually.", | ||||||||||||||
| "risk": "High", | ||||||||||||||
| } | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| def _deterministic_resolution(self, data: dict[str, Any]) -> list[dict[str, Any]]: | ||||||||||||||
| """Perform robust semantic-version analysis. | ||||||||||||||
| Args: | ||||||||||||||
| data: Dictionary containing package requirements. | ||||||||||||||
| Returns: | ||||||||||||||
| List of strategy dictionaries or empty list to trigger AI fallback. | ||||||||||||||
| """ | ||||||||||||||
| try: | ||||||||||||||
| dependency = data["dependency"] | ||||||||||||||
| req_a = data["package_a"]["requires"].strip() | ||||||||||||||
| req_b = data["package_b"]["requires"].strip() | ||||||||||||||
|
|
||||||||||||||
| # 1. Handle exact equality (Fast return for Low risk) | ||||||||||||||
| if req_a == req_b: | ||||||||||||||
| return [ | ||||||||||||||
| { | ||||||||||||||
| "id": 1, | ||||||||||||||
| "type": "Recommended", | ||||||||||||||
| "risk": "Low", | ||||||||||||||
| "action": f"Use {dependency} {req_a}", | ||||||||||||||
| "explanation": "Both packages require the same version.", | ||||||||||||||
| } | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| # 2. Mathematical Match Check (Handles intersection and whitespace tests) | ||||||||||||||
| spec_a = sv.SimpleSpec(req_a) | ||||||||||||||
| spec_b = sv.SimpleSpec(req_b) | ||||||||||||||
|
|
||||||||||||||
| # Find boundary version to prove overlap | ||||||||||||||
| v_match = re.search(r"(\d+\.\d+\.\d+)", req_a) | ||||||||||||||
| if v_match: | ||||||||||||||
| base_v = sv.Version(v_match.group(1)) | ||||||||||||||
| if spec_a.match(base_v) and spec_b.match(base_v): | ||||||||||||||
| return [ | ||||||||||||||
| { | ||||||||||||||
| "id": 1, | ||||||||||||||
| "type": "Recommended", | ||||||||||||||
| "risk": "Low", | ||||||||||||||
| "action": f"Use {dependency} {req_a},{req_b}", | ||||||||||||||
| "explanation": "Mathematical intersection verified.", | ||||||||||||||
| } | ||||||||||||||
| ] | ||||||||||||||
|
|
||||||||||||||
| # 3. Trigger AI fallback for complex conflicts (CRITICAL FOR AI TESTS) | ||||||||||||||
| # We return [] to let the 'resolve' method proceed to the AI reasoning logic. | ||||||||||||||
| return [] | ||||||||||||||
|
|
||||||||||||||
| except Exception as e: | ||||||||||||||
| logger.debug(f"Deterministic logic skipped: {e}") | ||||||||||||||
| return [] | ||||||||||||||
|
|
||||||||||||||
| def _build_prompt(self, data: dict[str, Any]) -> str: | ||||||||||||||
| """Constructs a prompt for direct JSON response with parseable actions. | ||||||||||||||
| Args: | ||||||||||||||
| data: The conflict data to process. | ||||||||||||||
| Returns: | ||||||||||||||
| A formatted prompt string for the LLM. | ||||||||||||||
| """ | ||||||||||||||
| return ( | ||||||||||||||
| f"Act as a semantic version conflict resolver. " | ||||||||||||||
| f"Analyze this conflict for the dependency: {data['dependency']}. " | ||||||||||||||
| f"Package '{data['package_a']['name']}' requires {data['package_a']['requires']}. " | ||||||||||||||
| f"Package '{data['package_b']['name']}' requires {data['package_b']['requires']}. " | ||||||||||||||
| "Return ONLY a JSON array of 2 objects with keys: 'id', 'type', 'action', 'risk'. " | ||||||||||||||
| "IMPORTANT: The 'action' field MUST follow the exact format: 'Use <package_name> <version>' " | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix PEP 8 line length violation in prompt string. Line 171 exceeds 79 characters. Break this long string literal into multiple parts. 🔧 Proposed fix f"Package '{data['package_b']['name']}' requires {data['package_b']['requires']}. "
"Return ONLY a JSON array of 2 objects with keys: 'id', 'type', 'action', 'risk'. "
- "IMPORTANT: The 'action' field MUST follow the exact format: 'Use <package_name> <version>' "
+ "IMPORTANT: The 'action' field MUST follow the exact format: "
+ "'Use <package_name> <version>' "
"(e.g., 'Use django 4.2.0') so it can be parsed by the system. "As per coding guidelines, PEP 8 style compliance is required. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| "(e.g., 'Use django 4.2.0') so it can be parsed by the system. " | ||||||||||||||
| f"Do not mention packages other than {data['package_a']['name']}, " | ||||||||||||||
| f"{data['package_b']['name']}, and {data['dependency']}." | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| def _parse_ai_response(self, response: str) -> list[dict[str, Any]]: | ||||||||||||||
| """Parses the LLM output safely using Regex to find JSON arrays. | ||||||||||||||
| Args: | ||||||||||||||
| response: The raw string response from the AI. | ||||||||||||||
| Returns: | ||||||||||||||
| A list of parsed strategy dictionaries or an empty list if parsing fails. | ||||||||||||||
| """ | ||||||||||||||
| try: | ||||||||||||||
| match = re.search(r"\[.*\]", response, re.DOTALL) | ||||||||||||||
| if match: | ||||||||||||||
| return json.loads(match.group(0)) | ||||||||||||||
| return [] | ||||||||||||||
| except (json.JSONDecodeError, AttributeError): | ||||||||||||||
| return [] | ||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.