mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
refactor(skills): own explicit runtime context loading
This commit is contained in:
@@ -159,33 +159,6 @@ class ContextBuilder:
|
|||||||
|
|
||||||
return "\n\n---\n\n".join(parts)
|
return "\n\n---\n\n".join(parts)
|
||||||
|
|
||||||
def build_runtime_context_blocks(
|
|
||||||
self,
|
|
||||||
current_message: str,
|
|
||||||
blocks: Sequence[RuntimeContextBlock] | None = None,
|
|
||||||
) -> list[RuntimeContextBlock]:
|
|
||||||
"""Add explicitly invoked skill instructions to this turn's runtime context."""
|
|
||||||
merged = list(blocks or ())
|
|
||||||
invoked = self.skills.get_explicitly_invoked_skills(current_message)
|
|
||||||
if not invoked:
|
|
||||||
return merged
|
|
||||||
always_active = set(self.skills.get_always_skills())
|
|
||||||
skill_names = [name for name in invoked if name not in always_active]
|
|
||||||
skill_content = self.skills.load_skills_for_context(skill_names)
|
|
||||||
if not skill_content:
|
|
||||||
return merged
|
|
||||||
skill_block = RuntimeContextBlock(
|
|
||||||
source="explicit_skills",
|
|
||||||
content=(
|
|
||||||
"[Active Skills — instructions for this user turn]\n"
|
|
||||||
f"{skill_content}\n"
|
|
||||||
"[/Active Skills]"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if skill_block not in merged:
|
|
||||||
merged.append(skill_block)
|
|
||||||
return merged
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _without_duplicate_session_summary(
|
def _without_duplicate_session_summary(
|
||||||
entries: list[dict[str, Any]],
|
entries: list[dict[str, Any]],
|
||||||
@@ -347,11 +320,12 @@ class ContextBuilder:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build only the fresh turn message without merging it into history."""
|
"""Build only the fresh turn message without merging it into history."""
|
||||||
content = self.build_user_content(current_message, image_paths=media)
|
content = self.build_user_content(current_message, image_paths=media)
|
||||||
blocks = (
|
blocks: list[RuntimeContextBlock] = []
|
||||||
self.build_runtime_context_blocks(current_message, runtime_context_blocks)
|
if current_role == "user":
|
||||||
if current_role == "user"
|
blocks.extend(runtime_context_blocks or ())
|
||||||
else []
|
skill_context = self.skills.build_explicit_skill_runtime_context(current_message)
|
||||||
)
|
if skill_context is not None and skill_context not in blocks:
|
||||||
|
blocks.append(skill_context)
|
||||||
merged, runtime_context_meta = append_runtime_context(content, blocks)
|
merged, runtime_context_meta = append_runtime_context(content, blocks)
|
||||||
current: dict[str, Any] = {"role": current_role, "content": merged}
|
current: dict[str, Any] = {"role": current_role, "content": merged}
|
||||||
if current_role == "user" and runtime_context_meta is not None:
|
if current_role == "user" and runtime_context_meta is not None:
|
||||||
|
|||||||
@@ -797,10 +797,12 @@ class AgentLoop:
|
|||||||
]
|
]
|
||||||
blocks = runtime_context_blocks_from_metadata(request.metadata)
|
blocks = runtime_context_blocks_from_metadata(request.metadata)
|
||||||
blocks.extend(await resolve_runtime_context(providers, request))
|
blocks.extend(await resolve_runtime_context(providers, request))
|
||||||
return self.context.build_runtime_context_blocks(
|
skill_context = self.context.skills.build_explicit_skill_runtime_context(
|
||||||
request.original_user_text or "",
|
request.original_user_text or ""
|
||||||
blocks,
|
|
||||||
)
|
)
|
||||||
|
if skill_context is not None and skill_context not in blocks:
|
||||||
|
blocks.append(skill_context)
|
||||||
|
return blocks
|
||||||
|
|
||||||
async def _dispatch_command_inline(
|
async def _dispatch_command_inline(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from typing import Any, cast
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from nanobot.runtime_context import RuntimeContextBlock
|
||||||
|
|
||||||
# Default builtin skills directory (relative to this file)
|
# Default builtin skills directory (relative to this file)
|
||||||
BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"
|
BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"
|
||||||
|
|
||||||
@@ -177,6 +179,28 @@ class SkillsLoader:
|
|||||||
invoked.append(name)
|
invoked.append(name)
|
||||||
return invoked
|
return invoked
|
||||||
|
|
||||||
|
def build_explicit_skill_runtime_context(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
) -> RuntimeContextBlock | None:
|
||||||
|
"""Load non-always skills explicitly invoked by the current message."""
|
||||||
|
skill_names = self.get_explicitly_invoked_skills(text)
|
||||||
|
if not skill_names:
|
||||||
|
return None
|
||||||
|
always_active = set(self.get_always_skills())
|
||||||
|
skill_names = [name for name in skill_names if name not in always_active]
|
||||||
|
content = self.load_skills_for_context(skill_names)
|
||||||
|
if not content:
|
||||||
|
return None
|
||||||
|
return RuntimeContextBlock(
|
||||||
|
source="explicit_skills",
|
||||||
|
content=(
|
||||||
|
"[Active Skills — instructions for this user turn]\n"
|
||||||
|
f"{content}\n"
|
||||||
|
"[/Active Skills]"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def build_skills_summary(self, exclude: set[str] | None = None) -> str:
|
def build_skills_summary(self, exclude: set[str] | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
Build a summary of all skills (name, description, path, availability).
|
Build a summary of all skills (name, description, path, availability).
|
||||||
|
|||||||
@@ -383,6 +383,36 @@ def test_explicit_skill_references_resolve_available_enabled_names_in_order(
|
|||||||
assert invoked == ["alpha"]
|
assert invoked == ["alpha"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_explicit_skills_share_one_ordered_runtime_context(tmp_path: Path) -> None:
|
||||||
|
workspace = tmp_path / "ws"
|
||||||
|
skills_root = workspace / "skills"
|
||||||
|
skills_root.mkdir(parents=True)
|
||||||
|
_write_skill(skills_root, "alpha", body="Alpha instructions")
|
||||||
|
_write_skill(skills_root, "beta", body="Beta instructions")
|
||||||
|
_write_skill(
|
||||||
|
skills_root,
|
||||||
|
"always",
|
||||||
|
metadata_json={"always": True},
|
||||||
|
body="Always instructions",
|
||||||
|
)
|
||||||
|
builtin = tmp_path / "builtin"
|
||||||
|
builtin.mkdir()
|
||||||
|
loader = SkillsLoader(workspace, builtin_skills_dir=builtin)
|
||||||
|
|
||||||
|
context = loader.build_explicit_skill_runtime_context(
|
||||||
|
"Use $beta, then $alpha, $beta again, and $always."
|
||||||
|
)
|
||||||
|
|
||||||
|
assert context is not None
|
||||||
|
assert context.source == "explicit_skills"
|
||||||
|
assert context.content.count("### Skill: beta") == 1
|
||||||
|
assert context.content.count("### Skill: alpha") == 1
|
||||||
|
assert context.content.index("### Skill: beta") < context.content.index(
|
||||||
|
"### Skill: alpha"
|
||||||
|
)
|
||||||
|
assert "### Skill: always" not in context.content
|
||||||
|
|
||||||
|
|
||||||
# -- multiline description tests (YAML folded > and literal |) -----------------
|
# -- multiline description tests (YAML folded > and literal |) -----------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user