-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathavailable_tools.py
More file actions
167 lines (130 loc) · 5.71 KB
/
available_tools.py
File metadata and controls
167 lines (130 loc) · 5.71 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
160
161
162
163
164
165
166
167
# 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:
raise BadToolNameError(
f'Not a valid toolname: "{toolname}". '
f'Expected format: "packagename.filename"'
)
package, filename = components
try:
pkg_dir = importlib.resources.files(package)
if not pkg_dir.is_dir():
raise BadToolNameError(
f"Cannot load {toolname} because {pkg_dir} is not a valid directory."
)
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:
raise FileTypeException(
f"Error in {filepath}: expected filetype {tooltype.value!r}, "
f"got {filetype!r}."
)
# Parse into the appropriate Pydantic model
model_cls = DOCUMENT_MODELS.get(filetype)
if model_cls is None:
raise BadToolNameError(
f"Unknown filetype {filetype!r} in {toolname}"
)
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
raise BadToolNameError(
f"Validation error loading {toolname}: {exc}"
) from exc
# Cache and return
if tooltype not in self._cache:
self._cache[tooltype] = {}
self._cache[tooltype][toolname] = doc
return doc
except ModuleNotFoundError as exc:
raise BadToolNameError(f"Cannot load {toolname}: {exc}") from exc
except FileNotFoundError:
raise BadToolNameError(
f"Cannot load {toolname} because {filepath} is not a valid file."
)
except ValueError as exc:
raise BadToolNameError(f"Cannot load {toolname}: {exc}") from exc