agentgraph-server 0.5.3__tar.gz → 0.6.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.
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/SKILL.md +4 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/PKG-INFO +1 -1
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/backend.py +56 -26
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/cli.py +80 -12
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/config.py +1 -1
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/base.py +39 -34
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/demo.py +85 -45
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/mcp/server.py +34 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/PKG-INFO +1 -1
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/pyproject.toml +1 -1
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_cli.py +158 -36
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_config.py +1 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_connectors.py +6 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_launch_demo.py +57 -18
- agentgraph_server-0.6.0/tests/test_release_metadata.py +85 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_schema.py +97 -0
- agentgraph_server-0.5.3/tests/test_release_metadata.py +0 -38
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/commands.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/data-model.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/operations.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/slack-auth/SKILL.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/LICENSE +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/README.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/auth/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/auth/credentials.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/schema.sql +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/vector.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/cli_query.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/cli_sync.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/command_effects.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/registry.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/status.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/context.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/runtime.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/storage.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/demo_fixtures/reliable-webhooks.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/demo_fixtures/retry-guidance.md +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/bookmark.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/delete.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/download.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/embeddings.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/expiration.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/fetch.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/link.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/operations.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/person.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/query.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/upsert.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/logging.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/mcp/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/perf.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/__init__.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/app.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/cli_api.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/meta_api.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/observation.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/router.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/static/viewer.html +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/sync.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/sync_api.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/skills.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/SOURCES.txt +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/dependency_links.txt +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/entry_points.txt +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/requires.txt +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/top_level.txt +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/setup.cfg +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_auth.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_benchmarks.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_browse.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_command_effects.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_discord_attachments.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_docs_build.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_embeddings.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_expiration.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_fetch.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_graph_operations.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_logging.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_observation.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_query.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_registry.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_router.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_rss_connector.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_server_imports.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_slack_auth_artifacts.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_slack_oauth.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_sync.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_upsert.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_viewer_layout_contract.py +0 -0
- {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_web_connector.py +0 -0
|
@@ -13,6 +13,10 @@ Prefer the `agentgraph` CLI when a shell is available. Use the equivalent MCP to
|
|
|
13
13
|
when AgentGraph is connected directly to the agent. Do not query AgentGraph's SQLite
|
|
14
14
|
database or connector internals directly.
|
|
15
15
|
|
|
16
|
+
Use `agentgraph demo add` or `add_demo_tool` to add the fictional Atlas fixtures to
|
|
17
|
+
the configured graph. Remove them afterward with `agentgraph demo remove` or
|
|
18
|
+
`remove_demo_tool`.
|
|
19
|
+
|
|
16
20
|
## Commands that use localhost
|
|
17
21
|
|
|
18
22
|
`agentgraph poll` and connector or authentication commands that queue a poll or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentgraph-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A local-first CLI and MCP server that turns selected digital sources into a searchable graph for the AI agent you already use.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -64,7 +64,7 @@ _LIST_PAGE_ORDER_BY = {
|
|
|
64
64
|
_COLUMN_FILTERS = {"platform", "platform_entity_id", "entity_type"}
|
|
65
65
|
_FTS_DELETE_CHUNK_SIZE = 500
|
|
66
66
|
_BUSY_TIMEOUT_MS = 5_000
|
|
67
|
-
_SCHEMA_VERSION =
|
|
67
|
+
_SCHEMA_VERSION = 3
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def _now() -> str:
|
|
@@ -208,9 +208,9 @@ class SQLiteBackend(StorageBackend):
|
|
|
208
208
|
async def _run_migrations(self) -> None:
|
|
209
209
|
conn = self._conn_or_raise()
|
|
210
210
|
cursor = await conn.execute("PRAGMA table_info(entities)")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
211
|
+
table_info = await cursor.fetchall()
|
|
212
|
+
entity_columns = {str(row["name"]): bool(row["notnull"]) for row in table_info}
|
|
213
|
+
entity_defaults = {str(row["name"]): row["dflt_value"] for row in table_info}
|
|
214
214
|
columns = set(entity_columns)
|
|
215
215
|
if "cumulative_dwell_ms" in columns:
|
|
216
216
|
await conn.execute(
|
|
@@ -236,6 +236,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
236
236
|
"last_accessed" in columns
|
|
237
237
|
or not entity_columns.get("created_at", False)
|
|
238
238
|
or not entity_columns.get("updated_at", False)
|
|
239
|
+
or entity_defaults.get("created_at") is None
|
|
240
|
+
or entity_defaults.get("updated_at") is None
|
|
239
241
|
or entity_columns.get("observed_at", False)
|
|
240
242
|
or "source_created_at" not in columns
|
|
241
243
|
or "source_updated_at" not in columns
|
|
@@ -249,9 +251,7 @@ class SQLiteBackend(StorageBackend):
|
|
|
249
251
|
"UPDATE observations SET event_type = 'observation_threshold' "
|
|
250
252
|
"WHERE event_type = 'dwell_threshold'"
|
|
251
253
|
)
|
|
252
|
-
await conn.execute(
|
|
253
|
-
"UPDATE entities SET entity_type = 'Email' WHERE entity_type = 'Thread'"
|
|
254
|
-
)
|
|
254
|
+
await conn.execute("UPDATE entities SET entity_type = 'Email' WHERE entity_type = 'Thread'")
|
|
255
255
|
await conn.execute(
|
|
256
256
|
"CREATE INDEX IF NOT EXISTS idx_entities_bookmarked ON entities(bookmarked)"
|
|
257
257
|
)
|
|
@@ -352,10 +352,13 @@ class SQLiteBackend(StorageBackend):
|
|
|
352
352
|
"AND cumulative_observation_duration_ms > 0 THEN observed_at ELSE NULL END"
|
|
353
353
|
)
|
|
354
354
|
observed = (
|
|
355
|
-
"CASE WHEN platform = 'rss' AND entity_type = 'Folder' THEN NULL "
|
|
356
|
-
|
|
355
|
+
f"CASE WHEN platform = 'rss' AND entity_type = 'Folder' THEN NULL ELSE {observed} END"
|
|
356
|
+
)
|
|
357
|
+
duration = (
|
|
358
|
+
"cumulative_observation_duration_ms"
|
|
359
|
+
if "cumulative_observation_duration_ms" in columns
|
|
360
|
+
else "0"
|
|
357
361
|
)
|
|
358
|
-
duration = "cumulative_observation_duration_ms" if "cumulative_observation_duration_ms" in columns else "0"
|
|
359
362
|
bookmarked = "bookmarked" if "bookmarked" in columns else "0"
|
|
360
363
|
await conn.execute("PRAGMA foreign_keys=OFF")
|
|
361
364
|
try:
|
|
@@ -371,8 +374,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
371
374
|
content TEXT,
|
|
372
375
|
content_embedding BLOB,
|
|
373
376
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
374
|
-
created_at TEXT NOT NULL,
|
|
375
|
-
updated_at TEXT NOT NULL,
|
|
377
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
378
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
376
379
|
source_created_at TEXT,
|
|
377
380
|
source_updated_at TEXT,
|
|
378
381
|
synced_at TEXT,
|
|
@@ -503,6 +506,7 @@ class SQLiteBackend(StorageBackend):
|
|
|
503
506
|
for p in persons:
|
|
504
507
|
canonical_key = p.canonical_email or f"{p.platform}:{p.platform_user_id}"
|
|
505
508
|
meta: dict[str, str] = {}
|
|
509
|
+
meta.update({key: str(value) for key, value in p.metadata.items() if value is not None})
|
|
506
510
|
if p.canonical_email:
|
|
507
511
|
meta["canonical_email"] = p.canonical_email
|
|
508
512
|
meta[f"{p.platform}_user_id"] = p.platform_user_id
|
|
@@ -534,7 +538,9 @@ class SQLiteBackend(StorageBackend):
|
|
|
534
538
|
existing_metadata = json.loads(existing_row[3] or "{}") if existing_row else {}
|
|
535
539
|
merged_metadata = {**existing_metadata, **meta}
|
|
536
540
|
new_title = p.display_name if p.display_name is not None else existing_row[0]
|
|
537
|
-
new_content =
|
|
541
|
+
new_content = (
|
|
542
|
+
p.canonical_email if p.canonical_email is not None else existing_row[1]
|
|
543
|
+
)
|
|
538
544
|
new_embedding = emb_blob if emb_blob is not None else existing_row[2]
|
|
539
545
|
changed = (
|
|
540
546
|
new_title != existing_row[0]
|
|
@@ -647,9 +653,7 @@ class SQLiteBackend(StorageBackend):
|
|
|
647
653
|
if parent_id is None:
|
|
648
654
|
parent_id = await self._resolve_existing_entity_id(conn, e.platform, parent_ref)
|
|
649
655
|
if parent_id is None:
|
|
650
|
-
raise ValueError(
|
|
651
|
-
f"Retention parent {e.platform}:{parent_ref} is not available"
|
|
652
|
-
)
|
|
656
|
+
raise ValueError(f"Retention parent {e.platform}:{parent_ref} is not available")
|
|
653
657
|
elif e.retention_parent_platform_entity_id is not None:
|
|
654
658
|
raise ValueError(
|
|
655
659
|
f"Entity {e.platform}:{e.platform_entity_id} has a parent but is not owned"
|
|
@@ -660,8 +664,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
660
664
|
"""
|
|
661
665
|
INSERT INTO entities
|
|
662
666
|
(id, entity_type, platform, platform_entity_id, title, metadata,
|
|
663
|
-
retention_policy, retention_parent_id)
|
|
664
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
667
|
+
created_at, updated_at, retention_policy, retention_parent_id)
|
|
668
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
665
669
|
ON CONFLICT (platform, platform_entity_id) DO UPDATE SET
|
|
666
670
|
entity_type = entities.entity_type
|
|
667
671
|
RETURNING id
|
|
@@ -673,6 +677,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
673
677
|
e.platform_entity_id,
|
|
674
678
|
e.title,
|
|
675
679
|
json.dumps(dict(e.metadata)),
|
|
680
|
+
now,
|
|
681
|
+
now,
|
|
676
682
|
e.retention_policy,
|
|
677
683
|
parent_id,
|
|
678
684
|
],
|
|
@@ -689,8 +695,12 @@ class SQLiteBackend(StorageBackend):
|
|
|
689
695
|
[e.platform, e.platform_entity_id],
|
|
690
696
|
)
|
|
691
697
|
existing_row = await existing_cursor.fetchone()
|
|
692
|
-
existing_title =
|
|
693
|
-
|
|
698
|
+
existing_title = (
|
|
699
|
+
str(existing_row["title"]) if existing_row and existing_row["title"] else ""
|
|
700
|
+
)
|
|
701
|
+
existing_content = (
|
|
702
|
+
str(existing_row["content"]) if existing_row and existing_row["content"] else ""
|
|
703
|
+
)
|
|
694
704
|
fts_title = e.title if e.title is not None else existing_title
|
|
695
705
|
fts_content = e.content if e.content is not None else existing_content
|
|
696
706
|
rewrite_fts = (
|
|
@@ -752,8 +762,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
752
762
|
INSERT INTO entities
|
|
753
763
|
(id, entity_type, platform, platform_entity_id, title, content,
|
|
754
764
|
content_embedding, metadata, source_created_at, source_updated_at,
|
|
755
|
-
synced_at, retention_policy, retention_parent_id)
|
|
756
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
765
|
+
synced_at, created_at, updated_at, retention_policy, retention_parent_id)
|
|
766
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
757
767
|
ON CONFLICT (platform, platform_entity_id) DO UPDATE SET
|
|
758
768
|
entity_type = CASE WHEN entities.entity_type = 'Document' THEN EXCLUDED.entity_type ELSE entities.entity_type END,
|
|
759
769
|
title = COALESCE(EXCLUDED.title, entities.title),
|
|
@@ -780,6 +790,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
780
790
|
source_created,
|
|
781
791
|
source_updated,
|
|
782
792
|
now,
|
|
793
|
+
now,
|
|
794
|
+
local_updated,
|
|
783
795
|
e.retention_policy,
|
|
784
796
|
parent_id,
|
|
785
797
|
local_updated,
|
|
@@ -1734,7 +1746,14 @@ class SQLiteBackend(StorageBackend):
|
|
|
1734
1746
|
WHERE platform = ? AND platform_entity_id = ?
|
|
1735
1747
|
AND retention_policy = 'observed'
|
|
1736
1748
|
""",
|
|
1737
|
-
[
|
|
1749
|
+
[
|
|
1750
|
+
observation_duration_ms,
|
|
1751
|
+
now,
|
|
1752
|
+
observation_duration_ms,
|
|
1753
|
+
now,
|
|
1754
|
+
platform,
|
|
1755
|
+
platform_entity_id,
|
|
1756
|
+
],
|
|
1738
1757
|
)
|
|
1739
1758
|
|
|
1740
1759
|
async def record_observation_once(
|
|
@@ -1772,7 +1791,14 @@ class SQLiteBackend(StorageBackend):
|
|
|
1772
1791
|
WHERE platform = ? AND platform_entity_id = ?
|
|
1773
1792
|
AND retention_policy = 'observed'
|
|
1774
1793
|
""",
|
|
1775
|
-
[
|
|
1794
|
+
[
|
|
1795
|
+
observation_duration_ms,
|
|
1796
|
+
now,
|
|
1797
|
+
observation_duration_ms,
|
|
1798
|
+
now,
|
|
1799
|
+
platform,
|
|
1800
|
+
platform_entity_id,
|
|
1801
|
+
],
|
|
1776
1802
|
)
|
|
1777
1803
|
await conn.execute("COMMIT")
|
|
1778
1804
|
return inserted is not None
|
|
@@ -1871,8 +1897,12 @@ def _row_to_entity(row: Any) -> EntityResult:
|
|
|
1871
1897
|
"synced_at": row["synced_at"] if "synced_at" in keys else None,
|
|
1872
1898
|
"observed_at": row["observed_at"] if "observed_at" in keys else None,
|
|
1873
1899
|
"retention_policy": row["retention_policy"] if "retention_policy" in keys else "observed",
|
|
1874
|
-
"retention_parent_id": row["retention_parent_id"]
|
|
1875
|
-
|
|
1900
|
+
"retention_parent_id": row["retention_parent_id"]
|
|
1901
|
+
if "retention_parent_id" in keys
|
|
1902
|
+
else None,
|
|
1903
|
+
"cumulative_observation_duration_ms": row["cumulative_observation_duration_ms"]
|
|
1904
|
+
if "cumulative_observation_duration_ms" in keys
|
|
1905
|
+
else 0,
|
|
1876
1906
|
"bookmarked": bool(row["bookmarked"]) if "bookmarked" in keys else False,
|
|
1877
1907
|
"score": row["score"] if "score" in keys else None,
|
|
1878
1908
|
}
|
|
@@ -4,8 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json as _json
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
7
9
|
from collections.abc import Iterator
|
|
8
10
|
from contextlib import contextmanager
|
|
11
|
+
from importlib.metadata import PackageNotFoundError
|
|
12
|
+
from importlib.metadata import version as package_version
|
|
9
13
|
from typing import TYPE_CHECKING, cast
|
|
10
14
|
|
|
11
15
|
import typer
|
|
@@ -18,6 +22,33 @@ app = typer.Typer(
|
|
|
18
22
|
help="Local knowledge graph for AI agents.",
|
|
19
23
|
no_args_is_help=True,
|
|
20
24
|
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _version_callback(value: bool) -> None:
|
|
28
|
+
if not value:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
current = package_version("agentgraph-server")
|
|
33
|
+
except PackageNotFoundError:
|
|
34
|
+
current = "unknown"
|
|
35
|
+
typer.echo(f"agentgraph {current}")
|
|
36
|
+
raise typer.Exit()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback()
|
|
40
|
+
def main(
|
|
41
|
+
version: bool = typer.Option(
|
|
42
|
+
False,
|
|
43
|
+
"--version",
|
|
44
|
+
callback=_version_callback,
|
|
45
|
+
is_eager=True,
|
|
46
|
+
help="Show the installed version and exit.",
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Configure global CLI options."""
|
|
50
|
+
|
|
51
|
+
|
|
21
52
|
demo_app = typer.Typer(
|
|
22
53
|
help="Create reproducible local demo graphs.",
|
|
23
54
|
no_args_is_help=True,
|
|
@@ -50,30 +81,58 @@ def _status_label(status: str) -> str:
|
|
|
50
81
|
)
|
|
51
82
|
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
84
|
+
def _server_is_running() -> bool:
|
|
85
|
+
"""Return whether the configured AgentGraph server responds to health checks."""
|
|
86
|
+
from agentgraph.config import get_settings
|
|
87
|
+
|
|
88
|
+
settings = get_settings()
|
|
89
|
+
host = "127.0.0.1" if settings.server_host in ("", "0.0.0.0", "::") else settings.server_host
|
|
90
|
+
url = f"http://{host}:{settings.server_port}/health"
|
|
91
|
+
try:
|
|
92
|
+
with urllib.request.urlopen(url, timeout=1) as response:
|
|
93
|
+
return response.status == 200
|
|
94
|
+
except (OSError, urllib.error.URLError):
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@demo_app.command("add")
|
|
99
|
+
def add_demo_command(
|
|
56
100
|
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
57
101
|
) -> None:
|
|
58
|
-
"""
|
|
102
|
+
"""Add the fictional Atlas graph to the configured database."""
|
|
59
103
|
from agentgraph.config import get_config_paths
|
|
60
|
-
from agentgraph.demo import
|
|
104
|
+
from agentgraph.demo import add_demo
|
|
61
105
|
|
|
62
106
|
try:
|
|
63
|
-
result = asyncio.run(
|
|
64
|
-
except
|
|
107
|
+
result = asyncio.run(add_demo(get_config_paths()[0]))
|
|
108
|
+
except ValueError as exc:
|
|
65
109
|
typer.echo(str(exc), err=True)
|
|
66
110
|
raise typer.Exit(code=1) from exc
|
|
67
111
|
|
|
68
112
|
if json:
|
|
69
113
|
typer.echo(_json.dumps(result, indent=2))
|
|
70
114
|
return
|
|
71
|
-
typer.echo(f"
|
|
115
|
+
typer.echo(f"Added Atlas demo fixtures to {result['database']}")
|
|
72
116
|
typer.echo(
|
|
73
117
|
f"{result['entities']} entities, {result['persons']} people, {result['edges']} edges"
|
|
74
118
|
)
|
|
75
119
|
|
|
76
120
|
|
|
121
|
+
@demo_app.command("remove")
|
|
122
|
+
def remove_demo_command(
|
|
123
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Remove Atlas demo fixtures from the configured database."""
|
|
126
|
+
from agentgraph.config import get_config_paths
|
|
127
|
+
from agentgraph.demo import remove_demo
|
|
128
|
+
|
|
129
|
+
result = asyncio.run(remove_demo(get_config_paths()[0]))
|
|
130
|
+
if json:
|
|
131
|
+
typer.echo(_json.dumps(result, indent=2))
|
|
132
|
+
return
|
|
133
|
+
typer.echo(f"Removed {result['removed']} Atlas demo entities from {result['database']}")
|
|
134
|
+
|
|
135
|
+
|
|
77
136
|
def _remove_auth_credentials(provider: str, account_id: str | None) -> dict[str, object]:
|
|
78
137
|
from agentgraph.auth.credentials import remove_platform, remove_platform_account
|
|
79
138
|
|
|
@@ -416,6 +475,7 @@ def serve(
|
|
|
416
475
|
settings = get_settings()
|
|
417
476
|
configure_logging(settings.log_level, settings.log_file)
|
|
418
477
|
typer.echo(f"AgentGraph config directory: {get_config_paths()[0]}")
|
|
478
|
+
typer.echo(f"AgentGraph log file: {settings.log_file.expanduser()}")
|
|
419
479
|
uvicorn.run(
|
|
420
480
|
"agentgraph.server.app:app",
|
|
421
481
|
host=settings.server_host,
|
|
@@ -581,8 +641,15 @@ def onboard() -> None:
|
|
|
581
641
|
if label not in seen:
|
|
582
642
|
seen[label] = connector
|
|
583
643
|
|
|
584
|
-
|
|
585
|
-
|
|
644
|
+
# Only connectors with an explicit onboarding prompt own an interactive setup flow.
|
|
645
|
+
# Generic connectors such as Web are configured through connector commands instead.
|
|
646
|
+
steps = [
|
|
647
|
+
(label, connector)
|
|
648
|
+
for label, connector in seen.items()
|
|
649
|
+
if getattr(connector, "onboard_prompt", None)
|
|
650
|
+
]
|
|
651
|
+
steps.sort(key=lambda item: getattr(item[1], "onboard_last", False))
|
|
652
|
+
total = len(steps) + 1
|
|
586
653
|
|
|
587
654
|
typer.echo("=== AgentGraph Setup ===\n")
|
|
588
655
|
|
|
@@ -597,11 +664,12 @@ def onboard() -> None:
|
|
|
597
664
|
if i < total:
|
|
598
665
|
typer.echo()
|
|
599
666
|
|
|
667
|
+
typer.echo(f"\nStep {total}/{total}: Install the AgentGraph Chrome Extension")
|
|
600
668
|
typer.echo(
|
|
601
|
-
"\nInstall the AgentGraph Chrome Extension: "
|
|
602
669
|
"https://chromewebstore.google.com/detail/agentgraph-extension/iilkfclglabllelhjacijldknapbhidi"
|
|
603
670
|
)
|
|
604
|
-
|
|
671
|
+
if not _server_is_running():
|
|
672
|
+
typer.echo("Run `agentgraph serve` to start the server.")
|
|
605
673
|
|
|
606
674
|
|
|
607
675
|
@app.command()
|
|
@@ -125,7 +125,7 @@ class Settings(BaseSettings):
|
|
|
125
125
|
)
|
|
126
126
|
# Logging
|
|
127
127
|
log_level: str = Field(default="INFO")
|
|
128
|
-
log_file: Path = Field(
|
|
128
|
+
log_file: Path = Field(default_factory=lambda: get_config_paths()[0] / "agentgraph.log")
|
|
129
129
|
|
|
130
130
|
def __init__(self, **values: Any) -> None:
|
|
131
131
|
# Resolve the config-directory .env at instantiation time as well as
|
|
@@ -34,13 +34,13 @@ _URL_RE = re.compile(r"https?://\S+")
|
|
|
34
34
|
|
|
35
35
|
# Maps ResourceType values to entity_type strings stored in the DB
|
|
36
36
|
RESOURCE_TYPE_TO_ENTITY_TYPE: dict[str, str] = {
|
|
37
|
-
"channel":
|
|
38
|
-
"dm":
|
|
39
|
-
"document":
|
|
40
|
-
"folder":
|
|
41
|
-
"message":
|
|
37
|
+
"channel": "Channel",
|
|
38
|
+
"dm": "Channel",
|
|
39
|
+
"document": "Document",
|
|
40
|
+
"folder": "Folder",
|
|
41
|
+
"message": "Message",
|
|
42
42
|
"spreadsheet": "Spreadsheet",
|
|
43
|
-
"thread":
|
|
43
|
+
"thread": "Email",
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
|
|
@@ -76,10 +76,11 @@ class PersonRecord(BaseModel):
|
|
|
76
76
|
platform_username: str | None = None
|
|
77
77
|
canonical_email: str | None = None
|
|
78
78
|
display_name: str | None = None
|
|
79
|
+
metadata: dict[str, str | int | float | bool | None] = {}
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
class EntityRecord(BaseModel):
|
|
82
|
-
entity_type: str
|
|
83
|
+
entity_type: str # 'Message' | 'Document' | 'Channel' | 'Task'
|
|
83
84
|
platform: str
|
|
84
85
|
platform_entity_id: str
|
|
85
86
|
title: str | None = None
|
|
@@ -87,13 +88,13 @@ class EntityRecord(BaseModel):
|
|
|
87
88
|
source_created_at: datetime | None = None
|
|
88
89
|
source_updated_at: datetime | None = None
|
|
89
90
|
metadata: dict[str, str | int | float | bool | None] = {}
|
|
90
|
-
is_stub: bool = False
|
|
91
|
+
is_stub: bool = False # True → placeholder pending a full fetch; preserves synced_at=NULL
|
|
91
92
|
retention_policy: RetentionPolicy = "observed"
|
|
92
93
|
retention_parent_platform_entity_id: str | None = None
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
class EdgeRecord(BaseModel):
|
|
96
|
-
edge_type: str
|
|
97
|
+
edge_type: str # 'authored' | 'posted_in' | 'replied_to' | 'mentions'
|
|
97
98
|
source_platform_entity_id: str | None = None
|
|
98
99
|
source_platform_user_id: str | None = None
|
|
99
100
|
target_platform_entity_id: str | None = None
|
|
@@ -132,18 +133,22 @@ class EntityBatch(BaseModel):
|
|
|
132
133
|
if ref.source == entity.platform and ref.resource_id == entity.platform_entity_id:
|
|
133
134
|
continue
|
|
134
135
|
seen.add(key)
|
|
135
|
-
self.entities.append(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
136
|
+
self.entities.append(
|
|
137
|
+
EntityRecord(
|
|
138
|
+
entity_type=RESOURCE_TYPE_TO_ENTITY_TYPE[ref.resource_type],
|
|
139
|
+
platform=ref.source,
|
|
140
|
+
platform_entity_id=ref.resource_id,
|
|
141
|
+
is_stub=True,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
self.edges.append(
|
|
145
|
+
EdgeRecord(
|
|
146
|
+
edge_type="references",
|
|
147
|
+
source_platform_entity_id=entity.platform_entity_id,
|
|
148
|
+
target_platform_entity_id=ref.resource_id,
|
|
149
|
+
platform="cross",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
147
152
|
|
|
148
153
|
|
|
149
154
|
async def get_known_channel_syncs(
|
|
@@ -195,7 +200,6 @@ class ConnectorAccount(BaseModel):
|
|
|
195
200
|
metadata: dict[str, str] = Field(default_factory=dict)
|
|
196
201
|
|
|
197
202
|
|
|
198
|
-
|
|
199
203
|
class FetchPolicy:
|
|
200
204
|
"""Encapsulates refresh policy decisions for a resource."""
|
|
201
205
|
|
|
@@ -222,7 +226,7 @@ class FetchPolicy:
|
|
|
222
226
|
|
|
223
227
|
|
|
224
228
|
class BaseConnector(ABC):
|
|
225
|
-
source: ClassVar[str]
|
|
229
|
+
source: ClassVar[str] # platform name, e.g. "slack" — must be set by subclass
|
|
226
230
|
fetch_policy: ClassVar[FetchPolicy] # staleness policy — must be set by subclass
|
|
227
231
|
is_generic_url_fallback: ClassVar[bool] = False
|
|
228
232
|
"""True for broad fallback connectors that should not claim URLs during discovery."""
|
|
@@ -242,6 +246,7 @@ class BaseConnector(ABC):
|
|
|
242
246
|
auth_label: ClassVar[str | None] = None
|
|
243
247
|
auth_description: ClassVar[str | None] = None
|
|
244
248
|
onboard_prompt: ClassVar[str | None] = None
|
|
249
|
+
onboard_last: ClassVar[bool] = False
|
|
245
250
|
appears_in_auth_status: ClassVar[bool] = True
|
|
246
251
|
"""True for connectors backed by user/provider credentials.
|
|
247
252
|
|
|
@@ -280,14 +285,16 @@ class BaseConnector(ABC):
|
|
|
280
285
|
user = cls.get_authenticated_user()
|
|
281
286
|
if user is None:
|
|
282
287
|
return []
|
|
283
|
-
return [
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
return [
|
|
289
|
+
ConnectorAccount(
|
|
290
|
+
account_id=cls.source,
|
|
291
|
+
label=user,
|
|
292
|
+
auth_group=cls.auth_label or cls.source,
|
|
293
|
+
source=cls.source,
|
|
294
|
+
user_id=user,
|
|
295
|
+
email=user if "@" in user else None,
|
|
296
|
+
)
|
|
297
|
+
]
|
|
291
298
|
|
|
292
299
|
@classmethod
|
|
293
300
|
async def verify_auth(cls, account_id: str | None = None) -> tuple[str, str | None]:
|
|
@@ -339,9 +346,7 @@ class BaseConnector(ABC):
|
|
|
339
346
|
_ = (args, result)
|
|
340
347
|
return ConnectorCommandEffects()
|
|
341
348
|
|
|
342
|
-
def normalise_fetch_id(
|
|
343
|
-
self, resource_id: str, entity_type: str
|
|
344
|
-
) -> tuple[str, ResourceType]:
|
|
349
|
+
def normalise_fetch_id(self, resource_id: str, entity_type: str) -> tuple[str, ResourceType]:
|
|
345
350
|
"""Map a stored resource_id + entity_type to the (id, resource_type) that fetch() expects.
|
|
346
351
|
|
|
347
352
|
The default maps entity_type to ResourceType using the standard table and
|