agentgraph-server 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentgraph/__init__.py +1 -0
- agentgraph/auth/__init__.py +0 -0
- agentgraph/auth/credentials.py +224 -0
- agentgraph/backends/__init__.py +50 -0
- agentgraph/backends/sqlite/__init__.py +1 -0
- agentgraph/backends/sqlite/backend.py +1471 -0
- agentgraph/backends/sqlite/vector.py +142 -0
- agentgraph/cli.py +721 -0
- agentgraph/cli_query.py +519 -0
- agentgraph/config.py +90 -0
- agentgraph/connectors/__init__.py +0 -0
- agentgraph/connectors/base.py +455 -0
- agentgraph/connectors/registry.py +78 -0
- agentgraph/connectors/status.py +244 -0
- agentgraph/core/__init__.py +0 -0
- agentgraph/core/context.py +26 -0
- agentgraph/core/runtime.py +36 -0
- agentgraph/core/storage.py +240 -0
- agentgraph/graph/__init__.py +1 -0
- agentgraph/graph/bookmark.py +87 -0
- agentgraph/graph/delete.py +17 -0
- agentgraph/graph/download.py +35 -0
- agentgraph/graph/embeddings.py +58 -0
- agentgraph/graph/fetch.py +53 -0
- agentgraph/graph/gc.py +26 -0
- agentgraph/graph/link.py +63 -0
- agentgraph/graph/person.py +40 -0
- agentgraph/graph/query.py +244 -0
- agentgraph/graph/upsert.py +49 -0
- agentgraph/logging.py +78 -0
- agentgraph/mcp/__init__.py +0 -0
- agentgraph/mcp/server.py +811 -0
- agentgraph/perf.py +43 -0
- agentgraph/server/__init__.py +0 -0
- agentgraph/server/app.py +133 -0
- agentgraph/server/cli_api.py +708 -0
- agentgraph/server/dwell.py +79 -0
- agentgraph/server/graph_api.py +46 -0
- agentgraph/server/router.py +47 -0
- agentgraph/server/sync.py +247 -0
- agentgraph/skills.py +93 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/graph/SKILL.md +159 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/slack-auth/SKILL.md +92 -0
- agentgraph_server-0.5.0.dist-info/METADATA +286 -0
- agentgraph_server-0.5.0.dist-info/RECORD +49 -0
- agentgraph_server-0.5.0.dist-info/WHEEL +5 -0
- agentgraph_server-0.5.0.dist-info/entry_points.txt +2 -0
- agentgraph_server-0.5.0.dist-info/licenses/LICENSE +21 -0
- agentgraph_server-0.5.0.dist-info/top_level.txt +1 -0
agentgraph/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AgentGraph — local knowledge graph for AI agents."""
|
|
File without changes
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Credential storage in the AgentGraph config directory.
|
|
2
|
+
|
|
3
|
+
Each platform stores its credentials under its own top-level key so
|
|
4
|
+
connectors remain fully independent. Use load_platform / save_platform.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from agentgraph.config import CONFIG_DIR, CREDENTIALS_FILE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CredentialsFileError(ValueError):
|
|
20
|
+
"""The credentials file exists but could not be read as valid storage."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PlatformAccounts(BaseModel):
|
|
24
|
+
accounts: list[dict[str, Any]]
|
|
25
|
+
default_account_id: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_all_credentials() -> dict[str, Any]:
|
|
29
|
+
if not CREDENTIALS_FILE.exists():
|
|
30
|
+
return {}
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(CREDENTIALS_FILE.read_text())
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
# Never fall back to "no credentials" here: callers would treat a
|
|
35
|
+
# damaged file as a first-time setup and overwrite every platform.
|
|
36
|
+
raise CredentialsFileError(
|
|
37
|
+
f"Could not parse {CREDENTIALS_FILE}: {exc}. "
|
|
38
|
+
"Fix or move the file aside, then re-run auth for each platform."
|
|
39
|
+
) from exc
|
|
40
|
+
if not isinstance(data, dict):
|
|
41
|
+
raise CredentialsFileError(
|
|
42
|
+
f"Expected a JSON object at the top level of {CREDENTIALS_FILE}, got {type(data).__name__}."
|
|
43
|
+
)
|
|
44
|
+
return cast(dict[str, Any], data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _write_all_credentials(raw: dict[str, Any]) -> None:
|
|
48
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
# Write via a temp file in the same directory and rename, so a concurrent
|
|
50
|
+
# writer can never leave a shorter document overlaid on a longer one.
|
|
51
|
+
fd, tmp_name = tempfile.mkstemp(dir=CONFIG_DIR, prefix=".credentials-", suffix=".json")
|
|
52
|
+
try:
|
|
53
|
+
with os.fdopen(fd, "w") as handle:
|
|
54
|
+
json.dump(raw, handle, indent=2, default=str)
|
|
55
|
+
os.chmod(tmp_name, 0o600)
|
|
56
|
+
os.replace(tmp_name, CREDENTIALS_FILE)
|
|
57
|
+
except BaseException:
|
|
58
|
+
if os.path.exists(tmp_name):
|
|
59
|
+
os.unlink(tmp_name)
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _validate_accounts(platform: str, val: dict[str, Any]) -> PlatformAccounts:
|
|
64
|
+
try:
|
|
65
|
+
return PlatformAccounts.model_validate(val)
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
raise CredentialsFileError(
|
|
68
|
+
f"Stored '{platform}' credentials in {CREDENTIALS_FILE} are malformed: {exc}. "
|
|
69
|
+
f"Fix the file or re-run auth for {platform}."
|
|
70
|
+
) from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_platform(platform: str) -> dict[str, Any] | None:
|
|
74
|
+
"""Return the stored credential dict for a platform, or None if absent."""
|
|
75
|
+
accounts = load_platform_accounts(platform)
|
|
76
|
+
if not accounts:
|
|
77
|
+
return None
|
|
78
|
+
return accounts[0]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_platform_account(platform: str, account_id: str | None = None) -> dict[str, Any] | None:
|
|
82
|
+
"""Return the stored credential dict for one account, or the default account if omitted."""
|
|
83
|
+
val = _load_all_credentials().get(platform)
|
|
84
|
+
if not isinstance(val, dict):
|
|
85
|
+
return None
|
|
86
|
+
if "accounts" not in val:
|
|
87
|
+
return cast(dict[str, Any], val)
|
|
88
|
+
|
|
89
|
+
data = _validate_accounts(platform, cast(dict[str, Any], val))
|
|
90
|
+
if not data.accounts:
|
|
91
|
+
return None
|
|
92
|
+
target_id = account_id or data.default_account_id
|
|
93
|
+
if target_id:
|
|
94
|
+
for account in data.accounts:
|
|
95
|
+
if account.get("account_id") == target_id:
|
|
96
|
+
return account
|
|
97
|
+
return data.accounts[0]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_platform_accounts(platform: str) -> list[dict[str, Any]]:
|
|
101
|
+
"""Return every stored account credential dict for a platform."""
|
|
102
|
+
val = _load_all_credentials().get(platform)
|
|
103
|
+
if not isinstance(val, dict):
|
|
104
|
+
return []
|
|
105
|
+
if "accounts" not in val:
|
|
106
|
+
return [cast(dict[str, Any], val)]
|
|
107
|
+
return _validate_accounts(platform, cast(dict[str, Any], val)).accounts
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def save_platform(platform: str, data: Any) -> None:
|
|
111
|
+
"""Persist credentials for a platform, merging with the existing file."""
|
|
112
|
+
raw = _load_all_credentials()
|
|
113
|
+
raw[platform] = data.model_dump(mode="json") if hasattr(data, "model_dump") else data
|
|
114
|
+
_write_all_credentials(raw)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def remove_platform(platform: str) -> bool:
|
|
118
|
+
"""Remove all stored credentials for a platform.
|
|
119
|
+
|
|
120
|
+
Returns true when credentials were present and removed.
|
|
121
|
+
"""
|
|
122
|
+
raw = _load_all_credentials()
|
|
123
|
+
if platform not in raw:
|
|
124
|
+
return False
|
|
125
|
+
del raw[platform]
|
|
126
|
+
_write_all_credentials(raw)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def save_platform_accounts(
|
|
131
|
+
platform: str,
|
|
132
|
+
accounts: list[Any],
|
|
133
|
+
*,
|
|
134
|
+
default_account_id: str | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Persist all accounts for a platform."""
|
|
137
|
+
serialised: list[dict[str, Any]] = [
|
|
138
|
+
cast(dict[str, Any], account.model_dump(mode="json"))
|
|
139
|
+
if hasattr(account, "model_dump")
|
|
140
|
+
else cast(dict[str, Any], account)
|
|
141
|
+
for account in accounts
|
|
142
|
+
]
|
|
143
|
+
raw = _load_all_credentials()
|
|
144
|
+
raw[platform] = PlatformAccounts(
|
|
145
|
+
accounts=serialised,
|
|
146
|
+
default_account_id=default_account_id,
|
|
147
|
+
).model_dump(mode="json")
|
|
148
|
+
_write_all_credentials(raw)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def upsert_platform_account(
|
|
152
|
+
platform: str,
|
|
153
|
+
account_id: str,
|
|
154
|
+
data: Any,
|
|
155
|
+
*,
|
|
156
|
+
make_default: bool = False,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Insert or update one account for a platform."""
|
|
159
|
+
existing = load_platform_accounts(platform)
|
|
160
|
+
payload = data.model_dump(mode="json") if hasattr(data, "model_dump") else dict(data)
|
|
161
|
+
payload["account_id"] = account_id
|
|
162
|
+
|
|
163
|
+
updated = False
|
|
164
|
+
for i, account in enumerate(existing):
|
|
165
|
+
if account.get("account_id") == account_id:
|
|
166
|
+
existing[i] = payload
|
|
167
|
+
updated = True
|
|
168
|
+
break
|
|
169
|
+
if not updated:
|
|
170
|
+
existing.append(payload)
|
|
171
|
+
|
|
172
|
+
default_id = account_id if make_default else None
|
|
173
|
+
if default_id is None:
|
|
174
|
+
current = _load_all_credentials().get(platform)
|
|
175
|
+
if isinstance(current, dict) and "default_account_id" in current:
|
|
176
|
+
current_data = cast(dict[str, Any], current)
|
|
177
|
+
raw_default_id = current_data.get("default_account_id")
|
|
178
|
+
default_id = raw_default_id if isinstance(raw_default_id, str) else None
|
|
179
|
+
save_platform_accounts(platform, existing, default_account_id=default_id or account_id)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def remove_platform_account(platform: str, account_id: str) -> bool:
|
|
183
|
+
"""Remove one stored account for a platform.
|
|
184
|
+
|
|
185
|
+
Legacy single-account credentials are only removed when their stored
|
|
186
|
+
account_id matches the requested account_id. Multi-account credentials
|
|
187
|
+
drop the platform key entirely when the final account is removed.
|
|
188
|
+
"""
|
|
189
|
+
raw = _load_all_credentials()
|
|
190
|
+
val = raw.get(platform)
|
|
191
|
+
if not isinstance(val, dict):
|
|
192
|
+
return False
|
|
193
|
+
data_val = cast(dict[str, Any], val)
|
|
194
|
+
|
|
195
|
+
if "accounts" not in data_val:
|
|
196
|
+
if data_val.get("account_id") != account_id:
|
|
197
|
+
return False
|
|
198
|
+
del raw[platform]
|
|
199
|
+
_write_all_credentials(raw)
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
data = PlatformAccounts.model_validate(data_val)
|
|
204
|
+
except Exception:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
remaining = [account for account in data.accounts if account.get("account_id") != account_id]
|
|
208
|
+
if len(remaining) == len(data.accounts):
|
|
209
|
+
return False
|
|
210
|
+
if not remaining:
|
|
211
|
+
del raw[platform]
|
|
212
|
+
_write_all_credentials(raw)
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
default_account_id = data.default_account_id
|
|
216
|
+
if default_account_id == account_id:
|
|
217
|
+
raw_next_default = remaining[0].get("account_id")
|
|
218
|
+
default_account_id = raw_next_default if isinstance(raw_next_default, str) else None
|
|
219
|
+
raw[platform] = PlatformAccounts(
|
|
220
|
+
accounts=remaining,
|
|
221
|
+
default_account_id=default_account_id,
|
|
222
|
+
).model_dump(mode="json")
|
|
223
|
+
_write_all_credentials(raw)
|
|
224
|
+
return True
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Backend registry: maps backend names to StorageBackend implementations.
|
|
2
|
+
|
|
3
|
+
The built-in SQLite backend is always available. Additional backends can be
|
|
4
|
+
registered by third-party packages via the ``agentgraph.backends`` entry point
|
|
5
|
+
group.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from agentgraph.core.storage import StorageBackend
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_registry: dict[str, type[StorageBackend]] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _ensure_loaded() -> None:
|
|
21
|
+
if _registry:
|
|
22
|
+
return
|
|
23
|
+
_load_builtins()
|
|
24
|
+
_load_plugins()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_builtins() -> None:
|
|
28
|
+
try:
|
|
29
|
+
from agentgraph.backends.sqlite.backend import SQLiteBackend
|
|
30
|
+
|
|
31
|
+
_registry["sqlite"] = SQLiteBackend
|
|
32
|
+
except ImportError:
|
|
33
|
+
logger.debug("SQLite backend unavailable (aiosqlite not installed)")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_plugins() -> None:
|
|
37
|
+
for ep in importlib.metadata.entry_points(group="agentgraph.backends"):
|
|
38
|
+
try:
|
|
39
|
+
_registry[ep.name] = ep.load()
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
logger.warning("Failed to load backend %r: %s", ep.name, exc)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_backend_class(name: str) -> type[StorageBackend]:
|
|
45
|
+
_ensure_loaded()
|
|
46
|
+
if name not in _registry:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Unknown backend {name!r}. Available: {sorted(_registry)}"
|
|
49
|
+
)
|
|
50
|
+
return _registry[name]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SQLite storage backend package
|