|
|
|
|
@@ -18,6 +18,14 @@ except ImportError:
|
|
|
|
|
class RequestInterpreter:
|
|
|
|
|
"""Use Ollama to turn free-form text into a structured software request."""
|
|
|
|
|
|
|
|
|
|
REQUEST_PREFIX_WORDS = {
|
|
|
|
|
'a', 'an', 'app', 'application', 'build', 'create', 'dashboard', 'develop', 'design', 'for', 'generate',
|
|
|
|
|
'internal', 'make', 'me', 'modern', 'need', 'new', 'our', 'platform', 'please', 'project', 'service',
|
|
|
|
|
'simple', 'site', 'start', 'system', 'the', 'tool', 'us', 'want', 'web', 'website', 'with',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
REPO_NOISE_WORDS = REQUEST_PREFIX_WORDS | {'and', 'from', 'into', 'on', 'that', 'this', 'to'}
|
|
|
|
|
|
|
|
|
|
def __init__(self, ollama_url: str | None = None, model: str | None = None):
|
|
|
|
|
self.ollama_url = (ollama_url or settings.ollama_url).rstrip('/')
|
|
|
|
|
self.model = model or settings.OLLAMA_MODEL
|
|
|
|
|
@@ -301,6 +309,7 @@ class RequestInterpreter:
|
|
|
|
|
"""Normalize a candidate project name into a readable title."""
|
|
|
|
|
cleaned = re.sub(r'[^A-Za-z0-9\s-]+', ' ', raw_name).strip(' -')
|
|
|
|
|
cleaned = re.sub(r'\s+', ' ', cleaned)
|
|
|
|
|
cleaned = self._trim_request_prefix(cleaned)
|
|
|
|
|
special_upper = {'api', 'crm', 'erp', 'cms', 'hr', 'it', 'ui', 'qa'}
|
|
|
|
|
words = []
|
|
|
|
|
for word in cleaned.split()[:6]:
|
|
|
|
|
@@ -308,14 +317,49 @@ class RequestInterpreter:
|
|
|
|
|
words.append(lowered.upper() if lowered in special_upper else lowered.capitalize())
|
|
|
|
|
return ' '.join(words) or 'Generated Project'
|
|
|
|
|
|
|
|
|
|
def _trim_request_prefix(self, candidate: str) -> str:
|
|
|
|
|
"""Remove leading request phrasing from model-produced names and slugs."""
|
|
|
|
|
tokens = [token for token in re.split(r'[-\s]+', candidate or '') if token]
|
|
|
|
|
while tokens and tokens[0].lower() in self.REQUEST_PREFIX_WORDS:
|
|
|
|
|
tokens.pop(0)
|
|
|
|
|
trimmed = ' '.join(tokens).strip()
|
|
|
|
|
return trimmed or candidate.strip()
|
|
|
|
|
|
|
|
|
|
def _derive_repo_name(self, project_name: str) -> str:
|
|
|
|
|
"""Derive a repository slug from a human-readable project name."""
|
|
|
|
|
preferred = (project_name or 'project').strip().lower().replace(' ', '-')
|
|
|
|
|
preferred_name = self._trim_request_prefix((project_name or 'project').strip())
|
|
|
|
|
preferred = preferred_name.lower().replace(' ', '-')
|
|
|
|
|
sanitized = ''.join(ch if ch.isalnum() or ch in {'-', '_'} else '-' for ch in preferred)
|
|
|
|
|
while '--' in sanitized:
|
|
|
|
|
sanitized = sanitized.replace('--', '-')
|
|
|
|
|
return sanitized.strip('-') or 'project'
|
|
|
|
|
|
|
|
|
|
def _should_use_repo_name_candidate(self, candidate: str, project_name: str) -> bool:
|
|
|
|
|
"""Return whether a model-proposed repo slug is concise enough to trust directly."""
|
|
|
|
|
cleaned = self._trim_request_prefix(re.sub(r'[^A-Za-z0-9\s_-]+', ' ', candidate or '').strip())
|
|
|
|
|
if not cleaned:
|
|
|
|
|
return False
|
|
|
|
|
candidate_tokens = [token.lower() for token in re.split(r'[-\s_]+', cleaned) if token]
|
|
|
|
|
if not candidate_tokens:
|
|
|
|
|
return False
|
|
|
|
|
if len(candidate_tokens) > 6:
|
|
|
|
|
return False
|
|
|
|
|
noise_count = sum(1 for token in candidate_tokens if token in self.REPO_NOISE_WORDS)
|
|
|
|
|
if noise_count >= 2:
|
|
|
|
|
return False
|
|
|
|
|
if len('-'.join(candidate_tokens)) > 40:
|
|
|
|
|
return False
|
|
|
|
|
project_tokens = {
|
|
|
|
|
token.lower()
|
|
|
|
|
for token in re.split(r'[-\s_]+', project_name or '')
|
|
|
|
|
if token and token.lower() not in self.REPO_NOISE_WORDS
|
|
|
|
|
}
|
|
|
|
|
if project_tokens:
|
|
|
|
|
overlap = sum(1 for token in candidate_tokens if token in project_tokens)
|
|
|
|
|
if overlap == 0:
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _ensure_unique_repo_name(self, repo_name: str, reserved_names: set[str]) -> str:
|
|
|
|
|
"""Choose a repository slug that does not collide with tracked or remote repositories."""
|
|
|
|
|
base_name = self._derive_repo_name(repo_name)
|
|
|
|
|
@@ -329,7 +373,10 @@ class RequestInterpreter:
|
|
|
|
|
def _normalize_project_identity(self, payload: dict, fallback_name: str) -> tuple[str, str]:
|
|
|
|
|
"""Normalize model-proposed project and repository naming."""
|
|
|
|
|
project_name = self._humanize_name(str(payload.get('project_name') or payload.get('name') or fallback_name))
|
|
|
|
|
repo_name = self._derive_repo_name(str(payload.get('repo_name') or project_name))
|
|
|
|
|
repo_candidate = str(payload.get('repo_name') or '').strip()
|
|
|
|
|
repo_name = self._derive_repo_name(project_name)
|
|
|
|
|
if repo_candidate and self._should_use_repo_name_candidate(repo_candidate, project_name):
|
|
|
|
|
repo_name = self._derive_repo_name(repo_candidate)
|
|
|
|
|
return project_name, repo_name
|
|
|
|
|
|
|
|
|
|
def _heuristic_fallback(self, prompt_text: str, context: dict | None = None) -> tuple[dict, dict]:
|
|
|
|
|
|