mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Forward runtime kwargs to skill resource functions (#4417)
* support code skills * address pr review comments * address package and syntax checks * address pr review comments * address pr review comment * address failed check * rename agentskill and agetnskillprovider * move agent skills related assets to _skills.py * address pr review comments * address review comments * support kwargs * address pr review feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
55ddd841b7
commit
8bf4235f4e
@@ -107,6 +107,15 @@ class SkillResource:
|
||||
self.content = content
|
||||
self.function = function
|
||||
|
||||
# Precompute whether the function accepts **kwargs to avoid
|
||||
# repeated inspect.signature() calls on every invocation.
|
||||
self._accepts_kwargs: bool = False
|
||||
if function is not None:
|
||||
sig = inspect.signature(function)
|
||||
self._accepts_kwargs = any(
|
||||
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
||||
)
|
||||
|
||||
|
||||
class Skill:
|
||||
"""A skill definition with optional resources.
|
||||
@@ -511,7 +520,7 @@ class SkillsProvider(BaseContextProvider):
|
||||
|
||||
return content
|
||||
|
||||
async def _read_skill_resource(self, skill_name: str, resource_name: str) -> str:
|
||||
async def _read_skill_resource(self, skill_name: str, resource_name: str, **kwargs: Any) -> str:
|
||||
"""Read a named resource from a skill.
|
||||
|
||||
Resolves the resource by case-insensitive name lookup. Static
|
||||
@@ -521,6 +530,9 @@ class SkillsProvider(BaseContextProvider):
|
||||
Args:
|
||||
skill_name: The name of the owning skill.
|
||||
resource_name: The resource name to look up (case-insensitive).
|
||||
**kwargs: Runtime keyword arguments forwarded to resource functions
|
||||
that accept ``**kwargs`` (e.g. arguments passed via
|
||||
``agent.run(user_id="123")``).
|
||||
|
||||
Returns:
|
||||
The resource content string, or a user-facing error message on
|
||||
@@ -550,9 +562,11 @@ class SkillsProvider(BaseContextProvider):
|
||||
if resource.function is not None:
|
||||
try:
|
||||
if inspect.iscoroutinefunction(resource.function):
|
||||
result = await resource.function()
|
||||
result = (
|
||||
await resource.function(**kwargs) if resource._accepts_kwargs else await resource.function()
|
||||
)
|
||||
else:
|
||||
result = resource.function()
|
||||
result = resource.function(**kwargs) if resource._accepts_kwargs else resource.function()
|
||||
return str(result)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to read resource '%s' from skill '%s'", resource_name, skill_name)
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
@@ -993,6 +994,42 @@ class TestSkillsProviderCodeSkill:
|
||||
result = await provider._read_skill_resource("prog-skill", "nonexistent")
|
||||
assert result.startswith("Error:")
|
||||
|
||||
async def test_read_callable_resource_sync_with_kwargs(self) -> None:
|
||||
skill = Skill(name="prog-skill", description="A skill.", content="Body")
|
||||
|
||||
@skill.resource
|
||||
def get_user_config(**kwargs: Any) -> str:
|
||||
user_id = kwargs.get("user_id", "unknown")
|
||||
return f"config for {user_id}"
|
||||
|
||||
provider = SkillsProvider(skills=[skill])
|
||||
result = await provider._read_skill_resource("prog-skill", "get_user_config", user_id="user_123")
|
||||
assert result == "config for user_123"
|
||||
|
||||
async def test_read_callable_resource_async_with_kwargs(self) -> None:
|
||||
skill = Skill(name="prog-skill", description="A skill.", content="Body")
|
||||
|
||||
@skill.resource
|
||||
async def get_user_data(**kwargs: Any) -> str:
|
||||
token = kwargs.get("auth_token", "none")
|
||||
return f"data with token={token}"
|
||||
|
||||
provider = SkillsProvider(skills=[skill])
|
||||
result = await provider._read_skill_resource("prog-skill", "get_user_data", auth_token="abc")
|
||||
assert result == "data with token=abc"
|
||||
|
||||
async def test_read_callable_resource_without_kwargs_ignores_extra_args(self) -> None:
|
||||
"""Resource functions without **kwargs should still work when kwargs are passed."""
|
||||
skill = Skill(name="prog-skill", description="A skill.", content="Body")
|
||||
|
||||
@skill.resource
|
||||
def static_resource() -> str:
|
||||
return "static content"
|
||||
|
||||
provider = SkillsProvider(skills=[skill])
|
||||
result = await provider._read_skill_resource("prog-skill", "static_resource", user_id="ignored")
|
||||
assert result == "static content"
|
||||
|
||||
async def test_before_run_injects_code_skills(self) -> None:
|
||||
skill = Skill(name="prog-skill", description="A code-defined skill.", content="Body")
|
||||
provider = SkillsProvider(skills=[skill])
|
||||
|
||||
Reference in New Issue
Block a user