|
""" |
|
Cursor Rules Generator - Hugging Face Spaces App |
|
|
|
This module implements the Gradio interface for Hugging Face Spaces deployment. |
|
All code is self-contained in this file to avoid import issues. |
|
""" |
|
|
|
import os |
|
import gradio as gr |
|
import json |
|
import requests |
|
import traceback |
|
from dotenv import load_dotenv |
|
from abc import ABC, abstractmethod |
|
from typing import Dict, List, Optional, Any |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
class Settings: |
|
"""Application settings.""" |
|
|
|
|
|
APP_NAME = "Cursor Rules Generator" |
|
DEBUG = os.getenv("DEBUG", "False").lower() == "true" |
|
|
|
|
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") |
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") |
|
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") |
|
|
|
|
|
DEFAULT_PROVIDER = os.getenv("DEFAULT_PROVIDER", "gemini") |
|
DEFAULT_RULE_TYPE = os.getenv("DEFAULT_RULE_TYPE", "Always") |
|
|
|
|
|
GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta" |
|
OPENAI_API_URL = "https://api.openai.com/v1" |
|
OPENROUTER_API_URL = "https://openrouter.ai/api/v1" |
|
|
|
|
|
DEFAULT_GEMINI_MODEL = os.getenv("DEFAULT_GEMINI_MODEL", "gemini-2.0-flash") |
|
DEFAULT_OPENAI_MODEL = os.getenv("DEFAULT_OPENAI_MODEL", "gpt-4o") |
|
DEFAULT_OPENROUTER_MODEL = os.getenv("DEFAULT_OPENROUTER_MODEL", "openai/gpt-4o") |
|
|
|
|
|
MAX_RULE_LENGTH = int(os.getenv("MAX_RULE_LENGTH", "10000")) |
|
DEFAULT_TEMPERATURE = float(os.getenv("DEFAULT_TEMPERATURE", "0.7")) |
|
|
|
|
|
class LLMAdapter(ABC): |
|
"""Base adapter interface for LLM providers.""" |
|
|
|
@abstractmethod |
|
def initialize(self, api_key: str, **kwargs) -> None: |
|
"""Initialize the adapter with API key and optional parameters.""" |
|
pass |
|
|
|
@abstractmethod |
|
def validate_api_key(self, api_key: str) -> bool: |
|
"""Validate the API key.""" |
|
pass |
|
|
|
@abstractmethod |
|
def get_available_models(self) -> List[Dict[str, str]]: |
|
"""Get a list of available models from the provider.""" |
|
pass |
|
|
|
@abstractmethod |
|
def generate_rule( |
|
self, |
|
model: str, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
parameters: Optional[Dict[str, Any]] = None |
|
) -> str: |
|
"""Generate a Cursor Rule using the LLM provider.""" |
|
pass |
|
|
|
|
|
class GeminiAdapter(LLMAdapter): |
|
"""Adapter for Google's Gemini API.""" |
|
|
|
def __init__(self): |
|
"""Initialize the Gemini adapter.""" |
|
self.api_key = None |
|
self.api_url = Settings.GEMINI_API_URL |
|
self.initialized = False |
|
self.last_error = None |
|
|
|
def initialize(self, api_key: str, **kwargs) -> None: |
|
"""Initialize the adapter with API key and optional parameters.""" |
|
self.api_key = api_key |
|
self.api_url = kwargs.get('api_url', Settings.GEMINI_API_URL) |
|
self.initialized = True |
|
|
|
def validate_api_key(self, api_key: str) -> bool: |
|
"""Validate the Gemini API key.""" |
|
try: |
|
|
|
url = f"{self.api_url}/models?key={api_key}" |
|
response = requests.get(url) |
|
|
|
|
|
if response.status_code == 200: |
|
return True |
|
|
|
|
|
self.last_error = f"API Error: Status {response.status_code}, Response: {response.text}" |
|
print(f"Gemini API validation failed: {self.last_error}") |
|
return False |
|
except Exception as e: |
|
|
|
self.last_error = f"Exception: {str(e)}\n{traceback.format_exc()}" |
|
print(f"Gemini API validation exception: {self.last_error}") |
|
return False |
|
|
|
def get_available_models(self) -> List[Dict[str, str]]: |
|
"""Get a list of available Gemini models.""" |
|
if not self.initialized: |
|
raise ValueError("Adapter not initialized. Call initialize() first.") |
|
|
|
try: |
|
|
|
url = f"{self.api_url}/models?key={self.api_key}" |
|
response = requests.get(url) |
|
|
|
if response.status_code != 200: |
|
print(f"Failed to get models: Status {response.status_code}, Response: {response.text}") |
|
raise ValueError(f"Failed to get models: {response.text}") |
|
|
|
data = response.json() |
|
|
|
|
|
models = [] |
|
for model in data.get('models', []): |
|
if 'gemini' in model.get('name', '').lower(): |
|
model_id = model.get('name').split('/')[-1] |
|
models.append({ |
|
'id': model_id, |
|
'name': self._format_model_name(model_id) |
|
}) |
|
|
|
|
|
if not models: |
|
models = [ |
|
{'id': 'gemini-2.5-pro', 'name': 'Gemini 2.5 Pro'}, |
|
{'id': 'gemini-2.0-flash', 'name': 'Gemini 2.0 Flash'}, |
|
{'id': 'gemini-2.0-flash-lite', 'name': 'Gemini 2.0 Flash-Lite'} |
|
] |
|
|
|
return models |
|
except Exception as e: |
|
print(f"Exception in get_available_models: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return [ |
|
{'id': 'gemini-2.5-pro', 'name': 'Gemini 2.5 Pro'}, |
|
{'id': 'gemini-2.0-flash', 'name': 'Gemini 2.0 Flash'}, |
|
{'id': 'gemini-2.0-flash-lite', 'name': 'Gemini 2.0 Flash-Lite'} |
|
] |
|
|
|
def generate_rule( |
|
self, |
|
model: str, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
parameters: Optional[Dict[str, Any]] = None |
|
) -> str: |
|
"""Generate a Cursor Rule using Gemini.""" |
|
if not self.initialized: |
|
raise ValueError("Adapter not initialized. Call initialize() first.") |
|
|
|
|
|
if parameters is None: |
|
parameters = {} |
|
|
|
|
|
temperature = parameters.get('temperature', Settings.DEFAULT_TEMPERATURE) |
|
globs = parameters.get('globs', '') |
|
referenced_files = parameters.get('referenced_files', '') |
|
prompt = parameters.get('prompt', '') |
|
|
|
|
|
system_prompt = """ |
|
You are a Cursor Rules expert. Create a rule in MDC format based on the provided information. |
|
|
|
MDC format example: |
|
--- |
|
description: RPC Service boilerplate |
|
globs: |
|
alwaysApply: false |
|
--- |
|
|
|
- Use our internal RPC pattern when defining services |
|
- Always use snake_case for service names. |
|
|
|
@service-template.ts |
|
""" |
|
|
|
user_prompt = f""" |
|
Create a Cursor Rule with the following details: |
|
|
|
Rule Type: {rule_type} |
|
Description: {description} |
|
Content: {content} |
|
""" |
|
|
|
if globs: |
|
user_prompt += f"\nGlobs: {globs}" |
|
|
|
if referenced_files: |
|
user_prompt += f"\nReferenced Files: {referenced_files}" |
|
|
|
if prompt: |
|
user_prompt += f"\nAdditional Instructions: {prompt}" |
|
|
|
|
|
url = f"{self.api_url}/models/{model}:generateContent?key={self.api_key}" |
|
|
|
payload = { |
|
"contents": [ |
|
{ |
|
"role": "user", |
|
"parts": [ |
|
{"text": system_prompt + "\n\n" + user_prompt} |
|
] |
|
} |
|
], |
|
"generationConfig": { |
|
"temperature": temperature, |
|
"topP": 0.8, |
|
"topK": 40, |
|
"maxOutputTokens": 2048 |
|
} |
|
} |
|
|
|
|
|
try: |
|
response = requests.post(url, json=payload) |
|
|
|
if response.status_code != 200: |
|
print(f"Failed to generate rule: Status {response.status_code}, Response: {response.text}") |
|
raise ValueError(f"Failed to generate rule: {response.text}") |
|
|
|
data = response.json() |
|
|
|
|
|
generated_text = data.get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '') |
|
|
|
|
|
if not generated_text: |
|
return self._create_basic_rule(rule_type, description, content, globs, referenced_files) |
|
|
|
return generated_text |
|
except Exception as e: |
|
print(f"Exception in generate_rule: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return self._create_basic_rule(rule_type, description, content, globs, referenced_files) |
|
|
|
def _format_model_name(self, model_id: str) -> str: |
|
"""Format a model ID into a human-readable name.""" |
|
|
|
name = model_id.replace('-', ' ').title() |
|
|
|
|
|
name = name.replace('Gemini ', 'Gemini ') |
|
name = name.replace('Pro ', 'Pro ') |
|
name = name.replace('Flash ', 'Flash ') |
|
name = name.replace('Lite', 'Lite') |
|
|
|
return name |
|
|
|
def _create_basic_rule( |
|
self, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
globs: str = '', |
|
referenced_files: str = '' |
|
) -> str: |
|
"""Create a basic rule in MDC format without using the LLM.""" |
|
|
|
mdc = '---\n' |
|
mdc += f'description: {description}\n' |
|
|
|
if rule_type == 'Auto Attached' and globs: |
|
mdc += f'globs: {globs}\n' |
|
|
|
if rule_type == 'Always': |
|
mdc += 'alwaysApply: true\n' |
|
else: |
|
mdc += 'alwaysApply: false\n' |
|
|
|
mdc += '---\n\n' |
|
mdc += content + '\n' |
|
|
|
|
|
if referenced_files: |
|
mdc += '\n' + referenced_files |
|
|
|
return mdc |
|
|
|
|
|
class OpenAIAdapter(LLMAdapter): |
|
"""Adapter for OpenAI API.""" |
|
|
|
def __init__(self): |
|
"""Initialize the OpenAI adapter.""" |
|
self.api_key = None |
|
self.api_url = Settings.OPENAI_API_URL |
|
self.initialized = False |
|
self.last_error = None |
|
|
|
def initialize(self, api_key: str, **kwargs) -> None: |
|
"""Initialize the adapter with API key and optional parameters.""" |
|
self.api_key = api_key |
|
self.api_url = kwargs.get('api_url', Settings.OPENAI_API_URL) |
|
self.initialized = True |
|
|
|
def validate_api_key(self, api_key: str) -> bool: |
|
"""Validate the OpenAI API key.""" |
|
try: |
|
|
|
url = f"{self.api_url}/models" |
|
headers = { |
|
"Authorization": f"Bearer {api_key}" |
|
} |
|
response = requests.get(url, headers=headers) |
|
|
|
|
|
if response.status_code == 200: |
|
return True |
|
|
|
|
|
self.last_error = f"API Error: Status {response.status_code}, Response: {response.text}" |
|
print(f"OpenAI API validation failed: {self.last_error}") |
|
return False |
|
except Exception as e: |
|
|
|
self.last_error = f"Exception: {str(e)}\n{traceback.format_exc()}" |
|
print(f"OpenAI API validation exception: {self.last_error}") |
|
return False |
|
|
|
def get_available_models(self) -> List[Dict[str, str]]: |
|
"""Get a list of available OpenAI models.""" |
|
if not self.initialized: |
|
raise ValueError("Adapter not initialized. Call initialize() first.") |
|
|
|
try: |
|
|
|
url = f"{self.api_url}/models" |
|
headers = { |
|
"Authorization": f"Bearer {self.api_key}" |
|
} |
|
response = requests.get(url, headers=headers) |
|
|
|
if response.status_code != 200: |
|
print(f"Failed to get models: Status {response.status_code}, Response: {response.text}") |
|
raise ValueError(f"Failed to get models: {response.text}") |
|
|
|
data = response.json() |
|
|
|
|
|
models = [] |
|
for model in data.get('data', []): |
|
model_id = model.get('id') |
|
if any(prefix in model_id for prefix in ['gpt-4', 'gpt-3.5']): |
|
models.append({ |
|
'id': model_id, |
|
'name': self._format_model_name(model_id) |
|
}) |
|
|
|
|
|
if not models: |
|
models = [ |
|
{'id': 'gpt-4o', 'name': 'GPT-4o'}, |
|
{'id': 'gpt-4-turbo', 'name': 'GPT-4 Turbo'}, |
|
{'id': 'gpt-3.5-turbo', 'name': 'GPT-3.5 Turbo'} |
|
] |
|
|
|
return models |
|
except Exception as e: |
|
print(f"Exception in get_available_models: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return [ |
|
{'id': 'gpt-4o', 'name': 'GPT-4o'}, |
|
{'id': 'gpt-4-turbo', 'name': 'GPT-4 Turbo'}, |
|
{'id': 'gpt-3.5-turbo', 'name': 'GPT-3.5 Turbo'} |
|
] |
|
|
|
def generate_rule( |
|
self, |
|
model: str, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
parameters: Optional[Dict[str, Any]] = None |
|
) -> str: |
|
"""Generate a Cursor Rule using OpenAI.""" |
|
if not self.initialized: |
|
raise ValueError("Adapter not initialized. Call initialize() first.") |
|
|
|
|
|
if parameters is None: |
|
parameters = {} |
|
|
|
|
|
temperature = parameters.get('temperature', Settings.DEFAULT_TEMPERATURE) |
|
globs = parameters.get('globs', '') |
|
referenced_files = parameters.get('referenced_files', '') |
|
prompt = parameters.get('prompt', '') |
|
|
|
|
|
system_prompt = """ |
|
You are a Cursor Rules expert. Create a rule in MDC format based on the provided information. |
|
|
|
MDC format example: |
|
--- |
|
description: RPC Service boilerplate |
|
globs: |
|
alwaysApply: false |
|
--- |
|
|
|
- Use our internal RPC pattern when defining services |
|
- Always use snake_case for service names. |
|
|
|
@service-template.ts |
|
""" |
|
|
|
user_prompt = f""" |
|
Create a Cursor Rule with the following details: |
|
|
|
Rule Type: {rule_type} |
|
Description: {description} |
|
Content: {content} |
|
""" |
|
|
|
if globs: |
|
user_prompt += f"\nGlobs: {globs}" |
|
|
|
if referenced_files: |
|
user_prompt += f"\nReferenced Files: {referenced_files}" |
|
|
|
if prompt: |
|
user_prompt += f"\nAdditional Instructions: {prompt}" |
|
|
|
|
|
url = f"{self.api_url}/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {self.api_key}", |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"model": model, |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_prompt |
|
} |
|
], |
|
"temperature": temperature, |
|
"max_tokens": 2048 |
|
} |
|
|
|
|
|
try: |
|
response = requests.post(url, headers=headers, json=payload) |
|
|
|
if response.status_code != 200: |
|
print(f"Failed to generate rule: Status {response.status_code}, Response: {response.text}") |
|
raise ValueError(f"Failed to generate rule: {response.text}") |
|
|
|
data = response.json() |
|
|
|
|
|
generated_text = data.get('choices', [{}])[0].get('message', {}).get('content', '') |
|
|
|
|
|
if not generated_text: |
|
return self._create_basic_rule(rule_type, description, content, globs, referenced_files) |
|
|
|
return generated_text |
|
except Exception as e: |
|
print(f"Exception in generate_rule: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return self._create_basic_rule(rule_type, description, content, globs, referenced_files) |
|
|
|
def _format_model_name(self, model_id: str) -> str: |
|
"""Format a model ID into a human-readable name.""" |
|
|
|
name = model_id.replace('-', ' ').title() |
|
|
|
|
|
name = name.replace('Gpt ', 'GPT ') |
|
name = name.replace('Gpt4', 'GPT-4') |
|
name = name.replace('Gpt3', 'GPT-3') |
|
name = name.replace('Gpt 4', 'GPT-4') |
|
name = name.replace('Gpt 3', 'GPT-3') |
|
name = name.replace('Turbo', 'Turbo') |
|
name = name.replace('O', 'o') |
|
|
|
return name |
|
|
|
def _create_basic_rule( |
|
self, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
globs: str = '', |
|
referenced_files: str = '' |
|
) -> str: |
|
"""Create a basic rule in MDC format without using the LLM.""" |
|
|
|
mdc = '---\n' |
|
mdc += f'description: {description}\n' |
|
|
|
if rule_type == 'Auto Attached' and globs: |
|
mdc += f'globs: {globs}\n' |
|
|
|
if rule_type == 'Always': |
|
mdc += 'alwaysApply: true\n' |
|
else: |
|
mdc += 'alwaysApply: false\n' |
|
|
|
mdc += '---\n\n' |
|
mdc += content + '\n' |
|
|
|
|
|
if referenced_files: |
|
mdc += '\n' + referenced_files |
|
|
|
return mdc |
|
|
|
|
|
class OpenRouterAdapter(LLMAdapter): |
|
"""Adapter for OpenRouter API.""" |
|
|
|
def __init__(self): |
|
"""Initialize the OpenRouter adapter.""" |
|
self.api_key = None |
|
self.api_url = Settings.OPENROUTER_API_URL |
|
self.initialized = False |
|
self.last_error = None |
|
|
|
def initialize(self, api_key: str, **kwargs) -> None: |
|
"""Initialize the adapter with API key and optional parameters.""" |
|
self.api_key = api_key |
|
self.api_url = kwargs.get('api_url', Settings.OPENROUTER_API_URL) |
|
self.site_url = kwargs.get('site_url', 'https://cursor-rules-generator.example.com') |
|
self.site_name = kwargs.get('site_name', 'Cursor Rules Generator') |
|
self.initialized = True |
|
|
|
def validate_api_key(self, api_key: str) -> bool: |
|
"""Validate the OpenRouter API key.""" |
|
try: |
|
|
|
url = f"{self.api_url}/models" |
|
headers = { |
|
"Authorization": f"Bearer {api_key}" |
|
} |
|
response = requests.get(url, headers=headers) |
|
|
|
|
|
if response.status_code == 200: |
|
return True |
|
|
|
|
|
self.last_error = f"API Error: Status {response.status_code}, Response: {response.text}" |
|
print(f"OpenRouter API validation failed: {self.last_error}") |
|
return False |
|
except Exception as e: |
|
|
|
self.last_error = f"Exception: {str(e)}\n{traceback.format_exc()}" |
|
print(f"OpenRouter API validation exception: {self.last_error}") |
|
return False |
|
|
|
def get_available_models(self) -> List[Dict[str, str]]: |
|
"""Get a list of available OpenRouter models.""" |
|
if not self.initialized: |
|
raise ValueError("Adapter not initialized. Call initialize() first.") |
|
|
|
try: |
|
|
|
url = f"{self.api_url}/models" |
|
headers = { |
|
"Authorization": f"Bearer {self.api_key}" |
|
} |
|
response = requests.get(url, headers=headers) |
|
|
|
if response.status_code != 200: |
|
print(f"Failed to get models: Status {response.status_code}, Response: {response.text}") |
|
raise ValueError(f"Failed to get models: {response.text}") |
|
|
|
data = response.json() |
|
|
|
|
|
models = [] |
|
for model in data.get('data', []): |
|
model_id = model.get('id') |
|
model_name = model.get('name', model_id) |
|
|
|
|
|
if not model.get('capabilities', {}).get('chat'): |
|
continue |
|
|
|
models.append({ |
|
'id': model_id, |
|
'name': model_name |
|
}) |
|
|
|
|
|
if not models: |
|
models = [ |
|
{'id': 'openai/gpt-4o', 'name': 'OpenAI GPT-4o'}, |
|
{'id': 'anthropic/claude-3-opus', 'name': 'Anthropic Claude 3 Opus'}, |
|
{'id': 'google/gemini-2.5-pro', 'name': 'Google Gemini 2.5 Pro'}, |
|
{'id': 'meta-llama/llama-3-70b-instruct', 'name': 'Meta Llama 3 70B'} |
|
] |
|
|
|
return models |
|
except Exception as e: |
|
print(f"Exception in get_available_models: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return [ |
|
{'id': 'openai/gpt-4o', 'name': 'OpenAI GPT-4o'}, |
|
{'id': 'anthropic/claude-3-opus', 'name': 'Anthropic Claude 3 Opus'}, |
|
{'id': 'google/gemini-2.5-pro', 'name': 'Google Gemini 2.5 Pro'}, |
|
{'id': 'meta-llama/llama-3-70b-instruct', 'name': 'Meta Llama 3 70B'} |
|
] |
|
|
|
def generate_rule( |
|
self, |
|
model: str, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
parameters: Optional[Dict[str, Any]] = None |
|
) -> str: |
|
"""Generate a Cursor Rule using OpenRouter.""" |
|
if not self.initialized: |
|
raise ValueError("Adapter not initialized. Call initialize() first.") |
|
|
|
|
|
if parameters is None: |
|
parameters = {} |
|
|
|
|
|
temperature = parameters.get('temperature', Settings.DEFAULT_TEMPERATURE) |
|
globs = parameters.get('globs', '') |
|
referenced_files = parameters.get('referenced_files', '') |
|
prompt = parameters.get('prompt', '') |
|
|
|
|
|
system_prompt = """ |
|
You are a Cursor Rules expert. Create a rule in MDC format based on the provided information. |
|
|
|
MDC format example: |
|
--- |
|
description: RPC Service boilerplate |
|
globs: |
|
alwaysApply: false |
|
--- |
|
|
|
- Use our internal RPC pattern when defining services |
|
- Always use snake_case for service names. |
|
|
|
@service-template.ts |
|
""" |
|
|
|
user_prompt = f""" |
|
Create a Cursor Rule with the following details: |
|
|
|
Rule Type: {rule_type} |
|
Description: {description} |
|
Content: {content} |
|
""" |
|
|
|
if globs: |
|
user_prompt += f"\nGlobs: {globs}" |
|
|
|
if referenced_files: |
|
user_prompt += f"\nReferenced Files: {referenced_files}" |
|
|
|
if prompt: |
|
user_prompt += f"\nAdditional Instructions: {prompt}" |
|
|
|
|
|
url = f"{self.api_url}/chat/completions" |
|
headers = { |
|
"Authorization": f"Bearer {self.api_key}", |
|
"Content-Type": "application/json", |
|
"HTTP-Referer": self.site_url, |
|
"X-Title": self.site_name |
|
} |
|
|
|
payload = { |
|
"model": model, |
|
"messages": [ |
|
{ |
|
"role": "system", |
|
"content": system_prompt |
|
}, |
|
{ |
|
"role": "user", |
|
"content": user_prompt |
|
} |
|
], |
|
"temperature": temperature, |
|
"max_tokens": 2048 |
|
} |
|
|
|
|
|
try: |
|
response = requests.post(url, headers=headers, json=payload) |
|
|
|
if response.status_code != 200: |
|
print(f"Failed to generate rule: Status {response.status_code}, Response: {response.text}") |
|
raise ValueError(f"Failed to generate rule: {response.text}") |
|
|
|
data = response.json() |
|
|
|
|
|
generated_text = data.get('choices', [{}])[0].get('message', {}).get('content', '') |
|
|
|
|
|
if not generated_text: |
|
return self._create_basic_rule(rule_type, description, content, globs, referenced_files) |
|
|
|
return generated_text |
|
except Exception as e: |
|
print(f"Exception in generate_rule: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return self._create_basic_rule(rule_type, description, content, globs, referenced_files) |
|
|
|
def _create_basic_rule( |
|
self, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
globs: str = '', |
|
referenced_files: str = '' |
|
) -> str: |
|
"""Create a basic rule in MDC format without using the LLM.""" |
|
|
|
mdc = '---\n' |
|
mdc += f'description: {description}\n' |
|
|
|
if rule_type == 'Auto Attached' and globs: |
|
mdc += f'globs: {globs}\n' |
|
|
|
if rule_type == 'Always': |
|
mdc += 'alwaysApply: true\n' |
|
else: |
|
mdc += 'alwaysApply: false\n' |
|
|
|
mdc += '---\n\n' |
|
mdc += content + '\n' |
|
|
|
|
|
if referenced_files: |
|
mdc += '\n' + referenced_files |
|
|
|
return mdc |
|
|
|
|
|
class LLMAdapterFactory: |
|
"""Factory for creating LLM adapters.""" |
|
|
|
@staticmethod |
|
def create_adapter(provider_name: str) -> LLMAdapter: |
|
"""Create an adapter for the specified provider.""" |
|
provider_name = provider_name.lower() |
|
|
|
if provider_name == "gemini": |
|
return GeminiAdapter() |
|
elif provider_name == "openai": |
|
return OpenAIAdapter() |
|
elif provider_name == "openrouter": |
|
return OpenRouterAdapter() |
|
else: |
|
raise ValueError(f"Unsupported provider: {provider_name}") |
|
|
|
@staticmethod |
|
def get_supported_providers() -> Dict[str, str]: |
|
"""Get a dictionary of supported providers.""" |
|
return { |
|
"gemini": "Google Gemini", |
|
"openai": "OpenAI", |
|
"openrouter": "OpenRouter" |
|
} |
|
|
|
|
|
class RuleGenerator: |
|
"""Engine for generating Cursor Rules.""" |
|
|
|
def __init__(self): |
|
"""Initialize the rule generator.""" |
|
self.factory = LLMAdapterFactory() |
|
|
|
def create_rule( |
|
self, |
|
provider: str, |
|
model: str, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
api_key: str, |
|
parameters: Optional[Dict[str, Any]] = None |
|
) -> str: |
|
"""Create a Cursor Rule using the specified LLM provider.""" |
|
|
|
if parameters is None: |
|
parameters = {} |
|
|
|
try: |
|
|
|
adapter = self.factory.create_adapter(provider) |
|
adapter.initialize(api_key) |
|
|
|
|
|
rule = adapter.generate_rule(model, rule_type, description, content, parameters) |
|
|
|
return rule |
|
except Exception as e: |
|
print(f"Exception in create_rule: {str(e)}\n{traceback.format_exc()}") |
|
|
|
return self._create_basic_rule(rule_type, description, content, parameters) |
|
|
|
def _create_basic_rule( |
|
self, |
|
rule_type: str, |
|
description: str, |
|
content: str, |
|
parameters: Optional[Dict[str, Any]] = None |
|
) -> str: |
|
"""Create a basic rule in MDC format without using an LLM.""" |
|
|
|
if parameters is None: |
|
parameters = {} |
|
|
|
|
|
globs = parameters.get('globs', '') |
|
referenced_files = parameters.get('referenced_files', '') |
|
|
|
|
|
mdc = '---\n' |
|
mdc += f'description: {description}\n' |
|
|
|
if rule_type == 'Auto Attached' and globs: |
|
mdc += f'globs: {globs}\n' |
|
|
|
if rule_type == 'Always': |
|
mdc += 'alwaysApply: true\n' |
|
else: |
|
mdc += 'alwaysApply: false\n' |
|
|
|
mdc += '---\n\n' |
|
mdc += content + '\n' |
|
|
|
|
|
if referenced_files: |
|
mdc += '\n' + referenced_files |
|
|
|
return mdc |
|
|
|
def validate_rule_type(self, rule_type: str) -> bool: |
|
"""Validate if the rule type is supported.""" |
|
valid_types = ['Always', 'Auto Attached', 'Agent Requested', 'Manual'] |
|
return rule_type in valid_types |
|
|
|
def get_rule_types(self) -> List[Dict[str, str]]: |
|
"""Get a list of supported rule types.""" |
|
return [ |
|
{ |
|
'id': 'Always', |
|
'name': 'Always', |
|
'description': 'Always included in the model context' |
|
}, |
|
{ |
|
'id': 'Auto Attached', |
|
'name': 'Auto Attached', |
|
'description': 'Included when files matching glob patterns are referenced' |
|
}, |
|
{ |
|
'id': 'Agent Requested', |
|
'name': 'Agent Requested', |
|
'description': 'Rule is presented to the AI, which decides whether to include it' |
|
}, |
|
{ |
|
'id': 'Manual', |
|
'name': 'Manual', |
|
'description': 'Only included when explicitly referenced using @ruleName' |
|
} |
|
] |
|
|
|
|
|
rule_generator = RuleGenerator() |
|
factory = LLMAdapterFactory() |
|
|
|
|
|
providers = factory.get_supported_providers() |
|
provider_choices = list(providers.keys()) |
|
|
|
|
|
rule_types = rule_generator.get_rule_types() |
|
rule_type_choices = [rt['id'] for rt in rule_types] |
|
|
|
def validate_api_key(provider, api_key): |
|
"""Validate an API key for a specific provider. |
|
|
|
Args: |
|
provider: The LLM provider |
|
api_key: The API key to validate |
|
|
|
Returns: |
|
tuple: (success, message, model_names, model_ids) |
|
""" |
|
if not provider or not api_key: |
|
return False, "Lütfen bir sağlayıcı seçin ve API anahtarı girin.", [], [] |
|
|
|
try: |
|
|
|
adapter = factory.create_adapter(provider) |
|
|
|
|
|
print(f"Validating {provider} API key: {api_key[:5]}...{api_key[-5:] if len(api_key) > 10 else ''}") |
|
|
|
|
|
valid = adapter.validate_api_key(api_key) |
|
|
|
if valid: |
|
|
|
adapter.initialize(api_key) |
|
|
|
|
|
models = adapter.get_available_models() |
|
model_names = [model['name'] for model in models] |
|
model_ids = [model['id'] for model in models] |
|
|
|
print(f"Models found: {model_names}") |
|
print(f"Model IDs: {model_ids}") |
|
|
|
|
|
if not model_names or not model_ids: |
|
if provider == "gemini": |
|
model_names = ["Gemini 2.5 Pro", "Gemini 2.0 Flash", "Gemini 2.0 Flash-Lite"] |
|
model_ids = ["gemini-2.5-pro", "gemini-2.0-flash", "gemini-2.0-flash-lite"] |
|
elif provider == "openai": |
|
model_names = ["GPT-4o", "GPT-4 Turbo", "GPT-3.5 Turbo"] |
|
model_ids = ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"] |
|
elif provider == "openrouter": |
|
model_names = ["OpenAI GPT-4o", "Anthropic Claude 3 Opus", "Google Gemini 2.5 Pro"] |
|
model_ids = ["openai/gpt-4o", "anthropic/claude-3-opus", "google/gemini-2.5-pro"] |
|
|
|
print(f"Using default models: {model_names}") |
|
|
|
return True, "API anahtarı doğrulandı.", model_names, model_ids |
|
else: |
|
error_msg = getattr(adapter, 'last_error', 'Bilinmeyen hata') |
|
return False, f"Geçersiz API anahtarı. Hata: {error_msg}", [], [] |
|
except Exception as e: |
|
error_details = traceback.format_exc() |
|
print(f"Exception in validate_api_key: {str(e)}\n{error_details}") |
|
return False, f"Hata: {str(e)}", [], [] |
|
|
|
def generate_rule(provider, api_key, model_index, model_ids, rule_type, description, content, globs, referenced_files, prompt, temperature): |
|
"""Generate a Cursor Rule. |
|
|
|
Args: |
|
provider: The LLM provider |
|
api_key: The API key for the provider |
|
model_index: The index of the selected model |
|
model_ids: The list of model IDs |
|
rule_type: The type of rule to generate |
|
description: A short description of the rule's purpose |
|
content: The main content of the rule |
|
globs: Glob patterns for Auto Attached rules |
|
referenced_files: Referenced files |
|
prompt: Additional instructions for the LLM |
|
temperature: Temperature parameter for generation |
|
|
|
Returns: |
|
tuple: (success, message, rule) |
|
""" |
|
print(f"Generate rule called with model_index: {model_index}, model_ids: {model_ids}") |
|
|
|
if not provider or not api_key: |
|
return False, "Lütfen bir sağlayıcı seçin ve API anahtarı girin.", "" |
|
|
|
if model_index is None or model_index == "": |
|
return False, "Lütfen bir model seçin. Model seçimi yapılamıyorsa, API anahtarını tekrar doğrulayın.", "" |
|
|
|
if not rule_type or not description or not content: |
|
return False, "Lütfen kural tipi, açıklama ve içerik alanlarını doldurun.", "" |
|
|
|
|
|
try: |
|
if isinstance(model_index, str) and model_index.isdigit(): |
|
model_index = int(model_index) |
|
except: |
|
pass |
|
|
|
|
|
if not model_ids: |
|
return False, "Model listesi bulunamadı. Lütfen API anahtarını tekrar doğrulayın.", "" |
|
|
|
try: |
|
model_index = int(model_index) |
|
except: |
|
return False, f"Geçersiz model indeksi: {model_index}", "" |
|
|
|
if model_index < 0 or model_index >= len(model_ids): |
|
return False, f"Geçersiz model seçimi. İndeks: {model_index}, Mevcut modeller: {len(model_ids)}", "" |
|
|
|
model = model_ids[model_index] |
|
|
|
|
|
if not rule_generator.validate_rule_type(rule_type): |
|
return False, f"Geçersiz kural tipi: {rule_type}", "" |
|
|
|
|
|
if rule_type == 'Auto Attached' and not globs: |
|
return False, "Auto Attached kural tipi için glob desenleri gereklidir.", "" |
|
|
|
try: |
|
|
|
parameters = { |
|
'globs': globs, |
|
'referenced_files': referenced_files, |
|
'prompt': prompt, |
|
'temperature': float(temperature) if temperature else 0.7 |
|
} |
|
|
|
|
|
rule = rule_generator.create_rule( |
|
provider=provider, |
|
model=model, |
|
rule_type=rule_type, |
|
description=description, |
|
content=content, |
|
api_key=api_key, |
|
parameters=parameters |
|
) |
|
|
|
return True, "Kural başarıyla oluşturuldu.", rule |
|
except Exception as e: |
|
error_details = traceback.format_exc() |
|
print(f"Exception in generate_rule: {str(e)}\n{error_details}") |
|
return False, f"Kural oluşturulurken bir hata oluştu: {str(e)}", "" |
|
|
|
def update_rule_type_info(rule_type): |
|
"""Update the rule type information. |
|
|
|
Args: |
|
rule_type: The selected rule type |
|
|
|
Returns: |
|
str: Information about the selected rule type |
|
""" |
|
if rule_type == 'Always': |
|
return "Her zaman model bağlamına dahil edilir." |
|
elif rule_type == 'Auto Attached': |
|
return "Glob desenine uyan dosyalar referans alındığında dahil edilir." |
|
elif rule_type == 'Agent Requested': |
|
return "Kural AI'ya sunulur, dahil edilip edilmeyeceğine AI karar verir." |
|
elif rule_type == 'Manual': |
|
return "Yalnızca @ruleName kullanılarak açıkça belirtildiğinde dahil edilir." |
|
else: |
|
return "" |
|
|
|
def update_globs_visibility(rule_type): |
|
"""Update the visibility of the globs input. |
|
|
|
Args: |
|
rule_type: The selected rule type |
|
|
|
Returns: |
|
bool: Whether the globs input should be visible |
|
""" |
|
return rule_type == 'Auto Attached' |
|
|
|
|
|
with gr.Blocks(title="Cursor Rules Oluşturucu") as demo: |
|
gr.Markdown("# Cursor Rules Oluşturucu") |
|
gr.Markdown("Gemini, OpenRouter, OpenAI API ve tüm modellerini destekleyen dinamik bir Cursor Rules oluşturucu.") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
provider = gr.Dropdown( |
|
choices=provider_choices, |
|
label="LLM Sağlayıcı", |
|
value=provider_choices[0] if provider_choices else None |
|
) |
|
|
|
api_key = gr.Textbox( |
|
label="API Anahtarı", |
|
placeholder="API anahtarınızı girin", |
|
type="password" |
|
) |
|
|
|
validate_btn = gr.Button("API Anahtarını Doğrula") |
|
|
|
api_status = gr.Textbox( |
|
label="API Durumu", |
|
interactive=False |
|
) |
|
|
|
|
|
default_models = { |
|
"gemini": ["Gemini 2.5 Pro", "Gemini 2.0 Flash", "Gemini 2.0 Flash-Lite"], |
|
"openai": ["GPT-4o", "GPT-4 Turbo", "GPT-3.5 Turbo"], |
|
"openrouter": ["OpenAI GPT-4o", "Anthropic Claude 3 Opus", "Google Gemini 2.5 Pro"] |
|
} |
|
|
|
model_dropdown = gr.Dropdown( |
|
label="Model", |
|
choices=default_models.get(provider_choices[0] if provider_choices else "gemini", []), |
|
interactive=True |
|
) |
|
|
|
|
|
model_ids = gr.State([]) |
|
|
|
rule_type = gr.Dropdown( |
|
choices=rule_type_choices, |
|
label="Kural Tipi", |
|
value=rule_type_choices[0] if rule_type_choices else None |
|
) |
|
|
|
rule_type_info = gr.Textbox( |
|
label="Kural Tipi Bilgisi", |
|
interactive=False, |
|
value=update_rule_type_info(rule_type_choices[0] if rule_type_choices else "") |
|
) |
|
|
|
description = gr.Textbox( |
|
label="Açıklama", |
|
placeholder="Kuralın amacını açıklayan kısa bir açıklama" |
|
) |
|
|
|
globs = gr.Textbox( |
|
label="Glob Desenleri (Auto Attached için)", |
|
placeholder="Örn: *.ts, src/*.js", |
|
visible=False |
|
) |
|
|
|
content = gr.Textbox( |
|
label="Kural İçeriği", |
|
placeholder="Kuralın ana içeriği", |
|
lines=10 |
|
) |
|
|
|
referenced_files = gr.Textbox( |
|
label="Referans Dosyaları (İsteğe bağlı)", |
|
placeholder="Her satıra bir dosya adı girin, örn: @service-template.ts", |
|
lines=3 |
|
) |
|
|
|
prompt = gr.Textbox( |
|
label="AI Prompt (İsteğe bağlı)", |
|
placeholder="AI'ya özel talimatlar verin", |
|
lines=3 |
|
) |
|
|
|
temperature = gr.Slider( |
|
label="Sıcaklık", |
|
minimum=0.0, |
|
maximum=1.0, |
|
value=0.7, |
|
step=0.1 |
|
) |
|
|
|
generate_btn = gr.Button("Kural Oluştur") |
|
|
|
with gr.Column(): |
|
generation_status = gr.Textbox( |
|
label="Durum", |
|
interactive=False |
|
) |
|
|
|
rule_output = gr.Textbox( |
|
label="Oluşturulan Kural", |
|
lines=20, |
|
interactive=False |
|
) |
|
|
|
download_btn = gr.Button("İndir") |
|
|
|
|
|
def update_default_models(provider_value): |
|
if provider_value == "gemini": |
|
return gr.Dropdown.update(choices=default_models["gemini"], value=default_models["gemini"][0] if default_models["gemini"] else None) |
|
elif provider_value == "openai": |
|
return gr.Dropdown.update(choices=default_models["openai"], value=default_models["openai"][0] if default_models["openai"] else None) |
|
elif provider_value == "openrouter": |
|
return gr.Dropdown.update(choices=default_models["openrouter"], value=default_models["openrouter"][0] if default_models["openrouter"] else None) |
|
else: |
|
return gr.Dropdown.update(choices=[], value=None) |
|
|
|
provider.change( |
|
fn=update_default_models, |
|
inputs=[provider], |
|
outputs=[model_dropdown] |
|
) |
|
|
|
|
|
validate_btn.click( |
|
fn=validate_api_key, |
|
inputs=[provider, api_key], |
|
outputs=[api_status, model_dropdown, model_ids] |
|
) |
|
|
|
|
|
rule_type.change( |
|
fn=update_rule_type_info, |
|
inputs=[rule_type], |
|
outputs=[rule_type_info] |
|
) |
|
|
|
rule_type.change( |
|
fn=update_globs_visibility, |
|
inputs=[rule_type], |
|
outputs=[globs] |
|
) |
|
|
|
|
|
generate_btn.click( |
|
fn=generate_rule, |
|
inputs=[ |
|
provider, |
|
api_key, |
|
model_dropdown, |
|
model_ids, |
|
rule_type, |
|
description, |
|
content, |
|
globs, |
|
referenced_files, |
|
prompt, |
|
temperature |
|
], |
|
outputs=[generation_status, rule_output] |
|
) |
|
|
|
|
|
def download_rule(rule, description): |
|
if not rule: |
|
return None |
|
|
|
|
|
file_name = description.lower().replace(" ", "-").replace("/", "-") |
|
if not file_name: |
|
file_name = "cursor-rule" |
|
|
|
return { |
|
"name": f"{file_name}.mdc", |
|
"data": rule |
|
} |
|
|
|
download_btn.click( |
|
fn=download_rule, |
|
inputs=[rule_output, description], |
|
outputs=[gr.File()] |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch( |
|
server_name="0.0.0.0", |
|
server_port=int(os.environ.get("PORT", 7860)), |
|
share=True |
|
) |
|
|