acontext 0.0.17__tar.gz → 0.1.0__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 (42) hide show
  1. {acontext-0.0.17 → acontext-0.1.0}/PKG-INFO +2 -1
  2. {acontext-0.0.17 → acontext-0.1.0}/pyproject.toml +2 -1
  3. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/agent/__init__.py +0 -1
  4. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/agent/base.py +13 -0
  5. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/agent/disk.py +210 -47
  6. acontext-0.1.0/src/acontext/agent/skill.py +405 -0
  7. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_disks.py +104 -20
  8. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_sessions.py +20 -15
  9. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_skills.py +13 -28
  10. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/skills.py +21 -30
  11. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/skill.py +14 -18
  12. acontext-0.0.17/src/acontext/agent/skill.py +0 -148
  13. {acontext-0.0.17 → acontext-0.1.0}/README.md +0 -0
  14. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/__init__.py +0 -0
  15. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/_constants.py +0 -0
  16. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/_utils.py +0 -0
  17. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/async_client.py +0 -0
  18. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/client.py +0 -0
  19. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/client_types.py +0 -0
  20. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/errors.py +0 -0
  21. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/messages.py +0 -0
  22. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/py.typed +0 -0
  23. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/__init__.py +0 -0
  24. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_blocks.py +0 -0
  25. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_spaces.py +0 -0
  26. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_tools.py +0 -0
  27. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/async_users.py +0 -0
  28. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/blocks.py +0 -0
  29. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/disks.py +0 -0
  30. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/sessions.py +0 -0
  31. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/spaces.py +0 -0
  32. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/tools.py +0 -0
  33. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/resources/users.py +0 -0
  34. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/__init__.py +0 -0
  35. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/block.py +0 -0
  36. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/common.py +0 -0
  37. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/disk.py +0 -0
  38. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/session.py +0 -0
  39. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/space.py +0 -0
  40. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/tool.py +0 -0
  41. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/types/user.py +0 -0
  42. {acontext-0.0.17 → acontext-0.1.0}/src/acontext/uploads.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acontext
3
- Version: 0.0.17
3
+ Version: 0.1.0
4
4
  Summary: Python SDK for the Acontext API
5
5
  Keywords: acontext,sdk,client,api
6
6
  Requires-Dist: httpx>=0.28.1
7
7
  Requires-Dist: openai>=2.6.1
8
8
  Requires-Dist: anthropic>=0.72.0
9
9
  Requires-Dist: pydantic>=2.12.3
10
+ Requires-Dist: urllib3>=2.6.3
10
11
  Requires-Python: >=3.10
11
12
  Project-URL: Homepage, https://github.com/memodb-io/Acontext
12
13
  Project-URL: Issues, https://github.com/memodb-io/Acontext/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acontext"
3
- version = "0.0.17"
3
+ version = "0.1.0"
4
4
  description = "Python SDK for the Acontext API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -9,6 +9,7 @@ dependencies = [
9
9
  "openai>=2.6.1",
10
10
  "anthropic>=0.72.0",
11
11
  "pydantic>=2.12.3",
12
+ "urllib3>=2.6.3",
12
13
  ]
13
14
  keywords = ["acontext", "sdk", "client", "api"]
14
15
 
@@ -7,4 +7,3 @@ __all__ = [
7
7
  "DISK_TOOLS",
8
8
  "SKILL_TOOLS",
9
9
  ]
10
-
@@ -33,6 +33,9 @@ class BaseTool(BaseConverter):
33
33
  def execute(self, ctx: BaseContext, llm_arguments: dict) -> str:
34
34
  raise NotImplementedError
35
35
 
36
+ async def async_execute(self, ctx: BaseContext, llm_arguments: dict) -> str:
37
+ raise NotImplementedError
38
+
36
39
  def to_openai_tool_schema(self) -> dict:
