From 662e6a6e0f7e5544648b64a7eb1d1e4ee68a0d3c Mon Sep 17 00:00:00 2001 From: mirivlad Date: Sun, 10 May 2026 23:48:13 +0800 Subject: [PATCH] Harden local intent routing --- app/core/async_router.py | 45 +++++++++++++++++++++---------- app/core/intent_parser.py | 24 +++++++++++++++++ app/runtime/runtime_controller.py | 2 ++ 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/app/core/async_router.py b/app/core/async_router.py index 1e63ae2..11e1214 100644 --- a/app/core/async_router.py +++ b/app/core/async_router.py @@ -104,6 +104,16 @@ class AsyncRouter: reason="Task context explicitly requested a tool execution.", ) + parsed_intent = self._intent_parser.parse(task_summary) + if parsed_intent: + self._emit_event( + ORCHESTRATOR_RESULT, + {"reason": "deterministic_intent_parser", "directive": parsed_intent.model_dump(mode="json")}, + task_id, + session_id, + ) + return parsed_intent + if self._thinker is None: fallback = self._fallback_directive(task_summary) self._emit_event( @@ -365,20 +375,10 @@ class AsyncRouter: try: result = await classifier_model.generate(classification_prompt) - result = result.strip().lower() - - # Extract first word - LLM often adds explanation - first_word = result.split()[0] if result.split() else "" - - # Validate result is one of allowed values - allowed = {"execution", "conversation", "clarification_needed"} - if first_word in allowed: - logger.info(f"Intent classified: {first_word} for task: {task_summary}") - return first_word - - if result in allowed: - logger.info(f"Intent classified: {result} for task: {task_summary}") - return result + classification = self._extract_classification(result) + if classification: + logger.info(f"Intent classified: {classification} for task: {task_summary}") + return classification logger.warning(f"Invalid classification result: {result}, defaulting to conversation") return "conversation" @@ -386,6 +386,23 @@ class AsyncRouter: logger.warning(f"Intent classification failed: {e}, defaulting to conversation") return "conversation" + def _extract_classification(self, raw_result: str) -> str | None: + result = raw_result.strip().lower() + allowed = {"execution", "conversation", "clarification_needed"} + if result in allowed: + return result + + result = re.sub(r".*?", " ", result, flags=re.DOTALL) + tokens = re.findall(r"\b(execution|conversation|clarification_needed)\b", result) + if tokens: + return tokens[-1] + + first_word = result.split()[0] if result.split() else "" + if first_word in allowed: + return first_word + + return None + def _validate_directive(self, output: str, mode_hint: str) -> ExecutionDirective | None: if not output: return None diff --git a/app/core/intent_parser.py b/app/core/intent_parser.py index 1217e85..a5af87a 100644 --- a/app/core/intent_parser.py +++ b/app/core/intent_parser.py @@ -29,6 +29,17 @@ MEMORY_SEARCH_PATTERNS = ( r"find\s+(.+)", ) +SYSTEM_COMMAND_PATTERNS = ( + ( + re.compile(r"(сколько\s+времени\s+запущен|как\s+долго\s+работает|uptime|аптайм)", re.IGNORECASE), + "uptime -p", + ), + ( + re.compile(r"(проверь|посмотри|покажи).*(обновлен|обновл|updates|upgradable)", re.IGNORECASE), + "apt list --upgradable", + ), +) + class IntentParser: """Extracts explicit tool intents from natural-language task text.""" @@ -69,6 +80,19 @@ class IntentParser: reason="User explicitly requested to search memory.", ) + for pattern, command in SYSTEM_COMMAND_PATTERNS: + if pattern.search(normalized): + return ExecutionDirective( + type="tool", + payload={ + "tool": "shell_exec", + "args": {"command": command}, + }, + requires_permission=True, + confidence=0.9, + reason="User explicitly requested local system information.", + ) + for prefix in SHELL_PREFIXES: if lowered.startswith(prefix): command = normalized[len(prefix) :].strip() diff --git a/app/runtime/runtime_controller.py b/app/runtime/runtime_controller.py index 36e6325..72cb18e 100644 --- a/app/runtime/runtime_controller.py +++ b/app/runtime/runtime_controller.py @@ -270,6 +270,8 @@ class RuntimeController: try: emb_config = self.config.models.embeddings or {} model_path = self.base_dir / emb_config.get("path", "models/all-MiniLM-L6-v2") + if not model_path.exists() and not Path(emb_config.get("path", "")).is_absolute(): + model_path = self.base_dir / "models" / emb_config.get("path", "all-MiniLM-L6-v2") if not model_path.exists(): print(f"Memory init skipped: embeddings model not found at {model_path}") self._memory_interface = None