acontext 0.0.16__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.
Files changed (44) hide show
  1. {acontext-0.0.16 → acontext-0.0.18}/PKG-INFO +1 -1
  2. {acontext-0.0.16 → acontext-0.0.18}/pyproject.toml +1 -1
  3. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/agent/disk.py +106 -0
  4. acontext-0.0.18/src/acontext/agent/skill.py +253 -0
  5. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/async_skills.py +13 -28
  6. acontext-0.0.18/src/acontext/resources/async_users.py +57 -0
  7. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/disks.py +81 -0
  8. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/skills.py +21 -30
  9. acontext-0.0.18/src/acontext/resources/users.py +57 -0
  10. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/__init__.py +11 -0
  11. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/skill.py +14 -18
  12. acontext-0.0.18/src/acontext/types/user.py +36 -0
  13. acontext-0.0.16/src/acontext/agent/skill.py +0 -148
  14. acontext-0.0.16/src/acontext/resources/async_users.py +0 -20
  15. acontext-0.0.16/src/acontext/resources/users.py +0 -20
  16. {acontext-0.0.16 → acontext-0.0.18}/README.md +0 -0
  17. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/__init__.py +0 -0
  18. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/_constants.py +0 -0
  19. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/_utils.py +0 -0
  20. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/agent/__init__.py +0 -0
  21. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/agent/base.py +0 -0
  22. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/async_client.py +0 -0
  23. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/client.py +0 -0
  24. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/client_types.py +0 -0
  25. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/errors.py +0 -0
  26. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/messages.py +0 -0
  27. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/py.typed +0 -0
  28. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/__init__.py +0 -0
  29. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/async_blocks.py +0 -0
  30. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/async_disks.py +0 -0
  31. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/async_sessions.py +0 -0
  32. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/async_spaces.py +0 -0
  33. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/async_tools.py +0 -0
  34. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/blocks.py +0 -0
  35. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/sessions.py +0 -0
  36. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/spaces.py +0 -0
  37. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/resources/tools.py +0 -0
  38. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/block.py +0 -0
  39. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/common.py +0 -0
  40. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/disk.py +0 -0
  41. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/session.py +0 -0
  42. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/space.py +0 -0
  43. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/types/tool.py +0 -0
  44. {acontext-0.0.16 → acontext-0.0.18}/src/acontext/uploads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acontext
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: Python SDK for the Acontext API
5
5
  Keywords: acontext,sdk,client,api
6
6
  Requires-Dist: httpx>=0.28.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acontext"
3
- version = "0.0.16"
3
+ version = "0.0.18"
4
4
  description = "Python SDK for the Acontext API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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,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
- 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
- )
85
+ # Pydantic ignores extra fields, so ListSkillsOutput directly extracts name/description
86
+ return ListSkillsOutput.model_validate(data)
98
87
 
99
- async def get_by_name(self, name: str) -> Skill:
100
- """Get a skill by its name.
88
+ async def get(self, skill_id: str) -> Skill:
89
+ """Get a skill by its ID.
101
90
 
102
91
  Args:
