From 39f9651236e905c34dbef96425c62b807fbd99da Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Sat, 11 Apr 2026 11:53:18 +0200 Subject: [PATCH] fix: UI improvements and prompt hardening, refs NOISSUE --- .../agents/request_interpreter.py | 58 ++++++++++++++++-- ai_software_factory/config.py | 4 +- ai_software_factory/dashboard_ui.py | 60 +++++++++++++++++-- 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/ai_software_factory/agents/request_interpreter.py b/ai_software_factory/agents/request_interpreter.py index 525a009..2046507 100644 --- a/ai_software_factory/agents/request_interpreter.py +++ b/ai_software_factory/agents/request_interpreter.py @@ -25,6 +25,12 @@ class RequestInterpreter: } REPO_NOISE_WORDS = REQUEST_PREFIX_WORDS | {'and', 'from', 'into', 'on', 'that', 'this', 'to'} + GENERIC_PROJECT_NAME_WORDS = { + 'app', 'application', 'harness', 'platform', 'project', 'purpose', 'service', 'solution', 'suite', 'system', 'test', 'tool', + } + PLACEHOLDER_PROJECT_NAME_WORDS = { + 'generated project', 'new project', 'project', 'temporary name', 'temp name', 'placeholder', 'untitled project', + } def __init__(self, ollama_url: str | None = None, model: str | None = None): self.ollama_url = (ollama_url or settings.ollama_url).rstrip('/') @@ -153,10 +159,11 @@ class RequestInterpreter: ) if content: try: + fallback_name = self._preferred_project_name_fallback(prompt_text, interpreted.get('name')) parsed = json.loads(content) project_name, repo_name = self._normalize_project_identity( parsed, - fallback_name=interpreted.get('name') or self._derive_name(prompt_text), + fallback_name=fallback_name, ) repo_name = self._ensure_unique_repo_name(repo_name, constraints['repo_names']) interpreted['name'] = project_name @@ -166,7 +173,7 @@ class RequestInterpreter: except Exception: pass - fallback_name = interpreted.get('name') or self._derive_name(prompt_text) + fallback_name = self._preferred_project_name_fallback(prompt_text, interpreted.get('name')) routing['project_name'] = fallback_name routing['repo_name'] = self._ensure_unique_repo_name(self._derive_repo_name(fallback_name), constraints['repo_names']) return interpreted, routing, trace @@ -288,13 +295,22 @@ class RequestInterpreter: noun_phrase = re.search( r'(?:build|create|start|make|develop|generate|design|need|want)\s+' r'(?:me\s+|us\s+|an?\s+|the\s+|new\s+|internal\s+|simple\s+|lightweight\s+|modern\s+|web\s+|mobile\s+)*' - r'([a-z0-9][a-z0-9\s-]{2,80}?(?:portal|dashboard|app|application|service|tool|system|platform|api|bot|assistant|website|site|workspace|tracker|manager))\b', + r'([a-z0-9][a-z0-9\s-]{2,80}?(?:portal|dashboard|app|application|service|tool|system|platform|api|bot|assistant|website|site|workspace|tracker|manager|harness|runner|framework|suite|pipeline|lab))\b', first_line, flags=re.IGNORECASE, ) if noun_phrase: return self._humanize_name(noun_phrase.group(1)) + focused_phrase = re.search( + r'(?:purpose\s+is\s+to\s+create\s+(?:an?\s+)?)' + r'([a-z0-9][a-z0-9\s-]{2,80}?(?:portal|dashboard|app|application|service|tool|system|platform|api|bot|assistant|website|site|workspace|tracker|manager|harness|runner|framework|suite|pipeline|lab))\b', + first_line, + flags=re.IGNORECASE, + ) + if focused_phrase: + return self._humanize_name(focused_phrase.group(1)) + cleaned = re.sub(r'[^A-Za-z0-9 ]+', ' ', first_line) stopwords = { 'build', 'create', 'start', 'make', 'develop', 'generate', 'design', 'need', 'want', 'please', 'for', 'our', 'with', 'that', 'this', @@ -360,6 +376,36 @@ class RequestInterpreter: return False return True + def _should_use_project_name_candidate(self, candidate: str, fallback_name: str) -> bool: + """Return whether a model-proposed project title is concrete enough to trust.""" + 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) == 1 and candidate_tokens[0] in self.GENERIC_PROJECT_NAME_WORDS: + return False + if all(token in self.GENERIC_PROJECT_NAME_WORDS for token in candidate_tokens): + return False + fallback_tokens = { + token.lower() for token in re.split(r'[-\s]+', fallback_name or '') if token and token.lower() not in self.REPO_NOISE_WORDS + } + if fallback_tokens and len(candidate_tokens) <= 2: + overlap = sum(1 for token in candidate_tokens if token in fallback_tokens) + if overlap == 0 and any(token in self.GENERIC_PROJECT_NAME_WORDS for token in candidate_tokens): + return False + return True + + def _preferred_project_name_fallback(self, prompt_text: str, interpreted_name: str | None) -> str: + """Pick the best fallback title when the earlier interpretation produced a placeholder.""" + interpreted_clean = self._humanize_name(str(interpreted_name or '').strip()) if interpreted_name else '' + normalized_interpreted = interpreted_clean.lower() + if normalized_interpreted and normalized_interpreted not in self.PLACEHOLDER_PROJECT_NAME_WORDS: + if not (len(normalized_interpreted.split()) == 1 and normalized_interpreted in self.GENERIC_PROJECT_NAME_WORDS): + return interpreted_clean + return self._derive_name(prompt_text) + 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) @@ -372,7 +418,11 @@ 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)) + fallback_project_name = self._humanize_name(str(fallback_name or 'Generated Project')) + project_candidate = str(payload.get('project_name') or payload.get('name') or '').strip() + project_name = fallback_project_name + if project_candidate and self._should_use_project_name_candidate(project_candidate, fallback_project_name): + project_name = self._humanize_name(project_candidate) 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): diff --git a/ai_software_factory/config.py b/ai_software_factory/config.py index 619dd19..f14e515 100644 --- a/ai_software_factory/config.py +++ b/ai_software_factory/config.py @@ -120,10 +120,10 @@ class Settings(BaseSettings): "For summaries: only describe facts present in the provided context and tool outputs. Never claim a repository, commit, or pull request exists unless it is present in the supplied data." ) LLM_PROJECT_NAMING_GUARDRAIL_PROMPT: str = ( - "For project naming: prefer clear, product-like names and repository slugs that match the user's intent. Avoid reusing tracked project identities unless the request is clearly asking for an existing project." + "For project naming: prefer clear, product-like names and repository slugs that match the user's concrete deliverable. Avoid abstract or instructional words such as purpose, project, system, app, tool, platform, solution, new, create, or test unless the request truly centers on that exact noun. Base the name on the actual artifact or workflow being built, and avoid copying sentence fragments from the prompt. Avoid reusing tracked project identities unless the request is clearly asking for an existing project." ) LLM_PROJECT_NAMING_SYSTEM_PROMPT: str = ( - "You name newly requested software projects. Return only JSON with keys project_name, repo_name, and rationale. Project names should be concise human-readable titles. Repo names should be lowercase kebab-case slugs suitable for a Gitea repository name." + "You name newly requested software projects. Return only JSON with keys project_name, repo_name, and rationale. Project names should be concise human-readable titles based on the real product, artifact, or workflow being created. Repo names should be lowercase kebab-case slugs derived from that title. Never return generic names like purpose, project, system, app, tool, platform, solution, harness, or test by themselves, and never return a repo_name that is a copied sentence fragment from the prompt. Prefer 2 to 4 specific words when possible." ) LLM_PROJECT_ID_GUARDRAIL_PROMPT: str = ( "For project ids: produce short stable slugs for newly created projects. Avoid collisions with known project ids and keep ids lowercase with hyphens." diff --git a/ai_software_factory/dashboard_ui.py b/ai_software_factory/dashboard_ui.py index 5b17f74..62f1522 100644 --- a/ai_software_factory/dashboard_ui.py +++ b/ai_software_factory/dashboard_ui.py @@ -665,6 +665,24 @@ def _add_dashboard_styles() -> None: """Register shared dashboard styles.""" ui.add_head_html( """ +