From 960f08352b51bf6338402362834139a1abd9d4ea Mon Sep 17 00:00:00 2001 From: saschabuehrle Date: Wed, 1 Jul 2026 12:04:45 +0200 Subject: [PATCH 1/2] fix(tests): add future annotations import for py3.9 union syntax tests/test_provider_tool_retry.py used 'str | None' in method signatures, which Python 3.9 evaluates at definition time and rejects with 'TypeError: unsupported operand type(s) for |'. This broke test collection on the 3.9 CI matrix leg. Add 'from __future__ import annotations' to defer annotation evaluation, matching the rest of the suite. --- tests/test_provider_tool_retry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_provider_tool_retry.py b/tests/test_provider_tool_retry.py index 23b1107d..449307d7 100644 --- a/tests/test_provider_tool_retry.py +++ b/tests/test_provider_tool_retry.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio from cascadeflow.providers.base import BaseProvider, ModelResponse, RetryConfig From 9a58373dca0e05582c9f4bc46cf196d9cfb84de4 Mon Sep 17 00:00:00 2001 From: saschabuehrle Date: Wed, 1 Jul 2026 12:04:45 +0200 Subject: [PATCH 2/2] fix(hermes): match domain keywords on word boundaries The classifier scored domains with naive substring matching, so short keywords matched inside unrelated words -- e.g. 'ci' (ops) matched inside 'financial', tying with the finance hit and winning on dict order. This made test_high_stakes_domains_are_detected fail nondeterministically depending on PYTHONHASHSEED (which keyword next(iter(set)) picked). Use word-boundary regex matching so 'ci'/'api'/'law' no longer match inside 'financial'/'always'; multi-word keywords still match. The classifier is now correct and the test deterministic across seeds. --- cascadeflow/integrations/hermes/classifier.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cascadeflow/integrations/hermes/classifier.py b/cascadeflow/integrations/hermes/classifier.py index eb5f15ac..5852f216 100644 --- a/cascadeflow/integrations/hermes/classifier.py +++ b/cascadeflow/integrations/hermes/classifier.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from dataclasses import dataclass from typing import Optional @@ -122,10 +123,17 @@ def classify(self, request: HermesDelegationRequest) -> HermesTaskClassification reason=domain_reason, ) + @staticmethod + def _keyword_in_text(keyword: str, lowered: str) -> bool: + # Match on word boundaries so short keywords (e.g. "ci", "api", "law") + # don't match inside unrelated words like "financial" ("ci") or "always" + # ("law"). Handles multi-word keywords ("stack trace") correctly too. + return re.search(rf"\b{re.escape(keyword)}\b", lowered) is not None + def _detect_domain(self, lowered: str, toolsets: tuple[str, ...]) -> tuple[str, float, str]: scores: dict[str, int] = {} for domain, keywords in DOMAIN_KEYWORDS.items(): - hits = sum(1 for keyword in keywords if keyword in lowered) + hits = sum(1 for keyword in keywords if self._keyword_in_text(keyword, lowered)) if hits: scores[domain] = hits