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.
Files changed (49) hide show
  1. agentgraph/__init__.py +1 -0
  2. agentgraph/auth/__init__.py +0 -0
  3. agentgraph/auth/credentials.py +224 -0
  4. agentgraph/backends/__init__.py +50 -0
  5. agentgraph/backends/sqlite/__init__.py +1 -0
  6. agentgraph/backends/sqlite/backend.py +1471 -0
  7. agentgraph/backends/sqlite/vector.py +142 -0
  8. agentgraph/cli.py +721 -0
  9. agentgraph/cli_query.py +519 -0
  10. agentgraph/config.py +90 -0
  11. agentgraph/connectors/__init__.py +0 -0
  12. agentgraph/connectors/base.py +455 -0
  13. agentgraph/connectors/registry.py +78 -0
  14. agentgraph/connectors/status.py +244 -0
  15. agentgraph/core/__init__.py +0 -0
  16. agentgraph/core/context.py +26 -0
  17. agentgraph/core/runtime.py +36 -0
  18. agentgraph/core/storage.py +240 -0
  19. agentgraph/graph/__init__.py +1 -0
  20. agentgraph/graph/bookmark.py +87 -0
  21. agentgraph/graph/delete.py +17 -0
  22. agentgraph/graph/download.py +35 -0
  23. agentgraph/graph/embeddings.py +58 -0
  24. agentgraph/graph/fetch.py +53 -0
  25. agentgraph/graph/gc.py +26 -0
  26. agentgraph/graph/link.py +63 -0
  27. agentgraph/graph/person.py +40 -0
  28. agentgraph/graph/query.py +244 -0
  29. agentgraph/graph/upsert.py +49 -0
  30. agentgraph/logging.py +78 -0
  31. agentgraph/mcp/__init__.py +0 -0
  32. agentgraph/mcp/server.py +811 -0
  33. agentgraph/perf.py +43 -0
  34. agentgraph/server/__init__.py +0 -0
  35. agentgraph/server/app.py +133 -0
  36. agentgraph/server/cli_api.py +708 -0
  37. agentgraph/server/dwell.py +79 -0
  38. agentgraph/server/graph_api.py +46 -0
  39. agentgraph/server/router.py +47 -0
  40. agentgraph/server/sync.py +247 -0
  41. agentgraph/skills.py +93 -0
  42. agentgraph_server-0.5.0.data/data/.agents/skills/graph/SKILL.md +159 -0
  43. agentgraph_server-0.5.0.data/data/.agents/skills/slack-auth/SKILL.md +92 -0
  44. agentgraph_server-0.5.0.dist-info/METADATA +286 -0
  45. agentgraph_server-0.5.0.dist-info/RECORD +49 -0
  46. agentgraph_server-0.5.0.dist-info/WHEEL +5 -0
  47. agentgraph_server-0.5.0.dist-info/entry_points.txt +2 -0
  48. agentgraph_server-0.5.0.dist-info/licenses/LICENSE +21 -0
  49. 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