37
40
  return {
38
41
  "type": "function",
@@ -90,6 +93,13 @@ class BaseToolPool(BaseConverter):
90
93
  r = tool.execute(ctx, llm_arguments)
91
94
  return r.strip()
92
95
 
96
+ async def async_execute_tool(
97
+ self, ctx: BaseContext, tool_name: str, llm_arguments: dict
98
+ ) -> str:
99
+ tool = self.tools[tool_name]
100
+ r = await tool.async_execute(ctx, llm_arguments)
101
+ return r.strip()
102
+
93
103
  def tool_exists(self, tool_name: str) -> bool:
94
104
  return tool_name in self.tools
95
105
 
@@ -104,3 +114,6 @@ class BaseToolPool(BaseConverter):
104
114
 
105
115
  def format_context(self, *args, **kwargs) -> BaseContext:
106
116
  raise NotImplementedError
117
+
118
+ async def async_format_context(self, *args, **kwargs) -> BaseContext:
119
+ raise NotImplementedError
@@ -2,6 +2,7 @@ from dataclasses import dataclass
2
2
 
3
3
  from .base import BaseContext, BaseTool, BaseToolPool
4
4
  from ..client import AcontextClient
5
+ from ..async_client import AcontextAsyncClient
5
6
  from ..uploads import FileUpload
6
7
 
7
8
 
@@ -11,6 +12,12 @@ class DiskContext(BaseContext):
11
12
  disk_id: str
12
13
 
13
14
 
15
+ @dataclass
16
+ class AsyncDiskContext(BaseContext):
17
+ client: AcontextAsyncClient
18
+ disk_id: str
19
+
20
+
14
21
  def _normalize_path(path: str | None) -> str:
15
22
  """Normalize a file path to ensure it starts with '/'."""
16
23
  if not path:
@@ -73,6 +80,26 @@ class WriteFileTool(BaseTool):
73
80
  )
74
81
  return f"File '{artifact.filename}' written successfully to '{artifact.path}'"
75
82
 
83
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
84
+ """Write text content to a file (async)."""
85
+ filename = llm_arguments.get("filename")
86
+ content = llm_arguments.get("content")
87
+ file_path = llm_arguments.get("file_path")
88
+
89
+ if not filename:
90
+ raise ValueError("filename is required")
91
+ if not content:
92
+ raise ValueError("content is required")
93
+
94
+ normalized_path = _normalize_path(file_path)
95
+ payload = FileUpload(filename=filename, content=content.encode("utf-8"))
96
+ artifact = await ctx.client.disks.artifacts.upsert(
97
+ ctx.disk_id,
98
+ file=payload,
99
+ file_path=normalized_path,
100
+ )
101
+ return f"File '{artifact.filename}' written successfully to '{artifact.path}'"
102
+
76
103
 
77
104
  class ReadFileTool(BaseTool):
78
105
  """Tool for reading a text file from the Acontext disk."""
@@ -138,6 +165,34 @@ class ReadFileTool(BaseTool):
138
165
  preview = "\n".join(lines[line_start:line_end])
139
166
  return f"[{normalized_path}{filename} - showing L{line_start}-{line_end} of {len(lines)} lines]\n{preview}"
140
167
 
168
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
169
+ """Read a text file and return its content preview (async)."""
170
+ filename = llm_arguments.get("filename")
171
+ file_path = llm_arguments.get("file_path")
172
+ line_offset = llm_arguments.get("line_offset", 0)
173
+ line_limit = llm_arguments.get("line_limit", 100)
174
+
175
+ if not filename:
176
+ raise ValueError("filename is required")
177
+
178
+ normalized_path = _normalize_path(file_path)
179
+ result = await ctx.client.disks.artifacts.get(
180
+ ctx.disk_id,
181
+ file_path=normalized_path,
182
+ filename=filename,
183
+ with_content=True,
184
+ )
185
+
186
+ if not result.content:
187
+ raise RuntimeError("Failed to read file: server did not return content.")
188
+
189
+ content_str = result.content.raw
190
+ lines = content_str.split("\n")
191
+ line_start = min(line_offset, len(lines) - 1)
192
+ line_end = min(line_start + line_limit, len(lines))
193
+ preview = "\n".join(lines[line_start:line_end])
194
+ return f"[{normalized_path}{filename} - showing L{line_start}-{line_end} of {len(lines)} lines]\n{preview}"
195
+
141
196
 
142
197
  class ReplaceStringTool(BaseTool):
143
198
  """Tool for replacing an old string with a new string in a file on the Acontext disk."""
@@ -221,6 +276,52 @@ class ReplaceStringTool(BaseTool):
221
276
 
222
277
  return f"Found {replacement_count} old_string in {normalized_path}{filename} and replaced it."
223
278
 
