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.
- acontext-0.0.13/PKG-INFO +34 -0
- acontext-0.0.13/README.md +19 -0
- acontext-0.0.13/pyproject.toml +25 -0
- acontext-0.0.13/src/acontext/__init__.py +48 -0
- acontext-0.0.13/src/acontext/_constants.py +14 -0
- acontext-0.0.13/src/acontext/_utils.py +42 -0
- acontext-0.0.13/src/acontext/agent/__init__.py +0 -0
- acontext-0.0.13/src/acontext/agent/base.py +106 -0
- acontext-0.0.13/src/acontext/agent/disk.py +325 -0
- acontext-0.0.13/src/acontext/async_client.py +227 -0
- acontext-0.0.13/src/acontext/client.py +226 -0
- acontext-0.0.13/src/acontext/client_types.py +36 -0
- acontext-0.0.13/src/acontext/errors.py +44 -0
- acontext-0.0.13/src/acontext/messages.py +75 -0
- acontext-0.0.13/src/acontext/py.typed +0 -0
- acontext-0.0.13/src/acontext/resources/__init__.py +27 -0
- acontext-0.0.13/src/acontext/resources/async_blocks.py +164 -0
- acontext-0.0.13/src/acontext/resources/async_disks.py +195 -0
- acontext-0.0.13/src/acontext/resources/async_sessions.py +367 -0
- acontext-0.0.13/src/acontext/resources/async_spaces.py +190 -0
- acontext-0.0.13/src/acontext/resources/async_tools.py +34 -0
- acontext-0.0.13/src/acontext/resources/blocks.py +163 -0
- acontext-0.0.13/src/acontext/resources/disks.py +194 -0
- acontext-0.0.13/src/acontext/resources/sessions.py +368 -0
- acontext-0.0.13/src/acontext/resources/spaces.py +188 -0
- acontext-0.0.13/src/acontext/resources/tools.py +34 -0
- acontext-0.0.13/src/acontext/types/__init__.py +78 -0
- acontext-0.0.13/src/acontext/types/block.py +26 -0
- acontext-0.0.13/src/acontext/types/disk.py +65 -0
- acontext-0.0.13/src/acontext/types/session.py +271 -0
- acontext-0.0.13/src/acontext/types/space.py +69 -0
- acontext-0.0.13/src/acontext/types/tool.py +31 -0
- acontext-0.0.13/src/acontext/uploads.py +44 -0
acontext-0.0.13/PKG-INFO
ADDED
|
@@ -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)
|