103
- name: The name of the skill (unique within project).
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
- params = {"name": name}
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 get_file_by_name(
108
+ async def get_file(
123
109
  self,
124
110
  *,
125
- skill_name: str,
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 name.
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
- skill_name: The name of the skill.
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/by_name/{skill_name}/file"
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
-
@@ -0,0 +1,57 @@
1
+ """
2
+ User management endpoints (async).
3
+ """
4
+
5
+ from urllib.parse import quote
6
+
7
+ from .._utils import build_params
8
+ from ..client_types import AsyncRequesterProtocol
9
+ from ..types.user import GetUserResourcesOutput, ListUsersOutput
10
+
11
+
12
+ class AsyncUsersAPI:
13
+ def __init__(self, requester: AsyncRequesterProtocol) -> None:
14
+ self._requester = requester
15
+
16
+ async def list(
17
+ self,
18
+ *,
19
+ limit: int | None = None,
20
+ cursor: str | None = None,
21
+ time_desc: bool | None = None,
22
+ ) -> ListUsersOutput:
23
+ """List all users in the project.
24
+
25
+ Args:
26
+ limit: Maximum number of users to return. If not provided or 0, all users will be returned. Defaults to None.
27
+ cursor: Cursor for pagination. Defaults to None.
28
+ time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
29
+
30
+ Returns:
31
+ ListUsersOutput containing the list of users and pagination information.
32
+ """
33
+ params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
34
+ data = await self._requester.request("GET", "/user/ls", params=params or None)
35
+ return ListUsersOutput.model_validate(data)
36
+
37
+ async def get_resources(self, identifier: str) -> GetUserResourcesOutput:
38
+ """Get resource counts for a user.
39
+
40
+ Args:
41
+ identifier: The user identifier string.
42
+
43
+ Returns:
44
+ GetUserResourcesOutput containing counts for Spaces, Sessions, Disks, and Skills.
45
+ """
46
+ data = await self._requester.request(
47
+ "GET", f"/user/{quote(identifier, safe='')}/resources"
48
+ )
49
+ return GetUserResourcesOutput.model_validate(data)
50
+
51
+ async def delete(self, identifier: str) -> None:
52
+ """Delete a user and cascade delete all associated resources (Space, Session, Disk, Skill).
53
+
54
+ Args:
55
+ identifier: The user identifier string.
56
+ """
57
+ await self._requester.request("DELETE", f"/user/{quote(identifier, safe='')}")
@@ -2,10 +2,14 @@
2
2
  Disk and artifact endpoints.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import json
6
8
  from collections.abc import Mapping
7
9
  from typing import Any, BinaryIO, cast
8
10
 
11
+ from pydantic import TypeAdapter
12
+
9
13
  from .._utils import build_params
10
14
  from ..client_types import RequesterProtocol
11
15
  from ..types.disk import (
@@ -195,8 +199,85 @@ class DiskArtifactsAPI:
195
199
  *,
196
200
  path: str | None = None,
197
201
  ) -> ListArtifactsResp:
202
+ """List artifacts in a disk at a specific path.
203
+
204
+ Args:
205
+ disk_id: The UUID of the disk.
206
+ path: Directory path to list. Defaults to None (root).
207
+
208
+ Returns:
209
+ ListArtifactsResp containing the list of artifacts.
210
+ """
198
211
  params: dict[str, Any] = {}
199
212
  if path is not None:
200
213
  params["path"] = path
201
214
  data = self._requester.request("GET", f"/disk/{disk_id}/artifact/ls", params=params or None)
202
215
  return ListArtifactsResp.model_validate(data)
216
+
217
+ def grep_artifacts(
218
+ self,
219
+ disk_id: str,
220
+ *,
221
+ query: str,
222
+ limit: int = 100,
223
+ ) -> list[Artifact]:
224
+ """Search artifact content using regex pattern.
225
+
226
+ Args:
227
+ disk_id: The disk ID to search in
228
+ query: Regex pattern to search for in file content
229
+ limit: Maximum number of results (default 100, max 1000)
230
+
231
+ Returns:
232
+ List of matching artifacts
233
+
234
+ Example:
235
+ ```python
236
+ # Search for TODO comments in code
237
+ results = client.disks.artifacts.grep_artifacts(
238
+ disk_id="disk-uuid",
239
+ query="TODO.*bug"
240
+ )
241
+ ```
242
+ """
243
+ params = build_params(query=query, limit=limit)
244
+ data = self._requester.request(
245
+ "GET",
246
+ f"/disk/{disk_id}/artifact/grep",
247
+ params=params
248
+ )
249
+ return TypeAdapter(list[Artifact]).validate_python(data)
250
+
251
+ def glob_artifacts(
252
+ self,
253
+ disk_id: str,
254
+ *,
255
+ query: str,
256
+ limit: int = 100,
257
+ ) -> list[Artifact]:
258
+ """Search artifact paths using glob pattern.
259
+
260
+ Args:
261
+ disk_id: The disk ID to search in
262
+ query: Glob pattern (e.g., '**/*.py', '*.txt')
263
+ limit: Maximum number of results (default 100, max 1000)
264
+
265
+ Returns:
266
+ List of matching artifacts
267
+
268
+ Example:
269
+ ```python
270
+ # Find all Python files
271
+ results = client.disks.artifacts.glob_artifacts(
272
+ disk_id="disk-uuid",
273
+ query="**/*.py"
274
+ )
275
+ ```
276
+ """
277
+ params = build_params(query=query, limit=limit)
278
+ data = self._requester.request(
279
+ "GET",
280
+ f"/disk/{disk_id}/artifact/glob",
281
+ params=params
282
+ )
283
+ return TypeAdapter(list[Artifact]).validate_python(data)
@@ -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: FileUpload
29
- | tuple[str, BinaryIO | bytes]
30
- | tuple[str, BinaryIO | bytes, str],
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(user=user, limit=effective_limit, cursor=cursor, time_desc=time_desc)
84
- data = self._requester.request("GET", "/agent_skills", params=params or None)
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 get_by_name(self, name: str) -> Skill:
98
- """Get a skill by its name.
90
+ def get(self, skill_id: str) -> Skill:
91
+ """Get a skill by its ID.
99
92
 
100
93
  Args:
101
- name: The name of the skill (unique within project).
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
- params = {"name": name}
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 get_file_by_name(
110
+ def get_file(
119
111
  self,
120
112
  *,
121
- skill_name: str,
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 name.
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
- skill_name: The name of the skill.
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/by_name/{skill_name}/file"
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
-
@@ -0,0 +1,57 @@
1
+ """
2
+ User management endpoints.
3
+ """
4
+
5
+ from urllib.parse import quote
6
+
7
+ from .._utils import build_params
8
+ from ..client_types import RequesterProtocol
9
+ from ..types.user import GetUserResourcesOutput, ListUsersOutput
10
+
11
+
12
+ class UsersAPI:
13
+ def __init__(self, requester: RequesterProtocol) -> None:
14
+ self._requester = requester
15
+
16
+ def list(
17
+ self,
18
+ *,
19
+ limit: int | None = None,
20
+ cursor: str | None = None,
21
+ time_desc: bool | None = None,
22
+ ) -> ListUsersOutput:
23
+ """List all users in the project.
24
+
25
+ Args:
26
+ limit: Maximum number of users to return. If not provided or 0, all users will be returned. Defaults to None.
27
+ cursor: Cursor for pagination. Defaults to None.
28
+ time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
29
+
30
+ Returns:
31
+ ListUsersOutput containing the list of users and pagination information.
32
+ """
33
+ params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
34
+ data = self._requester.request("GET", "/user/ls", params=params or None)
35
+ return ListUsersOutput.model_validate(data)
36
+
37
+ def get_resources(self, identifier: str) -> GetUserResourcesOutput:
38
+ """Get resource counts for a user.
39
+
40
+ Args:
41
+ identifier: The user identifier string.
42
+
43
+ Returns:
44
+ GetUserResourcesOutput containing counts for Spaces, Sessions, Disks, and Skills.
45
+ """
46
+ data = self._requester.request(
47
+ "GET", f"/user/{quote(identifier, safe='')}/resources"
48
+ )
49
+ return GetUserResourcesOutput.model_validate(data)
50
+
51
+ def delete(self, identifier: str) -> None:
52
+ """Delete a user and cascade delete all associated resources (Space, Session, Disk, Skill).
53
+
54
+ Args:
55
+ identifier: The user identifier string.
56
+ """
57
+ self._requester.request("DELETE", f"/user/{quote(identifier, safe='')}")
@@ -45,6 +45,12 @@ from .skill import (
45
45
  Skill,
46
46
  SkillCatalogItem,
47
47
  )
48
+ from .user import (
49
+ GetUserResourcesOutput,
50
+ ListUsersOutput,
51
+ User,
52
+ UserResourceCounts,
53
+ )
48
54
 
49
55
  __all__ = [
50
56
  # Disk types
@@ -88,4 +94,9 @@ __all__ = [
88
94
  "SkillCatalogItem",
89
95
  "ListSkillsOutput",
90
96
  "GetSkillFileResp",
97
+ # User types
98
+ "GetUserResourcesOutput",
99
+ "ListUsersOutput",
100
+ "User",
101
+ "UserResourceCounts",
91
102
  ]
@@ -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(None, description="Presigned URL for downloading the file (present if file is not parseable)")
68
- content: FileContent | None = Field(None, description="Parsed file content if available (present if file is parseable)")
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
+ )
@@ -0,0 +1,36 @@
1
+ """Type definitions for user resources."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class User(BaseModel):
7
+ """User model representing a user resource."""
8
+
9
+ id: str = Field(..., description="User UUID")
10
+ project_id: str = Field(..., description="Project UUID")
11
+ identifier: str = Field(..., description="User identifier string")
12
+ created_at: str = Field(..., description="ISO 8601 formatted creation timestamp")
13
+ updated_at: str = Field(..., description="ISO 8601 formatted update timestamp")
14
+
15
+
16
+ class ListUsersOutput(BaseModel):
17
+ """Response model for listing users."""
18
+
19
+ items: list[User] = Field(..., description="List of users")
20
+ next_cursor: str | None = Field(None, description="Cursor for pagination")
21
+ has_more: bool = Field(..., description="Whether there are more items")
22
+
23
+
24
+ class UserResourceCounts(BaseModel):
25
+ """Resource counts for a user."""
26
+
27
+ spaces_count: int = Field(..., description="Number of spaces")
28
+ sessions_count: int = Field(..., description="Number of sessions")
29
+ disks_count: int = Field(..., description="Number of disks")
30
+ skills_count: int = Field(..., description="Number of skills")
31
+
32
+
33
+ class GetUserResourcesOutput(BaseModel):
34
+ """Response model for getting user resources."""
35
+
36
+ counts: UserResourceCounts = Field(..., description="Resource counts")
@@ -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())
@@ -1,20 +0,0 @@
1
- """
2
- User management endpoints (async).
3
- """
4
-
5
- from urllib.parse import quote
6
-
7
- from ..client_types import AsyncRequesterProtocol
8
-
9
-
10
- class AsyncUsersAPI:
11
- def __init__(self, requester: AsyncRequesterProtocol) -> None:
12
- self._requester = requester
13
-
14
- async def delete(self, identifier: str) -> None:
15
- """Delete a user and cascade delete all associated resources (Space, Session, Disk, Skill).
16
-
17
- Args:
18
- identifier: The user identifier string.
19
- """
20
- await self._requester.request("DELETE", f"/user/{quote(identifier, safe='')}")
@@ -1,20 +0,0 @@
1
- """
2
- User management endpoints.
3
- """
4
-
5
- from urllib.parse import quote
6
-
7
- from ..client_types import RequesterProtocol
8
-
9
-
10
- class UsersAPI:
11
- def __init__(self, requester: RequesterProtocol) -> None:
12
- self._requester = requester
13
-
14
- def delete(self, identifier: str) -> None:
15
- """Delete a user and cascade delete all associated resources (Space, Session, Disk, Skill).
16
-
17
- Args:
18
- identifier: The user identifier string.
19
- """
20
- self._requester.request("DELETE", f"/user/{quote(identifier, safe='')}")
File without changes