acontext 0.0.14__py3-none-any.whl → 0.0.17__py3-none-any.whl

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.
@@ -0,0 +1,10 @@
1
+ """Agent tools for LLM function calling."""
2
+
3
+ from .disk import DISK_TOOLS
4
+ from .skill import SKILL_TOOLS
5
+
6
+ __all__ = [
7
+ "DISK_TOOLS",
8
+ "SKILL_TOOLS",
9
+ ]
10
+
acontext/agent/disk.py CHANGED
@@ -328,6 +328,110 @@ class DownloadFileTool(BaseTool):
328
328
  return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"
329
329
 
330
330
 
331
+ class GrepArtifactsTool(BaseTool):
332
+ """Tool for searching artifact content using regex patterns."""
333
+
334
+ @property
335
+ def name(self) -> str:
336
+ return "grep_artifacts"
337
+
338
+ @property
339
+ def description(self) -> str:
340
+ return "Search for text patterns within file contents using regex. Only searches text-based files (code, markdown, json, csv, etc.). Use this to find specific code patterns, TODO comments, function definitions, or any text content."
341
+
342
+ @property
343
+ def arguments(self) -> dict:
344
+ return {
345
+ "query": {
346
+ "type": "string",
347
+ "description": "Regex pattern to search for (e.g., 'TODO.*', 'function.*calculate', 'import.*pandas')",
348
+ },
349
+ "limit": {
350
+ "type": "integer",
351
+ "description": "Maximum number of results to return (default 100)",
352
+ },
353
+ }
354
+
355
+ @property
356
+ def required_arguments(self) -> list[str]:
357
+ return ["query"]
358
+
359
+ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
360
+ """Search artifact content using regex pattern."""
361
+ query = llm_arguments.get("query")
362
+ limit = llm_arguments.get("limit", 100)
363
+
364
+ if not query:
365
+ raise ValueError("query is required")
366
+
367
+ results = ctx.client.disks.artifacts.grep_artifacts(
368
+ ctx.disk_id,
369
+ query=query,
370
+ limit=limit,
371
+ )
372
+
373
+ if not results:
374
+ return f"No matches found for pattern '{query}'"
375
+
376
+ matches = []
377
+ for artifact in results:
378
+ matches.append(f"{artifact.path}{artifact.filename}")
379
+
380
+ return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(matches)
381
+
382
+
383
+ class GlobArtifactsTool(BaseTool):
384
+ """Tool for finding files by path pattern using glob syntax."""
385
+
386
+ @property
387
+ def name(self) -> str:
388
+ return "glob_artifacts"
389
+
390
+ @property
391
+ def description(self) -> str:
392
+ return "Find files by path pattern using glob syntax. Use * for any characters, ? for single character, ** for recursive directories. Perfect for finding files by extension or location."
393
+
394
+ @property
395
+ def arguments(self) -> dict:
396
+ return {
397
+ "query": {
398
+ "type": "string",
399
+ "description": "Glob pattern (e.g., '**/*.py' for all Python files, '*.txt' for text files in root, '/docs/**/*.md' for markdown in docs)",
400
+ },
401
+ "limit": {
402
+ "type": "integer",
403
+ "description": "Maximum number of results to return (default 100)",
404
+ },
405
+ }
406
+
407
+ @property
408
+ def required_arguments(self) -> list[str]:
409
+ return ["query"]
410
+
411
+ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
412
+ """Search artifact paths using glob pattern."""
413
+ query = llm_arguments.get("query")
414
+ limit = llm_arguments.get("limit", 100)
415
+
416
+ if not query:
417
+ raise ValueError("query is required")
418
+
419
+ results = ctx.client.disks.artifacts.glob_artifacts(
420
+ ctx.disk_id,
421
+ query=query,
422
+ limit=limit,
423
+ )
424
+
425
+ if not results:
426
+ return f"No files found matching pattern '{query}'"
427
+
428
+ matches = []
429
+ for artifact in results:
430
+ matches.append(f"{artifact.path}{artifact.filename}")
431
+
432
+ return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(matches)
433
+
434
+
331
435
  class DiskToolPool(BaseToolPool):