279
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
280
+ """Replace an old string with a new string in a file (async)."""
281
+ filename = llm_arguments.get("filename")
282
+ file_path = llm_arguments.get("file_path")
283
+ old_string = llm_arguments.get("old_string")
284
+ new_string = llm_arguments.get("new_string")
285
+
286
+ if not filename:
287
+ raise ValueError("filename is required")
288
+ if old_string is None:
289
+ raise ValueError("old_string is required")
290
+ if new_string is None:
291
+ raise ValueError("new_string is required")
292
+
293
+ normalized_path = _normalize_path(file_path)
294
+
295
+ # Read the file content
296
+ result = await ctx.client.disks.artifacts.get(
297
+ ctx.disk_id,
298
+ file_path=normalized_path,
299
+ filename=filename,
300
+ with_content=True,
301
+ )
302
+
303
+ if not result.content:
304
+ raise RuntimeError("Failed to read file: server did not return content.")
305
+
306
+ content_str = result.content.raw
307
+
308
+ # Perform the replacement
309
+ if old_string not in content_str:
310
+ return f"String '{old_string}' not found in file '{filename}'"
311
+
312
+ updated_content = content_str.replace(old_string, new_string)
313
+ replacement_count = content_str.count(old_string)
314
+
315
+ # Write the updated content back
316
+ payload = FileUpload(filename=filename, content=updated_content.encode("utf-8"))
317
+ await ctx.client.disks.artifacts.upsert(
318
+ ctx.disk_id,
319
+ file=payload,
320
+ file_path=normalized_path,
321
+ )
322
+
323
+ return f"Found {replacement_count} old_string in {normalized_path}{filename} and replaced it."
324
+
224
325
 
225
326
  class ListTool(BaseTool):
226
327
  """Tool for listing files in a directory on the Acontext disk."""
@@ -271,6 +372,31 @@ Directories:
271
372
  Files:
