feat: add tools
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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]
|
||||||
Reference in New Issue
Block a user