332
436
  """Tool pool for disk operations on Acontext disks."""
333
437
 
@@ -340,6 +444,8 @@ DISK_TOOLS.add_tool(WriteFileTool())
340
444
  DISK_TOOLS.add_tool(ReadFileTool())
341
445
  DISK_TOOLS.add_tool(ReplaceStringTool())
342
446
  DISK_TOOLS.add_tool(ListTool())
447
+ DISK_TOOLS.add_tool(GrepArtifactsTool())
448
+ DISK_TOOLS.add_tool(GlobArtifactsTool())
343
449
  DISK_TOOLS.add_tool(DownloadFileTool())
344
450
 
345
451
 
@@ -0,0 +1,148 @@
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())
acontext/async_client.py CHANGED
@@ -17,6 +17,8 @@ from .resources.async_blocks import AsyncBlocksAPI as AsyncBlocksAPI
17
17
  from .resources.async_sessions import AsyncSessionsAPI as AsyncSessionsAPI
18
18
  from .resources.async_spaces import AsyncSpacesAPI as AsyncSpacesAPI
19
19
  from .resources.async_tools import AsyncToolsAPI as AsyncToolsAPI
20
+ from .resources.async_skills import AsyncSkillsAPI as AsyncSkillsAPI
21
+ from .resources.async_users import AsyncUsersAPI as AsyncUsersAPI
20
22
 
21
23
 
22
24
  class AcontextAsyncClient:
@@ -109,6 +111,8 @@ class AcontextAsyncClient:
109
111
  self.artifacts = self.disks.artifacts
110
112
  self.blocks = AsyncBlocksAPI(self)
111
113
  self.tools = AsyncToolsAPI(self)
114
+ self.skills = AsyncSkillsAPI(self)
115
+ self.users = AsyncUsersAPI(self)
112
116
 
113
117
  @property
114
118
  def base_url(self) -> str:
acontext/client.py CHANGED
@@ -17,6 +17,8 @@ from .resources.blocks import BlocksAPI as BlocksAPI
17
17
  from .resources.sessions import SessionsAPI as SessionsAPI
18
18
  from .resources.spaces import SpacesAPI as SpacesAPI
19
19
  from .resources.tools import ToolsAPI as ToolsAPI
20
+ from .resources.skills import SkillsAPI as SkillsAPI
21
+ from .resources.users import UsersAPI as UsersAPI
20
22
 
21
23
 
22
24
  class AcontextClient:
@@ -109,6 +111,8 @@ class AcontextClient:
109
111
  self.artifacts = self.disks.artifacts
110
112
  self.blocks = BlocksAPI(self)
111
113
  self.tools = ToolsAPI(self)
114
+ self.skills = SkillsAPI(self)
115
+ self.users = UsersAPI(self)
112
116
 
113
117
  @property
114
118
  def base_url(self) -> str:
@@ -5,11 +5,15 @@ from .async_disks import AsyncDisksAPI, AsyncDiskArtifactsAPI
5
5
  from .async_sessions import AsyncSessionsAPI
6
6
  from .async_spaces import AsyncSpacesAPI
7
7
  from .async_tools import AsyncToolsAPI
8
+ from .async_skills import AsyncSkillsAPI
9
+ from .async_users import AsyncUsersAPI
8
10
  from .blocks import BlocksAPI
9
11
  from .disks import DisksAPI, DiskArtifactsAPI
10
12
  from .sessions import SessionsAPI
11
13
  from .spaces import SpacesAPI
12
14
  from .tools import ToolsAPI
15
+ from .skills import SkillsAPI
16
+ from .users import UsersAPI
13
17
 
