-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathavailable_tools.py
More file actions
159 lines (123 loc) · 5.62 KB
/
available_tools.py
File metadata and controls
159 lines (123 loc) · 5.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# SPDX-FileCopyrightText: GitHub, Inc.
# SPDX-License-Identifier: MIT
"""YAML resource loader for taskflow grammar files.
Loads and caches personality, taskflow, toolbox, model_config, and prompt
YAML files, validating them against Pydantic grammar models at parse time.
"""
from __future__ import annotations
__all__ = ["AvailableTools"]
import importlib.resources
from enum import Enum
from typing import Union
import yaml
from pydantic import ValidationError
from .models import (
DOCUMENT_MODELS,
ModelConfigDocument,
PersonalityDocument,
PromptDocument,
TaskflowDocument,
ToolboxDocument,
)
class BadToolNameError(Exception):
pass
class VersionException(Exception):
pass
class FileTypeException(Exception):
pass
class AvailableToolType(Enum):
Personality = "personality"
Taskflow = "taskflow"
Prompt = "prompt"
Toolbox = "toolbox"
ModelConfig = "model_config"
# Union of all document model types returned by AvailableTools
DocumentModel = Union[
TaskflowDocument, PersonalityDocument, ToolboxDocument,
ModelConfigDocument, PromptDocument,
]
class AvailableTools:
"""Loads, validates, and caches YAML grammar files as Pydantic models."""
def __init__(self) -> None:
self._cache: dict[AvailableToolType, dict[str, DocumentModel]] = {}
def get_personality(self, name: str) -> PersonalityDocument:
"""Load a personality YAML and return a validated PersonalityDocument."""
return self._load(AvailableToolType.Personality, name)
def get_taskflow(self, name: str) -> TaskflowDocument:
"""Load a taskflow YAML and return a validated TaskflowDocument."""
return self._load(AvailableToolType.Taskflow, name)
def get_prompt(self, name: str) -> PromptDocument:
"""Load a prompt YAML and return a validated PromptDocument."""
return self._load(AvailableToolType.Prompt, name)
def get_toolbox(self, name: str) -> ToolboxDocument:
"""Load a toolbox YAML and return a validated ToolboxDocument."""
return self._load(AvailableToolType.Toolbox, name)
def get_model_config(self, name: str) -> ModelConfigDocument:
"""Load a model_config YAML and return a validated ModelConfigDocument."""
return self._load(AvailableToolType.ModelConfig, name)
# Keep legacy alias for code that uses the generic accessor
def get_tool(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel:
"""Generic loader — prefer the typed ``get_*()`` methods."""
return self._load(tooltype, toolname)
def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel:
"""Load, validate, and cache a YAML grammar file.
Args:
tooltype: Expected file type (personality, taskflow, etc.).
toolname: Dotted module path, e.g. ``"examples.taskflows.echo"``.
Returns:
A validated Pydantic document model instance.
Raises:
BadToolNameError: If the tool cannot be found or loaded.
VersionException: If the grammar version is unsupported.
FileTypeException: If the filetype doesn't match expectations.
"""
# Check cache first
if tooltype in self._cache and toolname in self._cache[tooltype]:
return self._cache[tooltype][toolname]
# Resolve package and filename from dotted path
components = toolname.rsplit(".", 1)
if len(components) != 2:
msg = f'Not a valid toolname: "{toolname}". Expected format: "packagename.filename"'
raise BadToolNameError(msg)
package, filename = components
try:
pkg_dir = importlib.resources.files(package)
if not pkg_dir.is_dir():
msg = f"Cannot load {toolname} because {pkg_dir} is not a valid directory."
raise BadToolNameError(msg)
filepath = pkg_dir.joinpath(filename + ".yaml")
with filepath.open() as fh:
raw = yaml.safe_load(fh)
# Validate header before full parse
header = raw.get("seclab-taskflow-agent", {})
filetype = header.get("filetype", "")
if filetype != tooltype.value:
msg = f"Error in {filepath}: expected filetype {tooltype.value!r}, got {filetype!r}."
raise FileTypeException(msg)
# Parse into the appropriate Pydantic model
model_cls = DOCUMENT_MODELS.get(filetype)
if model_cls is None:
msg = f"Unknown filetype {filetype!r} in {toolname}"
raise BadToolNameError(msg)
try:
doc = model_cls(**raw)
except ValidationError as exc:
# Surface version errors as VersionException for compat
for err in exc.errors():
if "Unsupported version" in str(err.get("msg", "")):
raise VersionException(str(err["msg"])) from exc
msg = f"Validation error loading {toolname}: {exc}"
raise BadToolNameError(msg) from exc
if tooltype not in self._cache:
self._cache[tooltype] = {}
self._cache[tooltype][toolname] = doc
return doc
except ModuleNotFoundError as exc:
msg = f"Cannot load {toolname}: {exc}"
raise BadToolNameError(msg) from exc
except FileNotFoundError:
msg = f"Cannot load {toolname} because {filepath} is not a valid file."
raise BadToolNameError(msg)
except ValueError as exc:
msg = f"Cannot load {toolname}: {exc}"
raise BadToolNameError(msg) from exc