feat: add tools

This commit is contained in:
Summer Shen
2026-03-31 22:14:10 +08:00
parent 21d764c685
commit d120f919df
5 changed files with 188 additions and 11 deletions
+5
View File
@@ -5,6 +5,11 @@
"source": "github/awesome-copilot", "source": "github/awesome-copilot",
"sourceType": "github", "sourceType": "github",
"computedHash": "e24e3c4cab4b5a07bc1d1801db14868f3a1cd184f7f28f5dd2c4a2b234bf2f2d" "computedHash": "e24e3c4cab4b5a07bc1d1801db14868f3a1cd184f7f28f5dd2c4a2b234bf2f2d"
},
"rfc-specification": {
"source": "jpoutrin/product-forge",
"sourceType": "github",
"computedHash": "7beaec93d328ba8cb90105c919d2da3f732000a4dfc4bd9168cbd0ce1b19e0af"
} }
} }
} }
+6 -3
View File
@@ -7,9 +7,10 @@ from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
from llama_agent_skills.agent import create_agent, run_agent from llama_agent_skills.agent import create_agent, run_agent, run_agent_streaming
from llama_agent_skills.config import Config from llama_agent_skills.config import Config
from llama_agent_skills.registry import SkillRegistry from llama_agent_skills.registry import SkillRegistry
from llama_agent_skills.tools import create_skill_tools
def _parse_args() -> argparse.Namespace: def _parse_args() -> argparse.Namespace:
@@ -27,8 +28,10 @@ def _parse_args() -> argparse.Namespace:
async def _run_interactive(config: Config, registry: SkillRegistry) -> None: async def _run_interactive(config: Config, registry: SkillRegistry) -> None:
skills = registry.list_skills() skills = registry.list_skills()
skill_tools = create_skill_tools(registry) if skills else []
agent = create_agent( agent = create_agent(
skills=skills, skills=skills,
tools=skill_tools,
model=config.llm_model, model=config.llm_model,
api_key=config.llm_api_key, api_key=config.llm_api_key,
base_url=config.llm_base_url or None, base_url=config.llm_base_url or None,
@@ -52,8 +55,8 @@ async def _run_interactive(config: Config, registry: SkillRegistry) -> None:
print("Bye!") print("Bye!")
break break
response = await run_agent(agent, user_input) await run_agent_streaming(agent, user_input)
print(f"Agent: {response}\n") print()
def main() -> None: def main() -> None:
+79 -8
View File
@@ -1,9 +1,17 @@
from __future__ import annotations from __future__ import annotations
import sys
from typing import Any, Callable from typing import Any, Callable
import tiktoken import tiktoken
from llama_index.core.agent.workflow import AgentWorkflow from llama_index.core.agent.workflow import (
AgentInput,
AgentOutput,
AgentStream,
AgentWorkflow,
ToolCall,
ToolCallResult,
)
from llama_index.core.base.llms.types import LLMMetadata from llama_index.core.base.llms.types import LLMMetadata
from llama_index.llms.openai import OpenAI from llama_index.llms.openai import OpenAI
from llama_index.llms.openai.utils import ( from llama_index.llms.openai.utils import (
@@ -61,13 +69,17 @@ def build_system_prompt(base_prompt: str, skills: list[Skill]) -> str:
if not skills: if not skills:
return base_prompt return base_prompt
parts = [base_prompt, "\n\n---\n\n## Active Skills\n"] parts = [
for i, skill in enumerate(skills): base_prompt,
if i > 0: "\n\n---\n\n## Available Skills\n\n",
parts.append("\n---\n") "You have access to the following skills. "
parts.append( "When a user's request matches a skill's description, "
f"\n### Skill: {skill.name}\n{skill.description}\n\n{skill.body}\n" "call `activate_skill` with the skill name to load its full instructions. "
) "Then follow those instructions to complete the task. "
"Use `read_skill_resource` to load any reference files the skill mentions.\n\n",
]
for skill in skills:
parts.append(f"- **{skill.name}**: {skill.description}\n")
return "".join(parts) return "".join(parts)
@@ -98,3 +110,62 @@ def create_agent(
async def run_agent(agent: AgentWorkflow, message: str) -> str: async def run_agent(agent: AgentWorkflow, message: str) -> str:
response = await agent.run(user_msg=message) response = await agent.run(user_msg=message)
return str(response) return str(response)
async def run_agent_streaming(
agent: AgentWorkflow,
message: str,
*,
verbose: bool = True,
) -> str:
handler = agent.run(user_msg=message)
async for event in handler.stream_events():
if isinstance(event, AgentStream):
print(event.delta, end="", flush=True)
elif isinstance(event, AgentInput):
if verbose:
print(
f"\n[Agent: {event.current_agent_name}] Input received",
file=sys.stderr,
flush=True,
)
elif isinstance(event, ToolCall):
if verbose:
kwargs_preview = str(event.tool_kwargs)
if len(kwargs_preview) > 120:
kwargs_preview = kwargs_preview[:120] + "..."
print(
f"\n>> [Tool] {event.tool_name}({kwargs_preview})",
file=sys.stderr,
flush=True,
)
elif isinstance(event, ToolCallResult):
if verbose:
output_preview = str(event.tool_output)
output_len = len(output_preview)
if output_len > 200:
output_preview = output_preview[:200] + "..."
print(
f"<< [Tool] {event.tool_name} returned {output_len} chars: {output_preview}",
file=sys.stderr,
flush=True,
)
elif isinstance(event, AgentOutput):
if verbose:
tool_names = (
[tc.tool_name for tc in event.tool_calls]
if event.tool_calls
else []
)
if tool_names:
print(
f"\n[Agent Output] Tool calls: {tool_names}",
file=sys.stderr,
flush=True,
)
print(flush=True)
result = await handler
return str(result)
+35
View File
@@ -12,6 +12,8 @@ class SkillLoadError(Exception):
_REQUIRED_FIELDS = ("name", "description") _REQUIRED_FIELDS = ("name", "description")
_RESOURCE_DIRS = ("references", "scripts", "assets")
@dataclass(frozen=True) @dataclass(frozen=True)
class Skill: class Skill:
@@ -20,6 +22,34 @@ class Skill:
body: str body: str
metadata: dict = field(default_factory=dict) metadata: dict = field(default_factory=dict)
source_path: Path | None = None source_path: Path | None = None
references: list[Path] = field(default_factory=list)
scripts: list[Path] = field(default_factory=list)
assets: list[Path] = field(default_factory=list)
def list_resources(self) -> dict[str, list[str]]:
result: dict[str, list[str]] = {}
for category, paths in [
("references", self.references),
("scripts", self.scripts),
("assets", self.assets),
]:
if paths:
result[category] = [p.name for p in paths]
return result
def find_resource(self, filename: str) -> Path | None:
for resource_list in (self.references, self.scripts, self.assets):
for p in resource_list:
if p.name == filename:
return p
return None
def _scan_resource_dir(skill_dir: Path, dirname: str) -> list[Path]:
resource_dir = skill_dir / dirname
if not resource_dir.is_dir():
return []
return sorted(p for p in resource_dir.iterdir() if p.is_file())
def load_skill(path: Path) -> Skill: def load_skill(path: Path) -> Skill:
@@ -43,10 +73,15 @@ def load_skill(path: Path) -> Skill:
f"Invalid SKILL file {path}: missing required field '{field_name}'" f"Invalid SKILL file {path}: missing required field '{field_name}'"
) )
skill_dir = path.parent
return Skill( return Skill(
name=post.metadata["name"], name=post.metadata["name"],
description=post.metadata["description"], description=post.metadata["description"],
body=post.content.strip(), body=post.content.strip(),
metadata=post.metadata.get("metadata", {}), metadata=post.metadata.get("metadata", {}),
source_path=path, source_path=path,
references=_scan_resource_dir(skill_dir, "references"),
scripts=_scan_resource_dir(skill_dir, "scripts"),
assets=_scan_resource_dir(skill_dir, "assets"),
) )
+63
View File
@@ -0,0 +1,63 @@
from __future__ import annotations
from llama_agent_skills.registry import SkillRegistry
def create_skill_tools(registry: SkillRegistry) -> list:
def activate_skill(skill_name: str) -> str:
"""Activate a skill to get its full instructions.
Call this when you determine a user's request matches a skill's description.
Returns the complete skill instructions (body) and lists available resources.
Args:
skill_name: The name of the skill to activate (from the skill list).
"""
skill = registry.get(skill_name)
if skill is None:
available = [s.name for s in registry.list_skills()]
return f"Skill '{skill_name}' not found. Available skills: {available}"
parts = [skill.body]
resources = skill.list_resources()
if resources:
parts.append("\n\n---\n\n## Available Resources\n")
parts.append(
"Use `read_skill_resource` to read any of these files when needed:\n"
)
for category, filenames in resources.items():
parts.append(f"\n### {category}/")
for fname in filenames:
parts.append(f"- {fname}")
return "\n".join(parts)
def read_skill_resource(skill_name: str, filename: str) -> str:
"""Read a resource file (reference, script, or asset) from a skill.
Use this to load supplementary material like templates, schemas,
or reference documentation that a skill mentions.
Args:
skill_name: The name of the skill that owns the resource.
filename: The filename to read (e.g. 'rfc-template.md', 'excalidraw-schema.md').
"""
skill = registry.get(skill_name)
if skill is None:
return f"Skill '{skill_name}' not found."
resource_path = skill.find_resource(filename)
if resource_path is None:
resources = skill.list_resources()
return (
f"Resource '{filename}' not found in skill '{skill_name}'. "
f"Available resources: {resources}"
)
try:
return resource_path.read_text(encoding="utf-8")
except Exception as exc:
return f"Failed to read '{filename}': {exc}"
return [activate_skill, read_skill_resource]