mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-21 03:56:20 +00:00
* feat(channels): select custom agents per conversation Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(channels): reserve agent slash command across clients Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(tui): hide reserved slash commands from skills Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(channels): preserve selected agent across clients --------- Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com>
75 lines
2.5 KiB
Python
75 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
from deerflow.constants import DEFAULT_SKILLS_CONTAINER_PATH
|
|
from deerflow.skills.types import Skill
|
|
|
|
#: Composer control commands that own the leading slash and must never be
|
|
#: treated as ``/skill`` activations. These values plus :data:`_SLASH_SKILL_RE`
|
|
#: are mirrored by the frontend display parser in
|
|
#: ``frontend/src/core/skills/slash.ts``; both sides are pinned to the shared
|
|
#: fixture at ``contracts/slash_skill_contract.json`` by contract tests
|
|
#: (``tests/test_slash_skill_contract.py`` here, ``slash-contract.test.ts`` on
|
|
#: the frontend), so a reserved command or grammar change in only one language
|
|
#: fails CI.
|
|
RESERVED_SLASH_SKILL_NAMES = frozenset({"agent", "bootstrap", "goal", "help", "memory", "models", "new", "status"})
|
|
_SLASH_SKILL_RE = re.compile(r"^/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+|$)")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SlashSkillReference:
|
|
"""Parsed slash-skill command with the skill name and remaining task text."""
|
|
|
|
name: str
|
|
remaining_text: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ResolvedSlashSkill:
|
|
"""Slash-skill activation resolved against enabled runtime-visible skills."""
|
|
|
|
skill: Skill
|
|
remaining_text: str
|
|
container_file_path: str
|
|
|
|
|
|
def parse_slash_skill_reference(text: str) -> SlashSkillReference | None:
|
|
"""Parse strict `/skill-name task` syntax, ignoring reserved control commands."""
|
|
match = _SLASH_SKILL_RE.match(text)
|
|
if not match:
|
|
return None
|
|
name = match.group(1)
|
|
if name in RESERVED_SLASH_SKILL_NAMES:
|
|
return None
|
|
return SlashSkillReference(
|
|
name=name,
|
|
remaining_text=text[match.end() :].lstrip(),
|
|
)
|
|
|
|
|
|
def resolve_slash_skill(
|
|
text: str,
|
|
skills: list[Skill],
|
|
*,
|
|
available_skills: set[str] | None = None,
|
|
container_base_path: str = DEFAULT_SKILLS_CONTAINER_PATH,
|
|
) -> ResolvedSlashSkill | None:
|
|
"""Resolve text into an enabled, whitelisted skill activation if possible."""
|
|
reference = parse_slash_skill_reference(text)
|
|
if reference is None:
|
|
return None
|
|
if available_skills is not None and reference.name not in available_skills:
|
|
return None
|
|
|
|
skill = next((candidate for candidate in skills if candidate.name == reference.name and candidate.enabled), None)
|
|
if skill is None:
|
|
return None
|
|
|
|
return ResolvedSlashSkill(
|
|
skill=skill,
|
|
remaining_text=reference.remaining_text,
|
|
container_file_path=skill.get_container_file_path(container_base_path),
|
|
)
|