14
18
  __all__ = [
15
19
  "DisksAPI",
@@ -18,10 +22,14 @@ __all__ = [
18
22
  "SessionsAPI",
19
23
  "SpacesAPI",
20
24
  "ToolsAPI",
25
+ "SkillsAPI",
26
+ "UsersAPI",
21
27
  "AsyncDisksAPI",
22
28
  "AsyncDiskArtifactsAPI",
23
29
  "AsyncBlocksAPI",
24
30
  "AsyncSessionsAPI",
25
31
  "AsyncSpacesAPI",
26
32
  "AsyncToolsAPI",
33
+ "AsyncSkillsAPI",
34
+ "AsyncUsersAPI",
27
35
  ]
@@ -27,6 +27,7 @@ class AsyncDisksAPI:
27
27
  async def list(
28
28
  self,
29
29
  *,
30
+ user: str | None = None,
30
31
  limit: int | None = None,
31
32
  cursor: str | None = None,
32
33
  time_desc: bool | None = None,
@@ -34,6 +35,7 @@ class AsyncDisksAPI:
34
35
  """List all disks in the project.
35
36
 
36
37
  Args:
38
+ user: Filter by user identifier. Defaults to None.
37
39
  limit: Maximum number of disks to return. Defaults to None.
38
40
  cursor: Cursor for pagination. Defaults to None.
39
41
  time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
@@ -41,17 +43,23 @@ class AsyncDisksAPI:
41
43
  Returns:
42
44
  ListDisksOutput containing the list of disks and pagination information.
43
45
  """
44
- params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
46
+ params = build_params(user=user, limit=limit, cursor=cursor, time_desc=time_desc)
45
47
  data = await self._requester.request("GET", "/disk", params=params or None)
46
48
  return ListDisksOutput.model_validate(data)
47
49
 
48
- async def create(self) -> Disk:
50
+ async def create(self, *, user: str | None = None) -> Disk:
49
51
  """Create a new disk.
50
52
 
53
+ Args:
54
+ user: Optional user identifier string. Defaults to None.
55
+
51
56
  Returns:
52
57
  The created Disk object.
53
58
  """
54
- data = await self._requester.request("POST", "/disk")
59
+ payload: dict[str, Any] = {}
60
+ if user is not None:
61
+ payload["user"] = user
62
+ data = await self._requester.request("POST", "/disk", json_data=payload or None)
55
63
  return Disk.model_validate(data)
56
64
 
57
65
  async def delete(self, disk_id: str) -> None:
@@ -37,6 +37,7 @@ class AsyncSessionsAPI:
37
37
  async def list(
38
38
  self,
39
39
  *,
40
+ user: str | None = None,
40
41
  space_id: str | None = None,
41
42
  not_connected: bool | None = None,
42
43
  limit: int | None = None,
@@ -46,6 +47,7 @@ class AsyncSessionsAPI:
46
47
  """List all sessions in the project.
47
48
 
48
49
  Args:
50
+ user: Filter by user identifier. Defaults to None.
49
51
  space_id: Filter sessions by space ID. Defaults to None.
50
52
  not_connected: Filter sessions that are not connected to a space. Defaults to None.
51
53
  limit: Maximum number of sessions to return. Defaults to None.
@@ -56,6 +58,8 @@ class AsyncSessionsAPI:
56
58
  ListSessionsOutput containing the list of sessions and pagination information.
57
59
  """
58
60
  params: dict[str, Any] = {}
61
+ if user:
62
+ params["user"] = user
59
63
  if space_id:
60
64
  params["space_id"] = space_id
61
65
  params.update(
@@ -72,12 +76,14 @@ class AsyncSessionsAPI:
72
76
  async def create(
73
77
  self,
74
78
  *,
79
+ user: str | None = None,
75
80
  space_id: str | None = None,
76
81
  configs: Mapping[str, Any] | None = None,
77
82
  ) -> Session:
78
83
  """Create a new session.
79
84
 
80
85
  Args:
86
+ user: Optional user identifier string. Defaults to None.
81
87
  space_id: Optional space ID to associate with the session. Defaults to None.
82
88
  configs: Optional session configuration dictionary. Defaults to None.
83
89
 
@@ -85,6 +91,8 @@ class AsyncSessionsAPI:
85
91
  The created Session object.
86
92
  """
87
93
  payload: dict[str, Any] = {}
94
+ if user:
95
+ payload["user"] = user
88
96
  if space_id:
89
97
  payload["space_id"] = space_id
90
98
  if configs is not None:
@@ -267,6 +275,7 @@ class AsyncSessionsAPI:
267
275
  format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
268
276
  time_desc: bool | None = None,
269
277
  edit_strategies: Optional[List[EditStrategy]] = None,
278
+ pin_editing_strategies_at_message: str | None = None,
270
279
  ) -> GetMessagesOutput:
271
280
  """Get messages for a session.
272
281
 
@@ -283,6 +292,12 @@ class AsyncSessionsAPI:
283
292
  - Remove tool results: [{"type": "remove_tool_result", "params": {"keep_recent_n_tool_results": 3}}]
284
293
  - Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
285
294
  Defaults to None.
295
+ pin_editing_strategies_at_message: Message ID to pin editing strategies at.
296
+ When provided, strategies are only applied to messages up to and including
297
+ this message ID, keeping subsequent messages unchanged. This helps maintain
298
+ prompt cache stability by preserving a stable prefix. The response includes
299
+ edit_at_message_id indicating where strategies were applied. Pass this value
300
+ in subsequent requests to maintain cache hits. Defaults to None.
286
301
 
287
302
  Returns:
288
303
  GetMessagesOutput containing the list of messages and pagination information.
@@ -300,6 +315,10 @@ class AsyncSessionsAPI:
300
315
  )
301
316
  if edit_strategies is not None:
302
317
  params["edit_strategies"] = json.dumps(edit_strategies)
318
+ if pin_editing_strategies_at_message is not None:
319
+ params["pin_editing_strategies_at_message"] = (
320
+ pin_editing_strategies_at_message
321
+ )
303
322
  data = await self._requester.request(
304
323
  "GET", f"/session/{session_id}/messages", params=params or None
305
324
  )
@@ -348,20 +367,21 @@ class AsyncSessionsAPI:
348
367
  )
349
368
  return TokenCounts.model_validate(data)
350
369
 
370
+
351
371
  async def messages_observing_status(self, session_id: str) -> MessageObservingStatus:
352
- """Get message observing status counts for a session.
353
-
354
- Returns the count of messages by their observing status:
355
- observed, in_process, and pending.
356
-
357
- Args:
358
- session_id: The UUID of the session.
359
-
360
- Returns:
361
- MessageObservingStatus object containing observed, in_process,
362
- pending counts and updated_at timestamp.
363
- """
364
- data = await self._requester.request(
365
- "GET", f"/session/{session_id}/observing_status"
366
- )
367
- return MessageObservingStatus.model_validate(data)
372
+ """Get message observing status counts for a session.
373
+
374
+ Returns the count of messages by their observing status:
375
+ observed, in_process, and pending.
376
+
377
+ Args:
378
+ session_id: The UUID of the session.
379
+
380
+ Returns:
381
+ MessageObservingStatus object containing observed, in_process,
382
+ pending counts and updated_at timestamp.
383
+ """
384
+ data = await self._requester.request(
385
+ "GET", f"/session/{session_id}/observing_status"
386
+ )
387
+ return MessageObservingStatus.model_validate(data)
@@ -0,0 +1,150 @@
1
+ """
2
+ Skills endpoints (async).
3
+ """
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from typing import Any, BinaryIO, cast
8
+
9
+ from .._utils import build_params
10
+ from ..client_types import AsyncRequesterProtocol
11
+ from ..types.skill import (
12
+ GetSkillFileResp,
13
+ ListSkillsOutput,
14
+ Skill,
15
+ SkillCatalogItem,
16
+ _ListSkillsResponse,
17
+ )
18
+ from ..uploads import FileUpload, normalize_file_upload
19
+
20
+
21
+ class AsyncSkillsAPI:
22
+ def __init__(self, requester: AsyncRequesterProtocol) -> None:
23
+ self._requester = requester
24
+
25
+ async def create(
26
+ self,
27
+ *,
28
+ file: FileUpload
29
+ | tuple[str, BinaryIO | bytes]
30
+ | tuple[str, BinaryIO | bytes, str],
31
+ user: str | None = None,
32
+ meta: Mapping[str, Any] | None = None,
33
+ ) -> Skill:
34
+ """Create a new skill by uploading a ZIP file.
35
+
36
+ The ZIP file must contain a SKILL.md file (case-insensitive) with YAML format
37
+ containing 'name' and 'description' fields.
38
+
39
+ Args:
40
+ file: The ZIP file to upload (FileUpload object or tuple format).
41
+ user: Optional user identifier string. Defaults to None.
42
+ meta: Custom metadata as JSON-serializable dict, defaults to None.
43
+
44
+ Returns:
45
+ Skill containing the created skill information.
46
+ """
47
+ upload = normalize_file_upload(file)
48
+ files = {"file": upload.as_httpx()}
49
+ form: dict[str, Any] = {}
50
+ if user is not None:
51
+ form["user"] = user
52
+ if meta is not None:
53
+ form["meta"] = json.dumps(cast(Mapping[str, Any], meta))
54
+ data = await self._requester.request(
55
+ "POST",
56
+ "/agent_skills",
57
+ data=form or None,
58
+ files=files,
59
+ )
60
+ return Skill.model_validate(data)
61
+
62
+ async def list_catalog(
63
+ self,
64
+ *,
65
+ user: str | None = None,
66
+ limit: int | None = None,
67
+ cursor: str | None = None,
68
+ time_desc: bool | None = None,
69
+ ) -> ListSkillsOutput:
70
+ """Get a catalog of skills (names and descriptions only) with pagination.
71
+
72
+ Args:
73
+ user: Filter by user identifier. Defaults to None.
74
+ limit: Maximum number of skills per page (defaults to 100, max 200).
75
+ cursor: Cursor for pagination to fetch the next page (optional).
76
+ time_desc: Order by created_at descending if True, ascending if False (defaults to False).
77
+
78
+ Returns:
79
+ ListSkillsOutput containing skills with name and description for the current page,
80
+ along with pagination information (next_cursor and has_more).
81
+ """
82
+ effective_limit = limit if limit is not None else 100
83
+ params = build_params(user=user, limit=effective_limit, cursor=cursor, time_desc=time_desc)
84
+ data = await self._requester.request(
85
+ "GET", "/agent_skills", params=params or None
86
+ )
87
+ api_response = _ListSkillsResponse.model_validate(data)
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
+ )
98
+
99
+ async def get_by_name(self, name: str) -> Skill:
100
+ """Get a skill by its name.
101
+
102
+ Args:
103
+ name: The name of the skill (unique within project).
104
+
105
+ Returns:
106
+ Skill containing the skill information.
107
+ """
108
+ params = {"name": name}
109
+ data = await self._requester.request(
110
+ "GET", "/agent_skills/by_name", params=params
111
+ )
112
+ return Skill.model_validate(data)
113
+
114
+ async def delete(self, skill_id: str) -> None:
115
+ """Delete a skill by its ID.
116
+
117
+ Args:
118
+ skill_id: The UUID of the skill to delete.
119
+ """
120
+ await self._requester.request("DELETE", f"/agent_skills/{skill_id}")
121
+
122
+ async def get_file_by_name(
123
+ self,
124
+ *,
125
+ skill_name: str,
126
+ file_path: str,
127
+ expire: int | None = None,
128
+ ) -> GetSkillFileResp:
129
+ """Get a file from a skill by name.
130
+
131
+ The backend automatically returns content for parseable text files, or a presigned URL
132
+ for non-parseable files (binary, images, etc.).
133
+
134
+ Args:
135
+ skill_name: The name of the skill.
136
+ file_path: Relative path to the file within the skill (e.g., 'scripts/extract_text.json').
137
+ expire: URL expiration time in seconds. Defaults to 900 (15 minutes).
138
+
139
+ Returns:
140
+ GetSkillFileResp containing the file path, MIME type, and either content or URL.
141
+ """
142
+ endpoint = f"/agent_skills/by_name/{skill_name}/file"
143
+
144
+ params = {"file_path": file_path}
145
+ if expire is not None:
146
+ params["expire"] = expire
147
+
148
+ data = await self._requester.request("GET", endpoint, params=params)
149
+ return GetSkillFileResp.model_validate(data)
150
+