diff --git a/ai_software_factory/agents/request_interpreter.py b/ai_software_factory/agents/request_interpreter.py index 6af1d44..525a009 100644 --- a/ai_software_factory/agents/request_interpreter.py +++ b/ai_software_factory/agents/request_interpreter.py @@ -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]: diff --git a/ai_software_factory/main.py b/ai_software_factory/main.py index 597cd0b..d392d71 100644 --- a/ai_software_factory/main.py +++ b/ai_software_factory/main.py @@ -798,6 +798,7 @@ def get_llm_prompt_settings(db: DbSession): @app.put('/llm/prompts/{prompt_key}') def update_llm_prompt_setting(prompt_key: str, request: LLMPromptSettingUpdateRequest, db: DbSession): """Persist one editable LLM prompt override into the database.""" + database_module.init_db() result = DatabaseManager(db).save_llm_prompt_setting(prompt_key, request.value, actor='api') if result.get('status') == 'error': raise HTTPException(status_code=400, detail=result.get('message', 'Prompt save failed')) @@ -807,6 +808,7 @@ def update_llm_prompt_setting(prompt_key: str, request: LLMPromptSettingUpdateRe @app.delete('/llm/prompts/{prompt_key}') def reset_llm_prompt_setting(prompt_key: str, db: DbSession): """Reset one editable LLM prompt override back to the environment/default value.""" + database_module.init_db() result = DatabaseManager(db).reset_llm_prompt_setting(prompt_key, actor='api') if result.get('status') == 'error': raise HTTPException(status_code=400, detail=result.get('message', 'Prompt reset failed'))