acontext 0.0.13__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 (33) hide show
  1. acontext-0.0.13/PKG-INFO +34 -0
  2. acontext-0.0.13/README.md +19 -0
  3. acontext-0.0.13/pyproject.toml +25 -0
  4. acontext-0.0.13/src/acontext/__init__.py +48 -0
  5. acontext-0.0.13/src/acontext/_constants.py +14 -0
  6. acontext-0.0.13/src/acontext/_utils.py +42 -0
  7. acontext-0.0.13/src/acontext/agent/__init__.py +0 -0
  8. acontext-0.0.13/src/acontext/agent/base.py +106 -0
  9. acontext-0.0.13/src/acontext/agent/disk.py +325 -0
  10. acontext-0.0.13/src/acontext/async_client.py +227 -0
  11. acontext-0.0.13/src/acontext/client.py +226 -0
  12. acontext-0.0.13/src/acontext/client_types.py +36 -0
  13. acontext-0.0.13/src/acontext/errors.py +44 -0
  14. acontext-0.0.13/src/acontext/messages.py +75 -0
  15. acontext-0.0.13/src/acontext/py.typed +0 -0
  16. acontext-0.0.13/src/acontext/resources/__init__.py +27 -0
  17. acontext-0.0.13/src/acontext/resources/async_blocks.py +164 -0
  18. acontext-0.0.13/src/acontext/resources/async_disks.py +195 -0
  19. acontext-0.0.13/src/acontext/resources/async_sessions.py +367 -0
  20. acontext-0.0.13/src/acontext/resources/async_spaces.py +190 -0
  21. acontext-0.0.13/src/acontext/resources/async_tools.py +34 -0
  22. acontext-0.0.13/src/acontext/resources/blocks.py +163 -0
  23. acontext-0.0.13/src/acontext/resources/disks.py +194 -0
  24. acontext-0.0.13/src/acontext/resources/sessions.py +368 -0
  25. acontext-0.0.13/src/acontext/resources/spaces.py +188 -0
  26. acontext-0.0.13/src/acontext/resources/tools.py +34 -0
  27. acontext-0.0.13/src/acontext/types/__init__.py +78 -0
  28. acontext-0.0.13/src/acontext/types/block.py +26 -0
  29. acontext-0.0.13/src/acontext/types/disk.py +65 -0
  30. acontext-0.0.13/src/acontext/types/session.py +271 -0
  31. acontext-0.0.13/src/acontext/types/space.py +69 -0
  32. acontext-0.0.13/src/acontext/types/tool.py +31 -0
  33. acontext-0.0.13/src/acontext/uploads.py +44 -0
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.3
2
+ Name: acontext
3
+ Version: 0.0.13
4
+ Summary: Python SDK for the Acontext API
5
+ Keywords: acontext,sdk,client,api
6
+ Requires-Dist: httpx>=0.28.1
7
+ Requires-Dist: openai>=2.6.1
8
+ Requires-Dist: anthropic>=0.72.0
9
+ Requires-Dist: pydantic>=2.12.3
10
+ Requires-Python: >=3.10
11
+ Project-URL: Homepage, https://github.com/memodb-io/Acontext
12
+ Project-URL: Issues, https://github.com/memodb-io/Acontext/issues
13
+ Project-URL: Repository, https://github.com/memodb-io/Acontext
14
+ Description-Content-Type: text/markdown
15
+
16
+ ## acontext client for python
17
+
18
+ Python SDK for interacting with the Acontext REST API.
19
+
20
+ ### Installation
21
+
22
+ ```bash
23
+ pip install acontext
24
+ ```
25
+
26
+ > Requires Python 3.10 or newer.
27
+
28
+
29
+
30
+
31
+
32
+ # 🔍 Document
33
+
34
+ To understand more about this SDK, please view [our docs](https://docs.acontext.io/) and [api references](https://docs.acontext.io/api-reference/introduction)
@@ -0,0 +1,19 @@
1
+ ## acontext client for python
2
+
3
+ Python SDK for interacting with the Acontext REST API.
4
+
5
+ ### Installation
6
+
7
+ ```bash
8
+ pip install acontext
9
+ ```
10
+
11
+ > Requires Python 3.10 or newer.
12
+
13
+
14
+
15
+
16
+
17
+ # 🔍 Document
18
+
19
+ To understand more about this SDK, please view [our docs](https://docs.acontext.io/) and [api references](https://docs.acontext.io/api-reference/introduction)
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "acontext"
3
+ version = "0.0.13"
4
+ description = "Python SDK for the Acontext API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "httpx>=0.28.1",
9
+ "openai>=2.6.1",
10
+ "anthropic>=0.72.0",
11
+ "pydantic>=2.12.3",
12
+ ]
13
+ keywords = ["acontext", "sdk", "client", "api"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/memodb-io/Acontext"
17
+ Repository = "https://github.com/memodb-io/Acontext"
18
+ Issues = "https://github.com/memodb-io/Acontext/issues"
19
+
20
+ [dependency-groups]
21
+ dev = ["pytest", "ruff", "pytest-asyncio"]
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.9.2,<0.10.0"]
25
+ build-backend = "uv_build"
@@ -0,0 +1,48 @@
1
+ """
2
+ Python SDK for the Acontext API.
3
+ """
4
+
5
+ from importlib import metadata as _metadata
6
+
7
+ from .async_client import AcontextAsyncClient
8
+ from .client import AcontextClient, FileUpload, MessagePart
9
+ from .messages import AcontextMessage
10
+ from .resources import (
11
+ AsyncBlocksAPI,
12
+ AsyncDiskArtifactsAPI,
13
+ AsyncDisksAPI,
14
+ AsyncSessionsAPI,
15
+ AsyncSpacesAPI,
16
+ BlocksAPI,
17
+ DiskArtifactsAPI,
18
+ DisksAPI,
19
+ SessionsAPI,
20
+ SpacesAPI,
21
+ )
22
+ from .types import Task, TaskData
23
+
24
+ __all__ = [
25
+ "AcontextClient",
26
+ "AcontextAsyncClient",
27
+ "FileUpload",
28
+ "MessagePart",
29
+ "AcontextMessage",
30
+ "DisksAPI",
31
+ "DiskArtifactsAPI",
32
+ "BlocksAPI",
33
+ "SessionsAPI",
34
+ "SpacesAPI",
35
+ "AsyncDisksAPI",
36
+ "AsyncDiskArtifactsAPI",
37
+ "AsyncBlocksAPI",
38
+ "AsyncSessionsAPI",
39
+ "AsyncSpacesAPI",
40
+ "Task",
41
+ "TaskData",
42
+ "__version__",
43
+ ]
44
+
45
+ try:
46
+ __version__ = _metadata.version("acontext")
47
+ except _metadata.PackageNotFoundError: # pragma: no cover - local/checkout usage
48
+ __version__ = "0.0.0"
@@ -0,0 +1,14 @@
1
+ """
2
+ Internal constants shared across the Python SDK.
3
+ """
4
+
5
+ from importlib import metadata as _metadata
6
+
7
+ DEFAULT_BASE_URL = "https://api.acontext.app/api/v1"
8
+
9
+ try:
10
+ _VERSION = _metadata.version("acontext-py")
11
+ except _metadata.PackageNotFoundError: # pragma: no cover - local/checkout usage
12
+ _VERSION = "0.0.0"
13
+
14
+ DEFAULT_USER_AGENT = f"acontext-py/{_VERSION}"
@@ -0,0 +1,42 @@
1
+ """Utility functions for the acontext Python client."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def bool_to_str(value: bool) -> str:
7
+ """Convert a boolean value to string representation used by the API.
8
+
9
+ Args:
10
+ value: The boolean value to convert.
11
+
12
+ Returns:
13
+ "true" if value is True, "false" otherwise.
14
+ """
15
+ return "true" if value else "false"
16
+
17
+
18
+ def build_params(**kwargs: Any) -> dict[str, Any]:
19
+ """Build query parameters dictionary, filtering None values and converting booleans.
20
+
21
+ This function filters out None values and converts boolean values to their
22
+ string representations ("true" or "false") as expected by the API.
23
+
24
+ Args:
25
+ **kwargs: Keyword arguments to build parameters from.
26
+
27
+ Returns:
28
+ Dictionary with non-None parameters, with booleans converted to strings.
29
+
30
+ Example:
31
+ >>> build_params(limit=10, cursor=None, time_desc=True)
32
+ {'limit': 10, 'time_desc': 'true'}
33
+ """
34
+ params: dict[str, Any] = {}
35
+ for key, value in kwargs.items():
36
+ if value is not None:
37
+ if isinstance(value, bool):
38
+ params[key] = bool_to_str(value)
39
+ else:
40
+ params[key] = value
41
+ return params
42
+
File without changes
@@ -0,0 +1,106 @@
1
+ class BaseContext:
2
+ pass
3
+
4
+
5
+ class BaseConverter:
6
+ def to_openai_tool_schema(self) -> dict:
7
+ raise NotImplementedError
8
+
9
+ def to_anthropic_tool_schema(self) -> dict:
10
+ raise NotImplementedError
11
+
12
+ def to_gemini_tool_schema(self) -> dict:
13
+ raise NotImplementedError
14
+
15
+
16
+ class BaseTool(BaseConverter):
17
+ @property
18
+ def name(self) -> str:
19
+ raise NotImplementedError
20
+
21
+ @property
22
+ def description(self) -> str:
23
+ raise NotImplementedError
24
+
25
+ @property
26
+ def arguments(self) -> dict:
27
+ raise NotImplementedError
28
+
29
+ @property
30
+ def required_arguments(self) -> list[str]:
31
+ raise NotImplementedError
32
+
33
+ def execute(self, ctx: BaseContext, llm_arguments: dict) -> str:
34
+ raise NotImplementedError
35
+
36
+ def to_openai_tool_schema(self) -> dict:
37
+ return {
38
+ "type": "function",
39
+ "function": {
40
+ "name": self.name,
41
+ "description": self.description,
42
+ "parameters": {
43
+ "type": "object",
44
+ "properties": self.arguments,
45
+ "required": self.required_arguments,
46
+ },
47
+ },
48
+ }
49
+
50
+ def to_anthropic_tool_schema(self) -> dict:
51
+ return {
52
+ "name": self.name,
53
+ "description": self.description,
54
+ "input_schema": {
55
+ "type": "object",
56
+ "properties": self.arguments,
57
+ "required": self.required_arguments,
58
+ },
59
+ }
60
+
61
+ def to_gemini_tool_schema(self) -> dict:
62
+ return {
63
+ "name": self.name,
64
+ "description": self.description,
65
+ "parameters": {
66
+ "type": "object",
67
+ "properties": self.arguments,
68
+ "required": self.required_arguments,
69
+ },
70
+ }
71
+
72
+
73
+ class BaseToolPool(BaseConverter):
74
+ def __init__(self):
75
+ self.tools: dict[str, BaseTool] = {}
76
+
77
+ def add_tool(self, tool: BaseTool):
78
+ self.tools[tool.name] = tool
79
+
80
+ def remove_tool(self, tool_name: str):
81
+ self.tools.pop(tool_name)
82
+
83
+ def extent_tool_pool(self, pool: "BaseToolPool"):
84
+ self.tools.update(pool.tools)
85
+
86
+ def execute_tool(
87
+ self, ctx: BaseContext, tool_name: str, llm_arguments: dict
88
+ ) -> str:
89
+ tool = self.tools[tool_name]
90
+ r = tool.execute(ctx, llm_arguments)
91
+ return r.strip()
92
+
93
+ def tool_exists(self, tool_name: str) -> bool:
94
+ return tool_name in self.tools
95
+
96
+ def to_openai_tool_schema(self) -> list[dict]:
97
+ return [tool.to_openai_tool_schema() for tool in self.tools.values()]
98
+
99
+ def to_anthropic_tool_schema(self) -> list[dict]:
100
+ return [tool.to_anthropic_tool_schema() for tool in self.tools.values()]
101
+
102
+ def to_gemini_tool_schema(self) -> list[dict]:
103
+ return [tool.to_gemini_tool_schema() for tool in self.tools.values()]
104
+
105
+ def format_context(self, *args, **kwargs) -> BaseContext:
106
+ raise NotImplementedError
@@ -0,0 +1,325 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .base import BaseContext, BaseTool, BaseToolPool
4
+ from ..client import AcontextClient
5
+ from ..uploads import FileUpload
6
+
7
+
8
+ @dataclass
9
+ class DiskContext(BaseContext):
10
+ client: AcontextClient
11
+ disk_id: str
12
+
13
+
14
+ def _normalize_path(path: str | None) -> str:
15
+ """Normalize a file path to ensure it starts with '/'."""
16
+ if not path:
17
+ return "/"
18
+ normalized = path if path.startswith("/") else f"/{path}"
19
+ if not normalized.endswith("/"):
20
+ normalized += "/"
21
+ return normalized
22
+
23
+
24
+ class WriteFileTool(BaseTool):
25
+ """Tool for writing text content to a file on the Acontext disk."""
26
+
27
+ @property
28
+ def name(self) -> str:
29
+ return "write_file"
30
+
31
+ @property
32
+ def description(self) -> str:
33
+ return "Write text content to a file in the file system. Creates the file if it doesn't exist, overwrites if it does."
34
+
35
+ @property
36
+ def arguments(self) -> dict:
37
+ return {
38
+ "file_path": {
39
+ "type": "string",
40
+ "description": "Optional folder path to organize files, e.g. '/notes/' or '/documents/'. Defaults to root '/' if not specified.",
41
+ },
42
+ "filename": {
43
+ "type": "string",
44
+ "description": "Filename such as 'report.md' or 'demo.txt'.",
45
+ },
46
+ "content": {
47
+ "type": "string",
48
+ "description": "Text content to write to the file.",
49
+ },
50
+ }
51
+
52
+ @property
53
+ def required_arguments(self) -> list[str]:
54
+ return ["filename", "content"]
55
+
56
+ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
57
+ """Write text content to a file."""
58
+ filename = llm_arguments.get("filename")
59
+ content = llm_arguments.get("content")
60
+ file_path = llm_arguments.get("file_path")
61
+
62
+ if not filename:
63
+ raise ValueError("filename is required")
64
+ if not content:
65
+ raise ValueError("content is required")
66
+
67
+ normalized_path = _normalize_path(file_path)
68
+ payload = FileUpload(filename=filename, content=content.encode("utf-8"))
69
+ artifact = ctx.client.disks.artifacts.upsert(
70
+ ctx.disk_id,
71
+ file=payload,
72
+ file_path=normalized_path,
73
+ )
74
+ return f"File '{artifact.filename}' written successfully to '{artifact.path}'"
75
+
76
+
77
+ class ReadFileTool(BaseTool):
78
+ """Tool for reading a text file from the Acontext disk."""
79
+
80
+ @property
81
+ def name(self) -> str:
82
+ return "read_file"
83
+
84
+ @property
85
+ def description(self) -> str:
86
+ return "Read a text file from the file system and return its content."
87
+
88
+ @property
89
+ def arguments(self) -> dict:
90
+ return {
91
+ "file_path": {
92
+ "type": "string",
93
+ "description": "Optional directory path where the file is located, e.g. '/notes/'. Defaults to root '/' if not specified.",
94
+ },
95
+ "filename": {
96
+ "type": "string",
97
+ "description": "Filename to read.",
98
+ },
99
+ "line_offset": {
100
+ "type": "integer",
101
+ "description": "The line number to start reading from. Default to 0",
102
+ },
103
+ "line_limit": {
104
+ "type": "integer",
105
+ "description": "The maximum number of lines to return. Default to 100",
106
+ },
107
+ }
108
+
109
+ @property
110
+ def required_arguments(self) -> list[str]:
111
+ return ["filename"]
112
+
113
+ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
114
+ """Read a text file and return its content preview."""
115
+ filename = llm_arguments.get("filename")
116
+ file_path = llm_arguments.get("file_path")
117
+ line_offset = llm_arguments.get("line_offset", 0)
118
+ line_limit = llm_arguments.get("line_limit", 100)
119
+
120
+ if not filename:
121
+ raise ValueError("filename is required")
122
+
123
+ normalized_path = _normalize_path(file_path)
124
+ result = ctx.client.disks.artifacts.get(
125
+ ctx.disk_id,
126
+ file_path=normalized_path,
127
+ filename=filename,
128
+ with_content=True,
129
+ )
130
+
131
+ if not result.content:
132
+ raise RuntimeError("Failed to read file: server did not return content.")
133
+
134
+ content_str = result.content.raw
135
+ lines = content_str.split("\n")
136
+ line_start = min(line_offset, len(lines) - 1)
137
+ line_end = min(line_start + line_limit, len(lines))
138
+ preview = "\n".join(lines[line_start:line_end])
139
+ return f"[{normalized_path}{filename} - showing L{line_start}-{line_end} of {len(lines)} lines]\n{preview}"
140
+
141
+
142
+ class ReplaceStringTool(BaseTool):
143
+ """Tool for replacing an old string with a new string in a file on the Acontext disk."""
144
+
145
+ @property
146
+ def name(self) -> str:
147
+ return "replace_string"
148
+
149
+ @property
150
+ def description(self) -> str:
151
+ return "Replace an old string with a new string in a file. Reads the file, performs the replacement, and writes it back."
152
+
153
+ @property
154
+ def arguments(self) -> dict:
155
+ return {
156
+ "file_path": {
157
+ "type": "string",
158
+ "description": "Optional directory path where the file is located, e.g. '/notes/'. Defaults to root '/' if not specified.",
159
+ },
160
+ "filename": {
161
+ "type": "string",
162
+ "description": "Filename to modify.",
163
+ },
164
+ "old_string": {
165
+ "type": "string",
166
+ "description": "The string to be replaced.",
167
+ },
168
+ "new_string": {
169
+ "type": "string",
170
+ "description": "The string to replace the old_string with.",
171
+ },
172
+ }
173
+
174
+ @property
175
+ def required_arguments(self) -> list[str]:
176
+ return ["filename", "old_string", "new_string"]
177
+
178
+ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
179
+ """Replace an old string with a new string in a file."""
180
+ filename = llm_arguments.get("filename")
181
+ file_path = llm_arguments.get("file_path")
182
+ old_string = llm_arguments.get("old_string")
183
+ new_string = llm_arguments.get("new_string")
184
+
185
+ if not filename:
186
+ raise ValueError("filename is required")
187
+ if old_string is None:
188
+ raise ValueError("old_string is required")
189
+ if new_string is None:
190
+ raise ValueError("new_string is required")
191
+
192
+ normalized_path = _normalize_path(file_path)
193
+
194
+ # Read the file content
195
+ result = ctx.client.disks.artifacts.get(
196
+ ctx.disk_id,
197
+ file_path=normalized_path,
198
+ filename=filename,
199
+ with_content=True,
200
+ )
201
+
202
+ if not result.content:
203
+ raise RuntimeError("Failed to read file: server did not return content.")
204
+
205
+ content_str = result.content.raw
206
+
207
+ # Perform the replacement
208
+ if old_string not in content_str:
209
+ return f"String '{old_string}' not found in file '{filename}'"
210
+
211
+ updated_content = content_str.replace(old_string, new_string)
212
+ replacement_count = content_str.count(old_string)
213
+
214
+ # Write the updated content back
215
+ payload = FileUpload(filename=filename, content=updated_content.encode("utf-8"))
216
+ ctx.client.disks.artifacts.upsert(
217
+ ctx.disk_id,
218
+ file=payload,
219
+ file_path=normalized_path,
220
+ )
221
+
222
+ return f"Found {replacement_count} old_string in {normalized_path}{filename} and replaced it."
223
+
224
+
225
+ class ListTool(BaseTool):
226
+ """Tool for listing files in a directory on the Acontext disk."""
227
+
228
+ @property
229
+ def name(self) -> str:
230
+ return "list_artifacts"
231
+
232
+ @property
233
+ def description(self) -> str:
234
+ return "List all files and directories in a specified path on the disk."
235
+
236
+ @property
237
+ def arguments(self) -> dict:
238
+ return {
239
+ "file_path": {
240
+ "type": "string",
241
+ "description": "Optional directory path to list, e.g. '/todo/' or '/notes/'. Root is '/'",
242
+ },
243
+ }
244
+
245
+ @property
246
+ def required_arguments(self) -> list[str]:
247
+ return ["file_path"]
248
+
249
+ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
250
+ """List all files in a specified path."""
251
+ file_path = llm_arguments.get("file_path")
252
+ normalized_path = _normalize_path(file_path)
253
+
254
+ result = ctx.client.disks.artifacts.list(
255
+ ctx.disk_id,
256
+ path=normalized_path,
257
+ )
258
+
259
+ artifacts_list = [artifact.filename for artifact in result.artifacts]
260
+
261
+ if not artifacts_list and not result.directories:
262
+ return f"No files or directories found in '{normalized_path}'"
263
+
264
+ output_parts = []
265
+ if artifacts_list:
266
+ output_parts.append(f"Files: {', '.join(artifacts_list)}")
267
+ if result.directories:
268
+ output_parts.append(f"Directories: {', '.join(result.directories)}")
269
+
270
+ ls_sect = "\n".join(output_parts)
271
+ return f"""[Listing in {normalized_path}]
272
+ {ls_sect}"""
273
+
274
+
275
+ class DiskToolPool(BaseToolPool):
276
+ """Tool pool for disk operations on Acontext disks."""
277
+
278
+ def format_context(self, client: AcontextClient, disk_id: str) -> DiskContext:
279
+ return DiskContext(client=client, disk_id=disk_id)
280
+
281
+
282
+ DISK_TOOLS = DiskToolPool()
283
+ DISK_TOOLS.add_tool(WriteFileTool())
284
+ DISK_TOOLS.add_tool(ReadFileTool())
285
+ DISK_TOOLS.add_tool(ReplaceStringTool())
286
+ DISK_TOOLS.add_tool(ListTool())
287
+
288
+
289
+ if __name__ == "__main__":
290
+ client = AcontextClient(
291
+ api_key="sk-ac-your-root-api-bearer-token",
292
+ base_url="http://localhost:8029/api/v1",
293
+ )
294
+ print(client.ping())
295
+ new_disk = client.disks.create()
296
+
297
+ ctx = DISK_TOOLS.format_context(client, new_disk.id)
298
+ r = DISK_TOOLS.execute_tool(
299
+ ctx,
300
+ "write_file",
301
+ {"filename": "test.txt", "file_path": "/try/", "content": "Hello, world!"},
302
+ )
303
+ print(r)
304
+ r = DISK_TOOLS.execute_tool(
305
+ ctx, "read_file", {"filename": "test.txt", "file_path": "/try/"}
306
+ )
307
+ print(r)
308
+ r = DISK_TOOLS.execute_tool(ctx, "list_artifacts", {"file_path": "/"})
309
+ print(r)
310
+
311
+ r = DISK_TOOLS.execute_tool(
312
+ ctx,
313
+ "replace_string",
314
+ {
315
+ "filename": "test.txt",
316
+ "file_path": "/try/",
317
+ "old_string": "Hello",
318
+ "new_string": "Hi",
319
+ },
320
+ )
321
+ print(r)
322
+ r = DISK_TOOLS.execute_tool(
323
+ ctx, "read_file", {"filename": "test.txt", "file_path": "/try/"}
324
+ )
325
+ print(r)