From 4a09def6dc9e7efe5fd0f45de85809680a0331c1 Mon Sep 17 00:00:00 2001 From: Jaco Bezuidenhout Date: Tue, 2 Jun 2026 06:09:45 +0000 Subject: [PATCH] fix: TransferEncodingError on task extraction failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - brain.py: unwrap JSON array responses from Claude (occasional array instead of object caused AttributeError → broken chunked stream) - main.py: wrap extract_task_fields() in try/except so any extraction failure gracefully falls back to regex — generator always completes - tools.py: _resolve_project() uses alphanumeric-stripped matching so "Generaladm" resolves to General/Admin via identifier prefix match. Word-overlap fallback added. Default changed from Agent Ecosystem to General/Admin (correct catch-all bucket) Co-Authored-By: Claude Sonnet 4.6 --- app/brain.py | 5 +++++ app/main.py | 8 ++++++-- app/tools.py | 25 ++++++++++++++++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/brain.py b/app/brain.py index fd52146..1038909 100644 --- a/app/brain.py +++ b/app/brain.py @@ -69,6 +69,11 @@ async def extract_task_fields(message: str) -> tuple[dict, dict]: if content.startswith("json"): content = content[4:] fields = json.loads(content.strip()) + # Claude occasionally wraps the object in an array — unwrap it + if isinstance(fields, list): + fields = fields[0] if fields else {} + if not isinstance(fields, dict): + fields = {} return fields, usage except Exception as e: logger.warning(f"extract_task_fields failed: {e}") diff --git a/app/main.py b/app/main.py index 0ab5904..111d3a8 100644 --- a/app/main.py +++ b/app/main.py @@ -686,8 +686,12 @@ async def chat_completions(req: ChatRequest): elif intent == "task": # LLM extraction — handles any natural language phrasing - fields, usage = await extract_task_fields(user_message) - log_usage("task_extract", usage["prompt_tokens"], usage["completion_tokens"]) + try: + fields, usage = await extract_task_fields(user_message) + log_usage("task_extract", usage["prompt_tokens"], usage["completion_tokens"]) + except Exception as e: + logger.warning(f"extraction failed, falling back to regex: {e}") + fields = {} title = fields.get("title") or extract_task_title(user_message) project_hint = fields.get("project") or extract_project_name(user_message) diff --git a/app/tools.py b/app/tools.py index eb253b6..8d88f27 100644 --- a/app/tools.py +++ b/app/tools.py @@ -147,14 +147,33 @@ async def get_plane_issues(project_hint: str) -> tuple[str, list[dict]]: def _resolve_project(projects: list[dict], hint: str | None) -> dict: + import re as _re if hint: hint_lower = hint.lower() + hint_alpha = _re.sub(r"[^a-z0-9]", "", hint_lower) # "generaladm" from "Generaladm" + for p in projects: - if hint_lower in p["name"].lower(): + name_lower = p["name"].lower() + ident_lower = p.get("identifier", "").lower() + name_alpha = _re.sub(r"[^a-z0-9]", "", name_lower) # "generaladmin" + # Exact or substring name match + if hint_lower in name_lower or name_lower in hint_lower: return p - # Default: Agent Ecosystem + # Identifier match (e.g. "generaladm" == "generaladm") + if hint_alpha == ident_lower or hint_alpha == _re.sub(r"[^a-z0-9]", "", ident_lower): + return p + # Stripped prefix match (e.g. "generaladm" starts "generaladmin") + if name_alpha.startswith(hint_alpha) or hint_alpha.startswith(name_alpha): + return p + # Word overlap fallback — any significant word of hint in project name + hint_words = {w for w in hint_lower.split() if len(w) > 2} + for p in projects: + if hint_words & set(p["name"].lower().split()): + return p + + # Default: General / Admin (catch-all bucket) for p in projects: - if "agent" in p["name"].lower(): + if "general" in p["name"].lower(): return p return projects[0]