feat: add tools
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
"source": "github/awesome-copilot",
|
||||
"sourceType": "github",
|
||||
"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 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.registry import SkillRegistry
|
||||
from llama_agent_skills.tools import create_skill_tools
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
@@ -27,8 +28,10 @@ def _parse_args() -> argparse.Namespace:
|
||||
|
||||
async def _run_interactive(config: Config, registry: SkillRegistry) -> None:
|
||||
skills = registry.list_skills()
|
||||
skill_tools = create_skill_tools(registry) if skills else []
|
||||
agent = create_agent(
|
||||
skills=skills,
|
||||
tools=skill_tools,
|
||||
model=config.llm_model,
|
||||
api_key=config.llm_api_key,
|
||||
base_url=config.llm_base_url or None,
|
||||
@@ -52,8 +55,8 @@ async def _run_interactive(config: Config, registry: SkillRegistry) -> None:
|
||||
print("Bye!")
|
||||
break
|
||||
|
||||
response = await run_agent(agent, user_input)
|
||||
print(f"Agent: {response}\n")
|
||||
await run_agent_streaming(agent, user_input)
|
||||
print()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any, Callable
|
||||
|
||||
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.llms.openai import OpenAI
|
||||
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:
|
||||
return base_prompt
|
||||
|
||||
parts = [base_prompt, "\n\n---\n\n## Active Skills\n"]
|
||||
for i, skill in enumerate(skills):
|
||||
if i > 0:
|
||||
parts.append("\n---\n")
|
||||
parts.append(
|
||||
f"\n### Skill: {skill.name}\n{skill.description}\n\n{skill.body}\n"
|
||||
)
|
||||
parts = [
|
||||
base_prompt,
|
||||
"\n\n---\n\n## Available Skills\n\n",
|
||||
"You have access to the following skills. "
|
||||
"When a user's request matches a skill's description, "
|
||||
"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)
|
||||
|
||||
@@ -98,3 +110,62 @@ def create_agent(
|
||||
async def run_agent(agent: AgentWorkflow, message: str) -> str:
|
||||
response = await agent.run(user_msg=message)
|
||||
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")
|
||||
|
||||
_RESOURCE_DIRS = ("references", "scripts", "assets")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Skill:
|
||||
@@ -20,6 +22,34 @@ class Skill:
|
||||
body: str
|
||||
metadata: dict = field(default_factory=dict)
|
||||
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:
|
||||
@@ -43,10 +73,15 @@ def load_skill(path: Path) -> Skill:
|
||||
f"Invalid SKILL file {path}: missing required field '{field_name}'"
|
||||
)
|
||||
|
||||
skill_dir = path.parent
|
||||
|
||||
return Skill(
|
||||
name=post.metadata["name"],
|
||||
description=post.metadata["description"],
|
||||
body=post.content.strip(),
|
||||
metadata=post.metadata.get("metadata", {}),
|
||||
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