Harden local intent routing
This commit is contained in:
parent
dc8267880a
commit
662e6a6e0f
|
|
@ -104,6 +104,16 @@ class AsyncRouter:
|
||||||
reason="Task context explicitly requested a tool execution.",
|
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:
|
if self._thinker is None:
|
||||||
fallback = self._fallback_directive(task_summary)
|
fallback = self._fallback_directive(task_summary)
|
||||||
self._emit_event(
|
self._emit_event(
|
||||||
|
|
@ -365,20 +375,10 @@ class AsyncRouter:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await classifier_model.generate(classification_prompt)
|
result = await classifier_model.generate(classification_prompt)
|
||||||
result = result.strip().lower()
|
classification = self._extract_classification(result)
|
||||||
|
if classification:
|
||||||
# Extract first word - LLM often adds explanation
|
logger.info(f"Intent classified: {classification} for task: {task_summary}")
|
||||||
first_word = result.split()[0] if result.split() else ""
|
return classification
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
logger.warning(f"Invalid classification result: {result}, defaulting to conversation")
|
logger.warning(f"Invalid classification result: {result}, defaulting to conversation")
|
||||||
return "conversation"
|
return "conversation"
|
||||||
|
|
@ -386,6 +386,23 @@ class AsyncRouter:
|
||||||
logger.warning(f"Intent classification failed: {e}, defaulting to conversation")
|
logger.warning(f"Intent classification failed: {e}, defaulting to conversation")
|
||||||
return "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:
|
def _validate_directive(self, output: str, mode_hint: str) -> ExecutionDirective | None:
|
||||||
if not output:
|
if not output:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,17 @@ MEMORY_SEARCH_PATTERNS = (
|
||||||
r"find\s+(.+)",
|
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:
|
class IntentParser:
|
||||||
"""Extracts explicit tool intents from natural-language task text."""
|
"""Extracts explicit tool intents from natural-language task text."""
|
||||||
|
|
@ -69,6 +80,19 @@ class IntentParser:
|
||||||
reason="User explicitly requested to search memory.",
|
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:
|
for prefix in SHELL_PREFIXES:
|
||||||
if lowered.startswith(prefix):
|
if lowered.startswith(prefix):
|
||||||
command = normalized[len(prefix) :].strip()
|
command = normalized[len(prefix) :].strip()
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,8 @@ class RuntimeController:
|
||||||
try:
|
try:
|
||||||
emb_config = self.config.models.embeddings or {}
|
emb_config = self.config.models.embeddings or {}
|
||||||
model_path = self.base_dir / emb_config.get("path", "models/all-MiniLM-L6-v2")
|
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():
|
if not model_path.exists():
|
||||||
print(f"Memory init skipped: embeddings model not found at {model_path}")
|
print(f"Memory init skipped: embeddings model not found at {model_path}")
|
||||||
self._memory_interface = None
|
self._memory_interface = None
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue