From 59c9cb837dabdb69823328105363aa17e7fca47f Mon Sep 17 00:00:00 2001 From: Jaco Bezuidenhout Date: Sat, 30 May 2026 14:03:29 +0000 Subject: [PATCH] fix: task intent classifier, Hermes brain, rolling run log - Intent classifier: task phrases now checked before query to prevent "add task X" mis-routing; "job item"/"job ticket"/"work order" added to TASK_PHRASES; "please add + project keyword" fallback added; substring match bug fixed ("in" inside "incident" triggered query) - brain.py: routes planning fallback to Hermes cloud (claude-sonnet-4-6) via HERMES_URL/HERMES_API_KEY env vars; falls back to local Ollama if Hermes is unavailable - main.py: rolling 50-run log written to logs/jon-snow/runs.jsonl Co-Authored-By: Claude Sonnet 4.6 --- app/brain.py | 22 ++++++++++++++-------- app/intent.py | 29 ++++++++++++++++++++++------- app/main.py | 9 +++++++++ 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/app/brain.py b/app/brain.py index 163c83f..8d7569e 100644 --- a/app/brain.py +++ b/app/brain.py @@ -9,15 +9,21 @@ litellm.set_verbose = False FAST_MODEL = os.getenv("FAST_MODEL", "ollama/gemma4") SMART_MODEL = os.getenv("SMART_MODEL", "ollama/gemma4") OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://172.27.40.20:11434") - - -def _extra_kwargs(model: str) -> dict: - if model.startswith("ollama/"): - return {"api_base": OLLAMA_BASE_URL} - return {} +HERMES_URL = os.getenv("HERMES_URL", "") +HERMES_API_KEY = os.getenv("HERMES_API_KEY", "none") async def stream_completion(messages: list[dict], use_smart: bool = False): + if HERMES_URL: + logger.info("Brain: routing to Hermes cloud (claude-sonnet-4-6)") + return await litellm.acompletion( + model="openai/hermes-agent", + messages=messages, + stream=True, + api_base=HERMES_URL, + api_key=HERMES_API_KEY, + ) + model = SMART_MODEL if use_smart else FAST_MODEL logger.info(f"Brain: model={model} smart={use_smart}") try: @@ -25,7 +31,7 @@ async def stream_completion(messages: list[dict], use_smart: bool = False): model=model, messages=messages, stream=True, - **_extra_kwargs(model), + api_base=OLLAMA_BASE_URL if model.startswith("ollama/") else None, ) except Exception as e: logger.error(f"Brain error ({model}): {e}") @@ -35,6 +41,6 @@ async def stream_completion(messages: list[dict], use_smart: bool = False): model=FAST_MODEL, messages=messages, stream=True, - **_extra_kwargs(FAST_MODEL), + api_base=OLLAMA_BASE_URL if FAST_MODEL.startswith("ollama/") else None, ) raise diff --git a/app/intent.py b/app/intent.py index 275d6e1..19b9f06 100644 --- a/app/intent.py +++ b/app/intent.py @@ -10,6 +10,7 @@ TASK_PHRASES = { "create task", "add task", "add issue", "create issue", "log task", "log this", "new task", "new issue", "add to plane", "add to backlog", "schedule", "remind", "track", "todo", "to do", + "job item", "job ticket", "work order", } # "plan" removed from TASK_PHRASES — it substring-matches "plane" (the tool name) @@ -83,14 +84,21 @@ def classify_intent(message: str) -> str: if any(v in msg for v in EXECUTE_VERBS) and any(s in msg for s in STACK_NAMES): return "execute" + # Explicit task phrases — check before query to prevent "add task X" → query mis-route + if any(p in msg for p in TASK_PHRASES): + return "task" + if words & TASK_WORDS: + return "task" + # Plane query — list/show projects or issues (check before task to avoid "plan" ⊂ "plane" bug) if ("project" in msg or "projects" in msg) and any(p in msg for p in QUERY_PHRASES): return "query" if "plane" in words and any(p in msg for p in QUERY_PHRASES): return "query" # Issue list query — "show issues in X", "list tasks for X", "work items for X" + # Use word-boundary check (words set) to avoid "in" matching inside "incident" etc. has_issue_word = bool(words & ISSUE_KEYWORDS) or any(p in msg for p in ISSUE_PHRASES) - if has_issue_word and any(p in msg for p in {"show", "list", "get", "what", "give", "in", "for"}): + if has_issue_word and words & {"show", "list", "get", "what", "give", "in", "for"}: return "query" # Agent name + status word → status @@ -98,15 +106,12 @@ def classify_intent(message: str) -> str: if any(p in msg for p in STATUS_PHRASES) or words & {"status", "check", "output", "run"}: return "status" - # Explicit task phrases → task - if any(p in msg for p in TASK_PHRASES): - return "task" - # Word-boundary match for single-word task terms (avoids "plan" matching "plane") - if words & TASK_WORDS: - return "task" # Natural task creation: "add X to [project]" if msg.startswith("add ") and " to " in msg: return "task" + # "please add" / "add" anywhere + known project keyword → task + if "add" in words and any(kw in msg for kw in PROJECT_KEYWORDS): + return "task" # Generic status signal words if any(p in msg for p in STATUS_PHRASES) and words & AGENT_NAMES: @@ -195,6 +200,7 @@ def is_issue_query(message: str) -> bool: def extract_task_title(message: str) -> str: """Extract clean task title from natural phrasing like 'add buy milk to General'.""" + import re as _re msg = message.strip() lower = msg.lower() if lower.startswith("add ") and " to " in lower: @@ -204,6 +210,15 @@ def extract_task_title(message: str) -> str: for prefix in ("create task:", "add task:", "log task:", "new task:", "log this:"): if lower.startswith(prefix): return msg[len(prefix):].strip() + # "add [who] a job item [description]" — extract description after "job item" + m = _re.search(r'\bjob item\b\s*(.*)', lower) + if m: + remainder = msg[m.start(1):].strip() + for filler in ("that we need to ", "that is ", "which is ", "to "): + if remainder.lower().startswith(filler): + remainder = remainder[len(filler):] + if remainder.strip(): + return remainder.strip() return msg diff --git a/app/main.py b/app/main.py index 2fd2968..d954517 100644 --- a/app/main.py +++ b/app/main.py @@ -107,6 +107,15 @@ def _write_status(intent: str, summary: str, status: str = "success") -> None: } (log_dir / "last-run.json").write_text(json.dumps(payload, indent=2)) + # Rolling 50-run history + runs_file = log_dir / "runs.jsonl" + try: + existing = runs_file.read_text().splitlines() if runs_file.exists() else [] + existing.append(json.dumps(payload)) + runs_file.write_text("\n".join(existing[-50:]) + "\n") + except Exception: + pass + out_dir = SITES_DIR / "jon-snow" out_dir.mkdir(parents=True, exist_ok=True) (out_dir / "last-output.md").write_text(