mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-10 15:56:03 +00:00
feat(agent-runner): expose skill resources through host context
This commit is contained in:
@@ -8,6 +8,7 @@ def make_resources(
|
||||
models: list[dict] | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
knowledge_bases: list[dict] | None = None,
|
||||
skills: list[dict] | None = None,
|
||||
storage: dict | None = None,
|
||||
files: list[dict] | None = None,
|
||||
) -> dict[str, typing.Any]:
|
||||
@@ -17,6 +18,7 @@ def make_resources(
|
||||
models: List of model dicts with 'model_id' key
|
||||
tools: List of tool dicts with 'tool_name' key
|
||||
knowledge_bases: List of KB dicts with 'kb_id' key
|
||||
skills: List of skill dicts with 'skill_name' key
|
||||
storage: Storage permissions dict
|
||||
files: List of file dicts with 'file_id' key
|
||||
|
||||
@@ -27,6 +29,7 @@ def make_resources(
|
||||
'models': models or [],
|
||||
'tools': tools or [],
|
||||
'knowledge_bases': knowledge_bases or [],
|
||||
'skills': skills or [],
|
||||
'files': files or [],
|
||||
'storage': storage or {'plugin_storage': False, 'workspace_storage': False},
|
||||
'platform_capabilities': {},
|
||||
@@ -71,6 +74,7 @@ def make_session(
|
||||
'model': {m.get('model_id') for m in res.get('models', [])},
|
||||
'tool': {t.get('tool_name') for t in res.get('tools', [])},
|
||||
'knowledge_base': {kb.get('kb_id') for kb in res.get('knowledge_bases', [])},
|
||||
'skill': {s.get('skill_name') for s in res.get('skills', [])},
|
||||
'file': {f.get('file_id') for f in res.get('files', [])},
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class TestContextValidation:
|
||||
'models': [],
|
||||
'tools': [],
|
||||
'knowledge_bases': [],
|
||||
'skills': [],
|
||||
'files': [],
|
||||
'storage': {'plugin_storage': True, 'workspace_storage': True},
|
||||
'platform_capabilities': {},
|
||||
|
||||
@@ -124,6 +124,20 @@ class FakeApplication:
|
||||
self.rag_mgr = types.SimpleNamespace(
|
||||
get_knowledge_base_by_uuid=AsyncMock(return_value=FakeKnowledgeBase("kb_001"))
|
||||
)
|
||||
self.skill_mgr = types.SimpleNamespace(
|
||||
skills={
|
||||
"demo": {
|
||||
"name": "demo",
|
||||
"display_name": "Demo Skill",
|
||||
"description": "Helps with demo tasks.",
|
||||
},
|
||||
"hidden": {
|
||||
"name": "hidden",
|
||||
"display_name": "Hidden Skill",
|
||||
"description": "Not bound to this pipeline.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FakeConversation:
|
||||
@@ -140,7 +154,12 @@ def make_descriptor() -> AgentRunnerDescriptor:
|
||||
plugin_name="local-agent",
|
||||
runner_name="default",
|
||||
protocol_version="1",
|
||||
capabilities={"streaming": True, "tool_calling": True, "knowledge_retrieval": True},
|
||||
capabilities={
|
||||
"streaming": True,
|
||||
"tool_calling": True,
|
||||
"knowledge_retrieval": True,
|
||||
"skill_authoring": True,
|
||||
},
|
||||
config_schema=[
|
||||
{"name": "model", "type": "model-fallback-selector"},
|
||||
{"name": "knowledge-bases", "type": "knowledge-base-multi-selector", "default": []},
|
||||
@@ -211,6 +230,7 @@ def make_query():
|
||||
variables={
|
||||
"_pipeline_bound_plugins": ["langbot/local-agent"],
|
||||
"_fallback_model_uuids": ["model_fallback"],
|
||||
"_pipeline_bound_skills": ["demo"],
|
||||
"public_param": "visible",
|
||||
},
|
||||
use_llm_model_uuid="model_primary",
|
||||
@@ -323,12 +343,20 @@ async def test_orchestrator_runs_fake_plugin_with_authorized_context(clean_agent
|
||||
assert {m["model_id"] for m in resources["models"]} == {"model_primary", "model_fallback"}
|
||||
assert resources["tools"][0]["tool_name"] == "langbot/test-tool/search"
|
||||
assert resources["knowledge_bases"][0]["kb_id"] == "kb_001"
|
||||
assert resources["skills"] == [
|
||||
{
|
||||
"skill_name": "demo",
|
||||
"display_name": "Demo Skill",
|
||||
"description": "Helps with demo tasks.",
|
||||
}
|
||||
]
|
||||
assert resources["storage"]["plugin_storage"] is True
|
||||
|
||||
session_during_run = plugin_connector.sessions_during_run[0]
|
||||
assert session_during_run is not None
|
||||
assert session_during_run["plugin_identity"] == "langbot/local-agent"
|
||||
assert session_during_run["authorization"]["authorized_ids"]["tool"] == {"langbot/test-tool/search"}
|
||||
assert session_during_run["authorization"]["authorized_ids"]["skill"] == {"demo"}
|
||||
assert await get_session_registry().get(context["run_id"]) is None
|
||||
|
||||
|
||||
@@ -766,6 +794,50 @@ class TestQueryEntryAdapterHostCapabilities:
|
||||
snapshot = await persistent_store.build_snapshot_from_event(event, binding, descriptor)
|
||||
assert snapshot["conversation"]["external.test_key"] == "test_value"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_from_query_restores_activated_skills_from_state(self, clean_agent_state):
|
||||
"""Persisted activated skill names are restored into the next Query run."""
|
||||
from langbot.pkg.agent.runner.persistent_state_store import get_persistent_state_store
|
||||
from langbot.pkg.provider.tools.loaders.skill import (
|
||||
ACTIVATED_SKILL_NAMES_STATE_KEY,
|
||||
ACTIVATED_SKILLS_KEY,
|
||||
)
|
||||
|
||||
db_engine = clean_agent_state
|
||||
descriptor = make_descriptor()
|
||||
plugin_connector = FakePluginConnector(
|
||||
results=[
|
||||
{
|
||||
"type": "message.completed",
|
||||
"data": {"message": {"role": "assistant", "content": "restored"}},
|
||||
}
|
||||
]
|
||||
)
|
||||
ap = FakeApplication(plugin_connector, db_engine)
|
||||
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
|
||||
query = make_query()
|
||||
|
||||
persistent_store = get_persistent_state_store(db_engine)
|
||||
event = QueryEntryAdapter.query_to_event(query)
|
||||
agent_config = QueryEntryAdapter.config_to_agent_config(query, RUNNER_ID)
|
||||
binding = AgentBindingResolver().resolve_one(event, [agent_config])
|
||||
success, error = await persistent_store.apply_update_from_event(
|
||||
event,
|
||||
binding,
|
||||
descriptor,
|
||||
"conversation",
|
||||
ACTIVATED_SKILL_NAMES_STATE_KEY,
|
||||
["demo"],
|
||||
None,
|
||||
)
|
||||
assert success is True
|
||||
assert error is None
|
||||
|
||||
messages = [message async for message in orchestrator.run_from_query(query)]
|
||||
|
||||
assert len(messages) == 1
|
||||
assert query.variables[ACTIVATED_SKILLS_KEY]["demo"]["name"] == "demo"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_event_log_and_transcript_written(self, clean_agent_state):
|
||||
"""EventLog and Transcript are written via Pipeline path."""
|
||||
|
||||
@@ -307,6 +307,21 @@ class TestIsResourceAllowed:
|
||||
|
||||
assert registry.is_resource_allowed(session, 'knowledge_base', 'kb_001') is False
|
||||
|
||||
def test_skill_allowed(self):
|
||||
"""Skill in resources should be allowed."""
|
||||
registry = AgentRunSessionRegistry()
|
||||
resources = make_resources(
|
||||
skills=[
|
||||
{'skill_name': 'demo', 'display_name': 'Demo'},
|
||||
{'skill_name': 'writer', 'display_name': 'Writer'},
|
||||
]
|
||||
)
|
||||
session = make_session(resources=resources)
|
||||
|
||||
assert registry.is_resource_allowed(session, 'skill', 'demo') is True
|
||||
assert registry.is_resource_allowed(session, 'skill', 'writer') is True
|
||||
assert registry.is_resource_allowed(session, 'skill', 'hidden') is False
|
||||
|
||||
def test_storage_plugin_allowed(self):
|
||||
"""Plugin storage permission should be checked."""
|
||||
registry = AgentRunSessionRegistry()
|
||||
|
||||
Reference in New Issue
Block a user