272
373
  {file_sect}"""
273
374
 
375
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
376
+ """List all files in a specified path (async)."""
377
+ file_path = llm_arguments.get("file_path")
378
+ normalized_path = _normalize_path(file_path)
379
+
380
+ result = await ctx.client.disks.artifacts.list(
381
+ ctx.disk_id,
382
+ path=normalized_path,
383
+ )
384
+
385
+ artifacts_list = [artifact.filename for artifact in result.artifacts]
386
+
387
+ if not artifacts_list and not result.directories:
388
+ return f"No files or directories found in '{normalized_path}'"
389
+
390
+ file_sect = "\n".join(artifacts_list) or "[NO FILE]"
391
+ dir_sect = (
392
+ "\n".join([d.rstrip("/") + "/" for d in result.directories]) or "[NO DIR]"
393
+ )
394
+ return f"""[Listing in {normalized_path}]
395
+ Directories:
396
+ {dir_sect}
397
+ Files:
398
+ {file_sect}"""
399
+
274
400
 
275
401
  class DownloadFileTool(BaseTool):
276
402
  """Tool for getting a public download URL for a file on the Acontext disk."""
@@ -327,6 +453,29 @@ class DownloadFileTool(BaseTool):
327
453
 
328
454
  return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"
329
455
 
456
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
457
+ """Get a public download URL for a file (async)."""
458
+ filename = llm_arguments.get("filename")
459
+ file_path = llm_arguments.get("file_path")
460
+ expire = llm_arguments.get("expire", 3600)
461
+
462
+ if not filename:
463
+ raise ValueError("filename is required")
464
+
465
+ normalized_path = _normalize_path(file_path)
466
+ result = await ctx.client.disks.artifacts.get(
467
+ ctx.disk_id,
468
+ file_path=normalized_path,
469
+ filename=filename,
470
+ with_public_url=True,
471
+ expire=expire,
472
+ )
473
+
474
+ if not result.public_url:
475
+ raise RuntimeError("Failed to get public URL: server did not return a URL.")
476
+
477
+ return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"
478
+
330
479
 
331
480
  class GrepArtifactsTool(BaseTool):
332
481
  """Tool for searching artifact content using regex patterns."""
@@ -377,7 +526,34 @@ class GrepArtifactsTool(BaseTool):
377
526
  for artifact in results:
378
527
  matches.append(f"{artifact.path}{artifact.filename}")
379
528
 
380
- return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(matches)
529
+ return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(
530
+ matches
531
+ )
532
+
533
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
534
+ """Search artifact content using regex pattern (async)."""
535
+ query = llm_arguments.get("query")
536
+ limit = llm_arguments.get("limit", 100)
537
+
538
+ if not query:
539
+ raise ValueError("query is required")
540
+
541
+ results = await ctx.client.disks.artifacts.grep_artifacts(
542
+ ctx.disk_id,
543
+ query=query,
544
+ limit=limit,
545
+ )
546
+
547
+ if not results:
548
+ return f"No matches found for pattern '{query}'"
549
+
550
+ matches = []
551
+ for artifact in results:
552
+ matches.append(f"{artifact.path}{artifact.filename}")
553
+
554
+ return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(
555
+ matches
556
+ )
381
557
 
382
558
 
383
559
  class GlobArtifactsTool(BaseTool):
@@ -429,7 +605,34 @@ class GlobArtifactsTool(BaseTool):
429
605
  for artifact in results:
430
606
  matches.append(f"{artifact.path}{artifact.filename}")
431
607
 
432
- return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(matches)
608
+ return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(
609
+ matches
610
+ )
611
+
612
+ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
613
+ """Search artifact paths using glob pattern (async)."""
614
+ query = llm_arguments.get("query")
615
+ limit = llm_arguments.get("limit", 100)
616
+
617
+ if not query:
618
+ raise ValueError("query is required")
619
+
620
+ results = await ctx.client.disks.artifacts.glob_artifacts(
621
+ ctx.disk_id,
622
+ query=query,
623
+ limit=limit,
624
+ )
625
+
626
+ if not results:
627
+ return f"No files found matching pattern '{query}'"
628
+
629
+ matches = []
630
+ for artifact in results:
631
+ matches.append(f"{artifact.path}{artifact.filename}")
632
+
633
+ return f"Found {len(matches)} file(s) matching '{query}':\n" + "\n".join(
634
+ matches
635
+ )
433
636
 
434
637
 
435
638
  class DiskToolPool(BaseToolPool):
@@ -438,6 +641,11 @@ class DiskToolPool(BaseToolPool):
438
641
  def format_context(self, client: AcontextClient, disk_id: str) -> DiskContext:
439
642
  return DiskContext(client=client, disk_id=disk_id)
440
643
 
644
+ async def async_format_context(
645
+ self, client: AcontextAsyncClient, disk_id: str
646
+ ) -> AsyncDiskContext:
647
+ return AsyncDiskContext(client=client, disk_id=disk_id)
648
+
441
649
 
442
650
  DISK_TOOLS = DiskToolPool()
443
651
  DISK_TOOLS.add_tool(WriteFileTool())
@@ -447,48 +655,3 @@ DISK_TOOLS.add_tool(ListTool())
447
655
  DISK_TOOLS.add_tool(GrepArtifactsTool())
448
656
  DISK_TOOLS.add_tool(GlobArtifactsTool())
449
657
  DISK_TOOLS.add_tool(DownloadFileTool())
450
-
451
-
452
- if __name__ == "__main__":
453
- client = AcontextClient(
454
- api_key="sk-ac-your-root-api-bearer-token",
455
- base_url="http://localhost:8029/api/v1",
456
- )
457
- print(client.ping())
458
- new_disk = client.disks.create()
459
-
460
- ctx = DISK_TOOLS.format_context(client, new_disk.id)
461
- r = DISK_TOOLS.execute_tool(
462
- ctx,
463
- "write_file",
464
- {"filename": "test.txt", "file_path": "/try/", "content": "Hello, world!"},
465
- )
466
- print(r)
467
- r = DISK_TOOLS.execute_tool(
468
- ctx, "read_file", {"filename": "test.txt", "file_path": "/try/"}
469
- )
470
- print(r)
471
- r = DISK_TOOLS.execute_tool(ctx, "list_artifacts", {"file_path": "/"})
472
- print(r)
473
-
474
- r = DISK_TOOLS.execute_tool(
475
- ctx,
476
- "replace_string",
477
- {
478
- "filename": "test.txt",
479
- "file_path": "/try/",
480
- "old_string": "Hello",
481
- "new_string": "Hi",
482
- },
483
- )
484
- print(r)
485
- r = DISK_TOOLS.execute_tool(
486
- ctx, "read_file", {"filename": "test.txt", "file_path": "/try/"}
487
- )
488
- print(r)
489
- r = DISK_TOOLS.execute_tool(
490
- ctx,
491
- "download_file",
492
- {"filename": "test.txt", "file_path": "/try/", "expire": 300},
493
- )
494
- print(r)