acontext 0.0.17__tar.gz → 0.0.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {acontext-0.0.17 → acontext-0.0.18}/PKG-INFO +1 -1
- {acontext-0.0.17 → acontext-0.0.18}/pyproject.toml +1 -1
- acontext-0.0.18/src/acontext/agent/skill.py +253 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_skills.py +13 -28
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/skills.py +21 -30
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/skill.py +14 -18
- acontext-0.0.17/src/acontext/agent/skill.py +0 -148
- {acontext-0.0.17 → acontext-0.0.18}/README.md +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/__init__.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/_constants.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/_utils.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/agent/__init__.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/agent/base.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/agent/disk.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/async_client.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/client.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/client_types.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/errors.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/messages.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/py.typed +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/__init__.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_blocks.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_disks.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_sessions.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_spaces.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_tools.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/async_users.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/blocks.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/disks.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/sessions.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/spaces.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/tools.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/resources/users.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/__init__.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/block.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/common.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/disk.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/session.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/space.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/tool.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/types/user.py +0 -0
- {acontext-0.0.17 → acontext-0.0.18}/src/acontext/uploads.py +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill tools for agent operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from .base import BaseContext, BaseTool, BaseToolPool
|
|
8
|
+
from ..client import AcontextClient
|
|
9
|
+
from ..types.skill import Skill
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SkillContext(BaseContext):
|
|
14
|
+
"""Context for skill tools with preloaded skill name mapping."""
|
|
15
|
+
|
|
16
|
+
client: AcontextClient
|
|
17
|
+
skills: dict[str, Skill] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def create(cls, client: AcontextClient, skill_ids: list[str]) -> "SkillContext":
|
|
21
|
+
"""Create a SkillContext by preloading skills from a list of skill IDs.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
client: The Acontext client instance.
|
|
25
|
+
skill_ids: List of skill UUIDs to preload.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
SkillContext with preloaded skills mapped by name.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If duplicate skill names are found.
|
|
32
|
+
"""
|
|
33
|
+
skills: dict[str, Skill] = {}
|
|
34
|
+
for skill_id in skill_ids:
|
|
35
|
+
skill = client.skills.get(skill_id)
|
|
36
|
+
if skill.name in skills:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Duplicate skill name '{skill.name}' found. "
|
|
39
|
+
f"Existing ID: {skills[skill.name].id}, New ID: {skill.id}"
|
|
40
|
+
)
|
|
41
|
+
skills[skill.name] = skill
|
|
42
|
+
return cls(client=client, skills=skills)
|
|
43
|
+
|
|
44
|
+
def get_skill(self, skill_name: str) -> Skill:
|
|
45
|
+
"""Get a skill by name from the preloaded skills.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
skill_name: The name of the skill.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The Skill object.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the skill is not found in the context.
|
|
55
|
+
"""
|
|
56
|
+
if skill_name not in self.skills:
|
|
57
|
+
available = ", ".join(self.skills.keys()) if self.skills else "[none]"
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Skill '{skill_name}' not found in context. Available skills: {available}"
|
|
60
|
+
)
|
|
61
|
+
return self.skills[skill_name]
|
|
62
|
+
|
|
63
|
+
def list_skill_names(self) -> list[str]:
|
|
64
|
+
"""Return list of available skill names in this context."""
|
|
65
|
+
return list(self.skills.keys())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GetSkillTool(BaseTool):
|
|
69
|
+
"""Tool for getting a skill by name."""
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def name(self) -> str:
|
|
73
|
+
return "get_skill"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def description(self) -> str:
|
|
77
|
+
return (
|
|
78
|
+
"Get a skill by its name. Returns the skill information including "
|
|
79
|
+
"the relative paths of the files and their mime type categories."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def arguments(self) -> dict:
|
|
84
|
+
return {
|
|
85
|
+
"skill_name": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "The name of the skill.",
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def required_arguments(self) -> list[str]:
|
|
93
|
+
return ["skill_name"]
|
|
94
|
+
|
|
95
|
+
def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
|
|
96
|
+
"""Get a skill by name."""
|
|
97
|
+
skill_name = llm_arguments.get("skill_name")
|
|
98
|
+
|
|
99
|
+
if not skill_name:
|
|
100
|
+
raise ValueError("skill_name is required")
|
|
101
|
+
|
|
102
|
+
skill = ctx.get_skill(skill_name)
|
|
103
|
+
|
|
104
|
+
file_count = len(skill.file_index)
|
|
105
|
+
|
|
106
|
+
# Format all files with path and MIME type
|
|
107
|
+
if skill.file_index:
|
|
108
|
+
file_list = "\n".join(
|
|
109
|
+
[
|
|
110
|
+
f" - {file_info.path} ({file_info.mime})"
|
|
111
|
+
for file_info in skill.file_index
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
file_list = " [NO FILES]"
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
f"Skill: {skill.name} (ID: {skill.id})\n"
|
|
119
|
+
f"Description: {skill.description}\n"
|
|
120
|
+
f"Files: {file_count} file(s)\n"
|
|
121
|
+
f"{file_list}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class GetSkillFileTool(BaseTool):
|
|
126
|
+
"""Tool for getting a file from a skill."""
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def name(self) -> str:
|
|
130
|
+
return "get_skill_file"
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def description(self) -> str:
|
|
134
|
+
return (
|
|
135
|
+
"Get a file from a skill by name. The file_path should be a relative "
|
|
136
|
+
"path within the skill (e.g., 'scripts/extract_text.json')."
|
|
137
|
+
"Tips: SKILL.md is the first file you should read to understand the full picture of this skill's content."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def arguments(self) -> dict:
|
|
142
|
+
return {
|
|
143
|
+
"skill_name": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "The name of the skill.",
|
|
146
|
+
},
|
|
147
|
+
"file_path": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"description": "Relative path to the file within the skill (e.g., 'scripts/extract_text.json').",
|
|
150
|
+
},
|
|
151
|
+
"expire": {
|
|
152
|
+
"type": "integer",
|
|
153
|
+
"description": "URL expiration time in seconds (only used for non-parseable files). Defaults to 900 (15 minutes).",
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def required_arguments(self) -> list[str]:
|
|
159
|
+
return ["skill_name", "file_path"]
|
|
160
|
+
|
|
161
|
+
def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
|
|
162
|
+
"""Get a skill file."""
|
|
163
|
+
skill_name = llm_arguments.get("skill_name")
|
|
164
|
+
file_path = llm_arguments.get("file_path")
|
|
165
|
+
expire = llm_arguments.get("expire")
|
|
166
|
+
|
|
167
|
+
if not skill_name:
|
|
168
|
+
raise ValueError("skill_name is required")
|
|
169
|
+
if not file_path:
|
|
170
|
+
raise ValueError("file_path is required")
|
|
171
|
+
|
|
172
|
+
skill = ctx.get_skill(skill_name)
|
|
173
|
+
|
|
174
|
+
result = ctx.client.skills.get_file(
|
|
175
|
+
skill_id=skill.id,
|
|
176
|
+
file_path=file_path,
|
|
177
|
+
expire=expire,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
output_parts = [
|
|
181
|
+
f"File '{result.path}' (MIME: {result.mime}) from skill '{skill_name}':"
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
if result.content:
|
|
185
|
+
output_parts.append(f"\nContent (type: {result.content.type}):")
|
|
186
|
+
output_parts.append(result.content.raw)
|
|
187
|
+
|
|
188
|
+
if result.url:
|
|
189
|
+
expire_seconds = expire if expire is not None else 900
|
|
190
|
+
output_parts.append(
|
|
191
|
+
f"\nDownload URL (expires in {expire_seconds} seconds):"
|
|
192
|
+
)
|
|
193
|
+
output_parts.append(result.url)
|
|
194
|
+
|
|
195
|
+
if not result.content and not result.url:
|
|
196
|
+
return f"File '{result.path}' retrieved but no content or URL returned."
|
|
197
|
+
|
|
198
|
+
return "\n".join(output_parts)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ListSkillsTool(BaseTool):
|
|
202
|
+
"""Tool for listing available skills in the context."""
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def name(self) -> str:
|
|
206
|
+
return "list_skills"
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def description(self) -> str:
|
|
210
|
+
return "List all available skills in the current context with their names and descriptions."
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def arguments(self) -> dict:
|
|
214
|
+
return {}
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def required_arguments(self) -> list[str]:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
|
|
221
|
+
"""List all available skills."""
|
|
222
|
+
if not ctx.skills:
|
|
223
|
+
return "No skills available in the current context."
|
|
224
|
+
|
|
225
|
+
skill_list = []
|
|
226
|
+
for skill_name, skill in ctx.skills.items():
|
|
227
|
+
skill_list.append(f"- {skill_name}: {skill.description}")
|
|
228
|
+
|
|
229
|
+
return f"Available skills ({len(ctx.skills)}):\n" + "\n".join(skill_list)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class SkillToolPool(BaseToolPool):
|
|
233
|
+
"""Tool pool for skill operations on Acontext skills."""
|
|
234
|
+
|
|
235
|
+
def format_context(
|
|
236
|
+
self, client: AcontextClient, skill_ids: list[str]
|
|
237
|
+
) -> SkillContext:
|
|
238
|
+
"""Create a SkillContext by preloading skills from a list of skill IDs.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
client: The Acontext client instance.
|
|
242
|
+
skill_ids: List of skill UUIDs to preload.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
SkillContext with preloaded skills mapped by name.
|
|
246
|
+
"""
|
|
247
|
+
return SkillContext.create(client=client, skill_ids=skill_ids)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
SKILL_TOOLS = SkillToolPool()
|
|
251
|
+
SKILL_TOOLS.add_tool(ListSkillsTool())
|
|
252
|
+
SKILL_TOOLS.add_tool(GetSkillTool())
|
|
253
|
+
SKILL_TOOLS.add_tool(GetSkillFileTool())
|
|
@@ -12,8 +12,6 @@ from ..types.skill import (
|
|
|
12
12
|
GetSkillFileResp,
|
|
13
13
|
ListSkillsOutput,
|
|
14
14
|
Skill,
|
|
15
|
-
SkillCatalogItem,
|
|
16
|
-
_ListSkillsResponse,
|
|
17
15
|
)
|
|
18
16
|
from ..uploads import FileUpload, normalize_file_upload
|
|
19
17
|
|
|
@@ -84,31 +82,19 @@ class AsyncSkillsAPI:
|
|
|
84
82
|
data = await self._requester.request(
|
|
85
83
|
"GET", "/agent_skills", params=params or None
|
|
86
84
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# Convert to catalog format (name and description only)
|
|
90
|
-
return ListSkillsOutput(
|
|
91
|
-
items=[
|
|
92
|
-
SkillCatalogItem(name=skill.name, description=skill.description)
|
|
93
|
-
for skill in api_response.items
|
|
94
|
-
],
|
|
95
|
-
next_cursor=api_response.next_cursor,
|
|
96
|
-
has_more=api_response.has_more,
|
|
97
|
-
)
|
|
85
|
+
# Pydantic ignores extra fields, so ListSkillsOutput directly extracts name/description
|
|
86
|
+
return ListSkillsOutput.model_validate(data)
|
|
98
87
|
|
|
99
|
-
async def
|
|
100
|
-
"""Get a skill by its
|
|
88
|
+
async def get(self, skill_id: str) -> Skill:
|
|
89
|
+
"""Get a skill by its ID.
|
|
101
90
|
|
|
102
91
|
Args:
|
|
103
|
-
|
|
92
|
+
skill_id: The UUID of the skill.
|
|
104
93
|
|
|
105
94
|
Returns:
|
|
106
|
-
Skill containing the skill information.
|
|
95
|
+
Skill containing the full skill information including file_index.
|
|
107
96
|
"""
|
|
108
|
-
|
|
109
|
-
data = await self._requester.request(
|
|
110
|
-
"GET", "/agent_skills/by_name", params=params
|
|
111
|
-
)
|
|
97
|
+
data = await self._requester.request("GET", f"/agent_skills/{skill_id}")
|
|
112
98
|
return Skill.model_validate(data)
|
|
113
99
|
|
|
114
100
|
async def delete(self, skill_id: str) -> None:
|
|
@@ -119,32 +105,31 @@ class AsyncSkillsAPI:
|
|
|
119
105
|
"""
|
|
120
106
|
await self._requester.request("DELETE", f"/agent_skills/{skill_id}")
|
|
121
107
|
|
|
122
|
-
async def
|
|
108
|
+
async def get_file(
|
|
123
109
|
self,
|
|
124
110
|
*,
|
|
125
|
-
|
|
111
|
+
skill_id: str,
|
|
126
112
|
file_path: str,
|
|
127
113
|
expire: int | None = None,
|
|
128
114
|
) -> GetSkillFileResp:
|
|
129
|
-
"""Get a file from a skill by
|
|
115
|
+
"""Get a file from a skill by skill ID.
|
|
130
116
|
|
|
131
117
|
The backend automatically returns content for parseable text files, or a presigned URL
|
|
132
118
|
for non-parseable files (binary, images, etc.).
|
|
133
119
|
|
|
134
120
|
Args:
|
|
135
|
-
|
|
121
|
+
skill_id: The UUID of the skill.
|
|
136
122
|
file_path: Relative path to the file within the skill (e.g., 'scripts/extract_text.json').
|
|
137
123
|
expire: URL expiration time in seconds. Defaults to 900 (15 minutes).
|
|
138
124
|
|
|
139
125
|
Returns:
|
|
140
126
|
GetSkillFileResp containing the file path, MIME type, and either content or URL.
|
|
141
127
|
"""
|
|
142
|
-
endpoint = f"/agent_skills/
|
|
128
|
+
endpoint = f"/agent_skills/{skill_id}/file"
|
|
143
129
|
|
|
144
|
-
params = {"file_path": file_path}
|
|
130
|
+
params: dict[str, Any] = {"file_path": file_path}
|
|
145
131
|
if expire is not None:
|
|
146
132
|
params["expire"] = expire
|
|
147
133
|
|
|
148
134
|
data = await self._requester.request("GET", endpoint, params=params)
|
|
149
135
|
return GetSkillFileResp.model_validate(data)
|
|
150
|
-
|
|
@@ -12,8 +12,6 @@ from ..types.skill import (
|
|
|
12
12
|
GetSkillFileResp,
|
|
13
13
|
ListSkillsOutput,
|
|
14
14
|
Skill,
|
|
15
|
-
SkillCatalogItem,
|
|
16
|
-
_ListSkillsResponse,
|
|
17
15
|
)
|
|
18
16
|
from ..uploads import FileUpload, normalize_file_upload
|
|
19
17
|
|
|
@@ -25,9 +23,11 @@ class SkillsAPI:
|
|
|
25
23
|
def create(
|
|
26
24
|
self,
|
|
27
25
|
*,
|
|
28
|
-
file:
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
file: (
|
|
27
|
+
FileUpload
|
|
28
|
+
| tuple[str, BinaryIO | bytes]
|
|
29
|
+
| tuple[str, BinaryIO | bytes, str]
|
|
30
|
+
),
|
|
31
31
|
user: str | None = None,
|
|
32
32
|
meta: Mapping[str, Any] | None = None,
|
|
33
33
|
) -> Skill:
|
|
@@ -80,31 +80,23 @@ class SkillsAPI:
|
|
|
80
80
|
along with pagination information (next_cursor and has_more).
|
|
81
81
|
"""
|
|
82
82
|
effective_limit = limit if limit is not None else 100
|
|
83
|
-
params = build_params(
|
|
84
|
-
|
|
85
|
-
api_response = _ListSkillsResponse.model_validate(data)
|
|
86
|
-
|
|
87
|
-
# Convert to catalog format (name and description only)
|
|
88
|
-
return ListSkillsOutput(
|
|
89
|
-
items=[
|
|
90
|
-
SkillCatalogItem(name=skill.name, description=skill.description)
|
|
91
|
-
for skill in api_response.items
|
|
92
|
-
],
|
|
93
|
-
next_cursor=api_response.next_cursor,
|
|
94
|
-
has_more=api_response.has_more,
|
|
83
|
+
params = build_params(
|
|
84
|
+
user=user, limit=effective_limit, cursor=cursor, time_desc=time_desc
|
|
95
85
|
)
|
|
86
|
+
data = self._requester.request("GET", "/agent_skills", params=params or None)
|
|
87
|
+
# Pydantic ignores extra fields, so ListSkillsOutput directly extracts name/description
|
|
88
|
+
return ListSkillsOutput.model_validate(data)
|
|
96
89
|
|
|
97
|
-
def
|
|
98
|
-
"""Get a skill by its
|
|
90
|
+
def get(self, skill_id: str) -> Skill:
|
|
91
|
+
"""Get a skill by its ID.
|
|
99
92
|
|
|
100
93
|
Args:
|
|
101
|
-
|
|
94
|
+
skill_id: The UUID of the skill.
|
|
102
95
|
|
|
103
96
|
Returns:
|
|
104
|
-
Skill containing the skill information.
|
|
97
|
+
Skill containing the full skill information including file_index.
|
|
105
98
|
"""
|
|
106
|
-
|
|
107
|
-
data = self._requester.request("GET", "/agent_skills/by_name", params=params)
|
|
99
|
+
data = self._requester.request("GET", f"/agent_skills/{skill_id}")
|
|
108
100
|
return Skill.model_validate(data)
|
|
109
101
|
|
|
110
102
|
def delete(self, skill_id: str) -> None:
|
|
@@ -115,32 +107,31 @@ class SkillsAPI:
|
|
|
115
107
|
"""
|
|
116
108
|
self._requester.request("DELETE", f"/agent_skills/{skill_id}")
|
|
117
109
|
|
|
118
|
-
def
|
|
110
|
+
def get_file(
|
|
119
111
|
self,
|
|
120
112
|
*,
|
|
121
|
-
|
|
113
|
+
skill_id: str,
|
|
122
114
|
file_path: str,
|
|
123
115
|
expire: int | None = None,
|
|
124
116
|
) -> GetSkillFileResp:
|
|
125
|
-
"""Get a file from a skill by
|
|
117
|
+
"""Get a file from a skill by skill ID.
|
|
126
118
|
|
|
127
119
|
The backend automatically returns content for parseable text files, or a presigned URL
|
|
128
120
|
for non-parseable files (binary, images, etc.).
|
|
129
121
|
|
|
130
122
|
Args:
|
|
131
|
-
|
|
123
|
+
skill_id: The UUID of the skill.
|
|
132
124
|
file_path: Relative path to the file within the skill (e.g., 'scripts/extract_text.json').
|
|
133
125
|
expire: URL expiration time in seconds. Defaults to 900 (15 minutes).
|
|
134
126
|
|
|
135
127
|
Returns:
|
|
136
128
|
GetSkillFileResp containing the file path, MIME type, and either content or URL.
|
|
137
129
|
"""
|
|
138
|
-
endpoint = f"/agent_skills/
|
|
130
|
+
endpoint = f"/agent_skills/{skill_id}/file"
|
|
139
131
|
|
|
140
|
-
params = {"file_path": file_path}
|
|
132
|
+
params: dict[str, Any] = {"file_path": file_path}
|
|
141
133
|
if expire is not None:
|
|
142
134
|
params["expire"] = expire
|
|
143
135
|
|
|
144
136
|
data = self._requester.request("GET", endpoint, params=params)
|
|
145
137
|
return GetSkillFileResp.model_validate(data)
|
|
146
|
-
|
|
@@ -24,9 +24,7 @@ class Skill(BaseModel):
|
|
|
24
24
|
file_index: list[FileInfo] = Field(
|
|
25
25
|
..., description="List of file information (path and MIME type) in the skill"
|
|
26
26
|
)
|
|
27
|
-
meta: dict[str, Any] | None = Field(
|
|
28
|
-
None, description="Custom metadata dictionary"
|
|
29
|
-
)
|
|
27
|
+
meta: dict[str, Any] | None = Field(None, description="Custom metadata dictionary")
|
|
30
28
|
created_at: str = Field(..., description="ISO 8601 formatted creation timestamp")
|
|
31
29
|
updated_at: str = Field(..., description="ISO 8601 formatted update timestamp")
|
|
32
30
|
|
|
@@ -39,7 +37,11 @@ class SkillCatalogItem(BaseModel):
|
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
class ListSkillsOutput(BaseModel):
|
|
42
|
-
"""Response model for listing skills (catalog format with name and description only).
|
|
40
|
+
"""Response model for listing skills (catalog format with name and description only).
|
|
41
|
+
|
|
42
|
+
Pydantic ignores extra fields, so this directly parses API responses
|
|
43
|
+
and extracts only name/description from each item.
|
|
44
|
+
"""
|
|
43
45
|
|
|
44
46
|
items: list[SkillCatalogItem] = Field(
|
|
45
47
|
..., description="List of skills with name and description"
|
|
@@ -48,22 +50,16 @@ class ListSkillsOutput(BaseModel):
|
|
|
48
50
|
has_more: bool = Field(..., description="Whether there are more items")
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
class _ListSkillsResponse(BaseModel):
|
|
52
|
-
"""Internal response model for API pagination (full Skill objects).
|
|
53
|
-
|
|
54
|
-
This is used internally to parse the raw API response before converting
|
|
55
|
-
to the catalog format (ListSkillsOutput).
|
|
56
|
-
"""
|
|
57
|
-
items: list[Skill]
|
|
58
|
-
next_cursor: str | None = None
|
|
59
|
-
has_more: bool = False
|
|
60
|
-
|
|
61
|
-
|
|
62
53
|
class GetSkillFileResp(BaseModel):
|
|
63
54
|
"""Response model for getting a skill file."""
|
|
64
55
|
|
|
65
56
|
path: str = Field(..., description="File path")
|
|
66
57
|
mime: str = Field(..., description="MIME type of the file")
|
|
67
|
-
url: str | None = Field(
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
url: str | None = Field(
|
|
59
|
+
None,
|
|
60
|
+
description="Presigned URL for downloading the file (present if file is not parseable)",
|
|
61
|
+
)
|
|
62
|
+
content: FileContent | None = Field(
|
|
63
|
+
None,
|
|
64
|
+
description="Parsed file content if available (present if file is parseable)",
|
|
65
|
+
)
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Skill tools for agent operations.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
from .base import BaseContext, BaseTool, BaseToolPool
|
|
8
|
-
from ..client import AcontextClient
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class SkillContext(BaseContext):
|
|
13
|
-
client: AcontextClient
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class GetSkillTool(BaseTool):
|
|
17
|
-
"""Tool for getting a skill by name."""
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def name(self) -> str:
|
|
21
|
-
return "get_skill"
|
|
22
|
-
|
|
23
|
-
@property
|
|
24
|
-
def description(self) -> str:
|
|
25
|
-
return (
|
|
26
|
-
"Get a skill by its name. Return the skill information including the relative paths of the files and their mime type categories"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def arguments(self) -> dict:
|
|
31
|
-
return {
|
|
32
|
-
"name": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"description": "The name of the skill (unique within project).",
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def required_arguments(self) -> list[str]:
|
|
40
|
-
return ["name"]
|
|
41
|
-
|
|
42
|
-
def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
|
|
43
|
-
"""Get a skill by name."""
|
|
44
|
-
name = llm_arguments.get("name")
|
|
45
|
-
|
|
46
|
-
if not name:
|
|
47
|
-
raise ValueError("name is required")
|
|
48
|
-
|
|
49
|
-
skill = ctx.client.skills.get_by_name(name)
|
|
50
|
-
|
|
51
|
-
file_count = len(skill.file_index)
|
|
52
|
-
|
|
53
|
-
# Format all files with path and MIME type
|
|
54
|
-
if skill.file_index:
|
|
55
|
-
file_list = "\n".join(
|
|
56
|
-
[f" - {file_info.path} ({file_info.mime})" for file_info in skill.file_index]
|
|
57
|
-
)
|
|
58
|
-
else:
|
|
59
|
-
file_list = " [NO FILES]"
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
f"Skill: {skill.name} (ID: {skill.id})\n"
|
|
63
|
-
f"Description: {skill.description}\n"
|
|
64
|
-
f"Files: {file_count} file(s)\n"
|
|
65
|
-
f"{file_list}\n"
|
|
66
|
-
f"Created: {skill.created_at}\n"
|
|
67
|
-
f"Updated: {skill.updated_at}"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class GetSkillFileTool(BaseTool):
|
|
72
|
-
"""Tool for getting a file from a skill."""
|
|
73
|
-
|
|
74
|
-
@property
|
|
75
|
-
def name(self) -> str:
|
|
76
|
-
return "get_skill_file"
|
|
77
|
-
|
|
78
|
-
@property
|
|
79
|
-
def description(self) -> str:
|
|
80
|
-
return (
|
|
81
|
-
"Get a file from a skill by name. The file_path should be a relative path within the skill (e.g., 'scripts/extract_text.json'). "
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
@property
|
|
85
|
-
def arguments(self) -> dict:
|
|
86
|
-
return {
|
|
87
|
-
"skill_name": {
|
|
88
|
-
"type": "string",
|
|
89
|
-
"description": "The name of the skill.",
|
|
90
|
-
},
|
|
91
|
-
"file_path": {
|
|
92
|
-
"type": "string",
|
|
93
|
-
"description": "Relative path to the file within the skill (e.g., 'scripts/extract_text.json').",
|
|
94
|
-
},
|
|
95
|
-
"expire": {
|
|
96
|
-
"type": "integer",
|
|
97
|
-
"description": "URL expiration time in seconds (only used for non-parseable files). Defaults to 900 (15 minutes).",
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@property
|
|
102
|
-
def required_arguments(self) -> list[str]:
|
|
103
|
-
return ["skill_name", "file_path"]
|
|
104
|
-
|
|
105
|
-
def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
|
|
106
|
-
"""Get a skill file."""
|
|
107
|
-
skill_name = llm_arguments.get("skill_name")
|
|
108
|
-
file_path = llm_arguments.get("file_path")
|
|
109
|
-
expire = llm_arguments.get("expire")
|
|
110
|
-
|
|
111
|
-
if not file_path:
|
|
112
|
-
raise ValueError("file_path is required")
|
|
113
|
-
if not skill_name:
|
|
114
|
-
raise ValueError("skill_name is required")
|
|
115
|
-
|
|
116
|
-
result = ctx.client.skills.get_file_by_name(
|
|
117
|
-
skill_name=skill_name,
|
|
118
|
-
file_path=file_path,
|
|
119
|
-
expire=expire,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
output_parts = [f"File '{result.path}' (MIME: {result.mime}) from skill '{skill_name}':"]
|
|
123
|
-
|
|
124
|
-
if result.content:
|
|
125
|
-
output_parts.append(f"\nContent (type: {result.content.type}):")
|
|
126
|
-
output_parts.append(result.content.raw)
|
|
127
|
-
|
|
128
|
-
if result.url:
|
|
129
|
-
expire_seconds = expire if expire is not None else 900
|
|
130
|
-
output_parts.append(f"\nDownload URL (expires in {expire_seconds} seconds):")
|
|
131
|
-
output_parts.append(result.url)
|
|
132
|
-
|
|
133
|
-
if not result.content and not result.url:
|
|
134
|
-
return f"File '{result.path}' retrieved but no content or URL returned."
|
|
135
|
-
|
|
136
|
-
return "\n".join(output_parts)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class SkillToolPool(BaseToolPool):
|
|
140
|
-
"""Tool pool for skill operations on Acontext skills."""
|
|
141
|
-
|
|
142
|
-
def format_context(self, client: AcontextClient) -> SkillContext:
|
|
143
|
-
return SkillContext(client=client)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
SKILL_TOOLS = SkillToolPool()
|
|
147
|
-
SKILL_TOOLS.add_tool(GetSkillTool())
|
|
148
|
-
SKILL_TOOLS.add_tool(GetSkillFileTool())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|