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.
Files changed (96) hide show
  1. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/SKILL.md +4 -0
  2. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/PKG-INFO +1 -1
  3. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/backend.py +56 -26
  4. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/cli.py +80 -12
  5. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/config.py +1 -1
  6. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/base.py +39 -34
  7. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/demo.py +85 -45
  8. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/mcp/server.py +34 -0
  9. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/PKG-INFO +1 -1
  10. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/pyproject.toml +1 -1
  11. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_cli.py +158 -36
  12. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_config.py +1 -0
  13. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_connectors.py +6 -0
  14. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_launch_demo.py +57 -18
  15. agentgraph_server-0.6.0/tests/test_release_metadata.py +85 -0
  16. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_schema.py +97 -0
  17. agentgraph_server-0.5.3/tests/test_release_metadata.py +0 -38
  18. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/commands.md +0 -0
  19. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/data-model.md +0 -0
  20. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/operations.md +0 -0
  21. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/.agents/skills/slack-auth/SKILL.md +0 -0
  22. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/LICENSE +0 -0
  23. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/README.md +0 -0
  24. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/__init__.py +0 -0
  25. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/auth/__init__.py +0 -0
  26. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/auth/credentials.py +0 -0
  27. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/__init__.py +0 -0
  28. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/__init__.py +0 -0
  29. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/schema.sql +0 -0
  30. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/vector.py +0 -0
  31. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/cli_query.py +0 -0
  32. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/cli_sync.py +0 -0
  33. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/__init__.py +0 -0
  34. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/command_effects.py +0 -0
  35. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/registry.py +0 -0
  36. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/connectors/status.py +0 -0
  37. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/__init__.py +0 -0
  38. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/context.py +0 -0
  39. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/runtime.py +0 -0
  40. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/core/storage.py +0 -0
  41. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/demo_fixtures/reliable-webhooks.md +0 -0
  42. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/demo_fixtures/retry-guidance.md +0 -0
  43. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/__init__.py +0 -0
  44. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/bookmark.py +0 -0
  45. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/delete.py +0 -0
  46. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/download.py +0 -0
  47. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/embeddings.py +0 -0
  48. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/expiration.py +0 -0
  49. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/fetch.py +0 -0
  50. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/link.py +0 -0
  51. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/operations.py +0 -0
  52. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/person.py +0 -0
  53. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/query.py +0 -0
  54. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/graph/upsert.py +0 -0
  55. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/logging.py +0 -0
  56. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/mcp/__init__.py +0 -0
  57. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/perf.py +0 -0
  58. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/__init__.py +0 -0
  59. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/app.py +0 -0
  60. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/cli_api.py +0 -0
  61. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/meta_api.py +0 -0
  62. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/observation.py +0 -0
  63. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/router.py +0 -0
  64. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/static/viewer.html +0 -0
  65. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/sync.py +0 -0
  66. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/server/sync_api.py +0 -0
  67. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph/skills.py +0 -0
  68. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/SOURCES.txt +0 -0
  69. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/dependency_links.txt +0 -0
  70. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/entry_points.txt +0 -0
  71. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/requires.txt +0 -0
  72. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/top_level.txt +0 -0
  73. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/setup.cfg +0 -0
  74. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_auth.py +0 -0
  75. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_benchmarks.py +0 -0
  76. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_browse.py +0 -0
  77. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_command_effects.py +0 -0
  78. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_discord_attachments.py +0 -0
  79. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_docs_build.py +0 -0
  80. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_embeddings.py +0 -0
  81. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_expiration.py +0 -0
  82. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_fetch.py +0 -0
  83. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_graph_operations.py +0 -0
  84. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_logging.py +0 -0
  85. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_observation.py +0 -0
  86. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_query.py +0 -0
  87. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_registry.py +0 -0
  88. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_router.py +0 -0
  89. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_rss_connector.py +0 -0
  90. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_server_imports.py +0 -0
  91. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_slack_auth_artifacts.py +0 -0
  92. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_slack_oauth.py +0 -0
  93. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_sync.py +0 -0
  94. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_upsert.py +0 -0
  95. {agentgraph_server-0.5.3 → agentgraph_server-0.6.0}/tests/test_viewer_layout_contract.py +0 -0
  96. {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.5.3
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 = 2
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
- entity_columns = {
212
- str(row["name"]): bool(row["notnull"]) for row in await cursor.fetchall()
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
- f"ELSE {observed} END"
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 = p.canonical_email if p.canonical_email is not None else existing_row[1]
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 = str(existing_row["title"]) if existing_row and existing_row["title"] else ""
693
- existing_content = str(existing_row["content"]) if existing_row and existing_row["content"] else ""
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
- [observation_duration_ms, now, observation_duration_ms, now, platform, platform_entity_id],
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
- [observation_duration_ms, now, observation_duration_ms, now, platform, platform_entity_id],
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"] if "retention_parent_id" in keys else None,
1875
- "cumulative_observation_duration_ms": row["cumulative_observation_duration_ms"] if "cumulative_observation_duration_ms" in keys else 0,
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
- @demo_app.command("seed")
54
- def seed_demo_command(
55
- force: bool = typer.Option(False, "--force", help="Replace an existing demo database"),
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
- """Seed the fictional Atlas graph used by the launch demo."""
102
+ """Add the fictional Atlas graph to the configured database."""
59
103
  from agentgraph.config import get_config_paths
60
- from agentgraph.demo import seed_demo
104
+ from agentgraph.demo import add_demo
61
105
 
62
106
  try:
63
- result = asyncio.run(seed_demo(get_config_paths()[0], force=force))
64
- except (FileExistsError, ValueError) as exc:
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"Seeded Atlas demo graph at {result['database']}")
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
- steps = list(seen.items())
585
- total = len(steps)
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
- typer.echo("Run `agentgraph serve` to start the server.")
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(default=Path("/tmp/agentgraph.log"))
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": "Channel",
38
- "dm": "Channel",
39
- "document": "Document",
40
- "folder": "Folder",
41
- "message": "Message",
37
+ "channel": "Channel",
38
+ "dm": "Channel",
39
+ "document": "Document",
40
+ "folder": "Folder",
41
+ "message": "Message",
42
42
  "spreadsheet": "Spreadsheet",
43
- "thread": "Email",
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 # 'Message' | 'Document' | 'Channel' | 'Task'
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 # True → placeholder pending a full fetch; preserves synced_at=NULL
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 # 'authored' | 'posted_in' | 'replied_to' | 'mentions'
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(EntityRecord(
136
- entity_type=RESOURCE_TYPE_TO_ENTITY_TYPE[ref.resource_type],
137
- platform=ref.source,
138
- platform_entity_id=ref.resource_id,
139
- is_stub=True,
140
- ))
141
- self.edges.append(EdgeRecord(
142
- edge_type="references",
143
- source_platform_entity_id=entity.platform_entity_id,
144
- target_platform_entity_id=ref.resource_id,
145
- platform="cross",
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] # platform name, e.g. "slack" — must be set by subclass
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 [ConnectorAccount(
284
- account_id=cls.source,
285
- label=user,
286
- auth_group=cls.auth_label or cls.source,
287
- source=cls.source,
288
- user_id=user,
289
- email=user if "@" in user else None,
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