Harden local intent routing

This commit is contained in:
mirivlad 2026-05-10 23:48:13 +08:00
parent dc8267880a
commit 662e6a6e0f
3 changed files with 57 additions and 14 deletions

View File

@ -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"<think>.*?</think>", " ", 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

View File

@ -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()

View File

@ -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