agentgraph-server 0.5.3__tar.gz → 0.5.4__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.5.4}/.agents/skills/AgentGraph/SKILL.md +4 -0
  2. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/PKG-INFO +1 -1
  3. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/backends/sqlite/backend.py +41 -19
  4. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/cli.py +22 -8
  5. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/config.py +1 -1
  6. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/connectors/base.py +38 -34
  7. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/demo.py +85 -45
  8. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/mcp/server.py +34 -0
  9. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph_server.egg-info/PKG-INFO +1 -1
  10. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/pyproject.toml +1 -1
  11. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_cli.py +43 -36
  12. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_config.py +1 -0
  13. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_launch_demo.py +57 -18
  14. agentgraph_server-0.5.4/tests/test_release_metadata.py +85 -0
  15. agentgraph_server-0.5.3/tests/test_release_metadata.py +0 -38
  16. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/.agents/skills/AgentGraph/references/commands.md +0 -0
  17. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/.agents/skills/AgentGraph/references/data-model.md +0 -0
  18. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/.agents/skills/AgentGraph/references/operations.md +0 -0
  19. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/.agents/skills/slack-auth/SKILL.md +0 -0
  20. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/LICENSE +0 -0
  21. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/README.md +0 -0
  22. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/__init__.py +0 -0
  23. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/auth/__init__.py +0 -0
  24. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/auth/credentials.py +0 -0
  25. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/backends/__init__.py +0 -0
  26. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/backends/sqlite/__init__.py +0 -0
  27. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/backends/sqlite/schema.sql +0 -0
  28. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/backends/sqlite/vector.py +0 -0
  29. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/cli_query.py +0 -0
  30. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/cli_sync.py +0 -0
  31. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/connectors/__init__.py +0 -0
  32. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/connectors/command_effects.py +0 -0
  33. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/connectors/registry.py +0 -0
  34. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/connectors/status.py +0 -0
  35. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/core/__init__.py +0 -0
  36. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/core/context.py +0 -0
  37. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/core/runtime.py +0 -0
  38. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/core/storage.py +0 -0
  39. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/demo_fixtures/reliable-webhooks.md +0 -0
  40. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/demo_fixtures/retry-guidance.md +0 -0
  41. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/__init__.py +0 -0
  42. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/bookmark.py +0 -0
  43. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/delete.py +0 -0
  44. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/download.py +0 -0
  45. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/embeddings.py +0 -0
  46. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/expiration.py +0 -0
  47. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/fetch.py +0 -0
  48. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/link.py +0 -0
  49. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/operations.py +0 -0
  50. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/person.py +0 -0
  51. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/query.py +0 -0
  52. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/graph/upsert.py +0 -0
  53. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/logging.py +0 -0
  54. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/mcp/__init__.py +0 -0
  55. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/perf.py +0 -0
  56. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/__init__.py +0 -0
  57. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/app.py +0 -0
  58. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/cli_api.py +0 -0
  59. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/meta_api.py +0 -0
  60. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/observation.py +0 -0
  61. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/router.py +0 -0
  62. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/static/viewer.html +0 -0
  63. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/sync.py +0 -0
  64. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/server/sync_api.py +0 -0
  65. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph/skills.py +0 -0
  66. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph_server.egg-info/SOURCES.txt +0 -0
  67. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph_server.egg-info/dependency_links.txt +0 -0
  68. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph_server.egg-info/entry_points.txt +0 -0
  69. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph_server.egg-info/requires.txt +0 -0
  70. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/agentgraph_server.egg-info/top_level.txt +0 -0
  71. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/setup.cfg +0 -0
  72. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_auth.py +0 -0
  73. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_benchmarks.py +0 -0
  74. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_browse.py +0 -0
  75. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_command_effects.py +0 -0
  76. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_connectors.py +0 -0
  77. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_discord_attachments.py +0 -0
  78. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_docs_build.py +0 -0
  79. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_embeddings.py +0 -0
  80. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_expiration.py +0 -0
  81. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_fetch.py +0 -0
  82. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_graph_operations.py +0 -0
  83. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_logging.py +0 -0
  84. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_observation.py +0 -0
  85. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_query.py +0 -0
  86. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_registry.py +0 -0
  87. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_router.py +0 -0
  88. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_rss_connector.py +0 -0
  89. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_schema.py +0 -0
  90. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_server_imports.py +0 -0
  91. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_slack_auth_artifacts.py +0 -0
  92. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_slack_oauth.py +0 -0
  93. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_sync.py +0 -0
  94. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_upsert.py +0 -0
  95. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/tests/test_viewer_layout_contract.py +0 -0
  96. {agentgraph_server-0.5.3 → agentgraph_server-0.5.4}/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.5.4
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
@@ -208,9 +208,7 @@ 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
+ entity_columns = {str(row["name"]): bool(row["notnull"]) for row in await cursor.fetchall()}
214
212
  columns = set(entity_columns)
215
213
  if "cumulative_dwell_ms" in columns:
216
214
  await conn.execute(
@@ -249,9 +247,7 @@ class SQLiteBackend(StorageBackend):
249
247
  "UPDATE observations SET event_type = 'observation_threshold' "
250
248
  "WHERE event_type = 'dwell_threshold'"
251
249
  )
252
- await conn.execute(
253
- "UPDATE entities SET entity_type = 'Email' WHERE entity_type = 'Thread'"
254
- )
250
+ await conn.execute("UPDATE entities SET entity_type = 'Email' WHERE entity_type = 'Thread'")
255
251
  await conn.execute(
256
252
  "CREATE INDEX IF NOT EXISTS idx_entities_bookmarked ON entities(bookmarked)"
257
253
  )
@@ -352,10 +348,13 @@ class SQLiteBackend(StorageBackend):
352
348
  "AND cumulative_observation_duration_ms > 0 THEN observed_at ELSE NULL END"
353
349
  )
354
350
  observed = (
355
- "CASE WHEN platform = 'rss' AND entity_type = 'Folder' THEN NULL "
356
- f"ELSE {observed} END"
351
+ f"CASE WHEN platform = 'rss' AND entity_type = 'Folder' THEN NULL ELSE {observed} END"
352
+ )
353
+ duration = (
354
+ "cumulative_observation_duration_ms"
355
+ if "cumulative_observation_duration_ms" in columns
356
+ else "0"
357
357
  )
358
- duration = "cumulative_observation_duration_ms" if "cumulative_observation_duration_ms" in columns else "0"
359
358
  bookmarked = "bookmarked" if "bookmarked" in columns else "0"
360
359
  await conn.execute("PRAGMA foreign_keys=OFF")
361
360
  try:
@@ -503,6 +502,7 @@ class SQLiteBackend(StorageBackend):
503
502
  for p in persons:
504
503
  canonical_key = p.canonical_email or f"{p.platform}:{p.platform_user_id}"
505
504
  meta: dict[str, str] = {}
505
+ meta.update({key: str(value) for key, value in p.metadata.items() if value is not None})
506
506
  if p.canonical_email:
507
507
  meta["canonical_email"] = p.canonical_email
508
508
  meta[f"{p.platform}_user_id"] = p.platform_user_id
@@ -534,7 +534,9 @@ class SQLiteBackend(StorageBackend):
534
534
  existing_metadata = json.loads(existing_row[3] or "{}") if existing_row else {}
535
535
  merged_metadata = {**existing_metadata, **meta}
536
536
  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]
537
+ new_content = (
538
+ p.canonical_email if p.canonical_email is not None else existing_row[1]
539
+ )
538
540
  new_embedding = emb_blob if emb_blob is not None else existing_row[2]
539
541
  changed = (
540
542
  new_title != existing_row[0]
@@ -647,9 +649,7 @@ class SQLiteBackend(StorageBackend):
647
649
  if parent_id is None:
648
650
  parent_id = await self._resolve_existing_entity_id(conn, e.platform, parent_ref)
649
651
  if parent_id is None:
650
- raise ValueError(
651
- f"Retention parent {e.platform}:{parent_ref} is not available"
652
- )
652
+ raise ValueError(f"Retention parent {e.platform}:{parent_ref} is not available")
653
653
  elif e.retention_parent_platform_entity_id is not None:
654
654
  raise ValueError(
655
655
  f"Entity {e.platform}:{e.platform_entity_id} has a parent but is not owned"
@@ -689,8 +689,12 @@ class SQLiteBackend(StorageBackend):
689
689
  [e.platform, e.platform_entity_id],
690
690
  )
691
691
  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 ""
692
+ existing_title = (
693
+ str(existing_row["title"]) if existing_row and existing_row["title"] else ""
694
+ )
695
+ existing_content = (
696
+ str(existing_row["content"]) if existing_row and existing_row["content"] else ""
697
+ )
694
698
  fts_title = e.title if e.title is not None else existing_title
695
699
  fts_content = e.content if e.content is not None else existing_content
696
700
  rewrite_fts = (
@@ -1734,7 +1738,14 @@ class SQLiteBackend(StorageBackend):
1734
1738
  WHERE platform = ? AND platform_entity_id = ?
1735
1739
  AND retention_policy = 'observed'
1736
1740
  """,
1737
- [observation_duration_ms, now, observation_duration_ms, now, platform, platform_entity_id],
1741
+ [
1742
+ observation_duration_ms,
1743
+ now,
1744
+ observation_duration_ms,
1745
+ now,
1746
+ platform,
1747
+ platform_entity_id,
1748
+ ],
1738
1749
  )
1739
1750
 
1740
1751
  async def record_observation_once(
@@ -1772,7 +1783,14 @@ class SQLiteBackend(StorageBackend):
1772
1783
  WHERE platform = ? AND platform_entity_id = ?
1773
1784
  AND retention_policy = 'observed'
1774
1785
  """,
1775
- [observation_duration_ms, now, observation_duration_ms, now, platform, platform_entity_id],
1786
+ [
1787
+ observation_duration_ms,
1788
+ now,
1789
+ observation_duration_ms,
1790
+ now,
1791
+ platform,
1792
+ platform_entity_id,
1793
+ ],
1776
1794
  )
1777
1795
  await conn.execute("COMMIT")
1778
1796
  return inserted is not None
@@ -1871,8 +1889,12 @@ def _row_to_entity(row: Any) -> EntityResult:
1871
1889
  "synced_at": row["synced_at"] if "synced_at" in keys else None,
1872
1890
  "observed_at": row["observed_at"] if "observed_at" in keys else None,
1873
1891
  "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,
1892
+ "retention_parent_id": row["retention_parent_id"]
1893
+ if "retention_parent_id" in keys
1894
+ else None,
1895
+ "cumulative_observation_duration_ms": row["cumulative_observation_duration_ms"]
1896
+ if "cumulative_observation_duration_ms" in keys
1897
+ else 0,
1876
1898
  "bookmarked": bool(row["bookmarked"]) if "bookmarked" in keys else False,
1877
1899
  "score": row["score"] if "score" in keys else None,
1878
1900
  }
@@ -50,30 +50,44 @@ def _status_label(status: str) -> str:
50
50
  )
51
51
 
52
52
 
53
- @demo_app.command("seed")
54
- def seed_demo_command(
55
- force: bool = typer.Option(False, "--force", help="Replace an existing demo database"),
53
+ @demo_app.command("add")
54
+ def add_demo_command(
56
55
  json: bool = typer.Option(False, "--json", help="Output as JSON"),
57
56
  ) -> None:
58
- """Seed the fictional Atlas graph used by the launch demo."""
57
+ """Add the fictional Atlas graph to the configured database."""
59
58
  from agentgraph.config import get_config_paths
60
- from agentgraph.demo import seed_demo
59
+ from agentgraph.demo import add_demo
61
60
 
62
61
  try:
63
- result = asyncio.run(seed_demo(get_config_paths()[0], force=force))
64
- except (FileExistsError, ValueError) as exc:
62
+ result = asyncio.run(add_demo(get_config_paths()[0]))
63
+ except ValueError as exc:
65
64
  typer.echo(str(exc), err=True)
66
65
  raise typer.Exit(code=1) from exc
67
66
 
68
67
  if json:
69
68
  typer.echo(_json.dumps(result, indent=2))
70
69
  return
71
- typer.echo(f"Seeded Atlas demo graph at {result['database']}")
70
+ typer.echo(f"Added Atlas demo fixtures to {result['database']}")
72
71
  typer.echo(
73
72
  f"{result['entities']} entities, {result['persons']} people, {result['edges']} edges"
74
73
  )
75
74
 
76
75
 
76
+ @demo_app.command("remove")
77
+ def remove_demo_command(
78
+ json: bool = typer.Option(False, "--json", help="Output as JSON"),
79
+ ) -> None:
80
+ """Remove Atlas demo fixtures from the configured database."""
81
+ from agentgraph.config import get_config_paths
82
+ from agentgraph.demo import remove_demo
83
+
84
+ result = asyncio.run(remove_demo(get_config_paths()[0]))
85
+ if json:
86
+ typer.echo(_json.dumps(result, indent=2))
87
+ return
88
+ typer.echo(f"Removed {result['removed']} Atlas demo entities from {result['database']}")
89
+
90
+
77
91
  def _remove_auth_credentials(provider: str, account_id: str | None) -> dict[str, object]:
78
92
  from agentgraph.auth.credentials import remove_platform, remove_platform_account
79
93
 
@@ -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."""
@@ -280,14 +284,16 @@ class BaseConnector(ABC):
280
284
  user = cls.get_authenticated_user()
281
285
  if user is None:
282
286
  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
- )]
287
+ return [
288
+ ConnectorAccount(
289
+ account_id=cls.source,
290
+ label=user,
291
+ auth_group=cls.auth_label or cls.source,
292
+ source=cls.source,
293
+ user_id=user,
294
+ email=user if "@" in user else None,
295
+ )
296
+ ]
291
297
 
292
298
  @classmethod
293
299
  async def verify_auth(cls, account_id: str | None = None) -> tuple[str, str | None]:
@@ -339,9 +345,7 @@ class BaseConnector(ABC):
339
345
  _ = (args, result)
340
346
  return ConnectorCommandEffects()
341
347
 
342
- def normalise_fetch_id(
343
- self, resource_id: str, entity_type: str
344
- ) -> tuple[str, ResourceType]:
348
+ def normalise_fetch_id(self, resource_id: str, entity_type: str) -> tuple[str, ResourceType]:
345
349
  """Map a stored resource_id + entity_type to the (id, resource_type) that fetch() expects.
346
350
 
347
351
  The default maps entity_type to ResourceType using the standard table and
@@ -11,6 +11,8 @@ from agentgraph.backends.sqlite.backend import SQLiteBackend
11
11
  from agentgraph.connectors.base import EdgeRecord, EntityBatch, EntityRecord, PersonRecord
12
12
 
13
13
  DEMO_DATABASE_NAME = "agentgraph.db"
14
+ DEMO_FIXTURE_METADATA_KEY = "_agentgraph_fixture"
15
+ DEMO_FIXTURE_METADATA_VALUE = "atlas-demo"
14
16
  DEMO_CREATED_AT = "2026-08-14T08:00:00Z"
15
17
  WEBHOOK_ARTICLE_URL = (
16
18
  "https://github.com/simonexmachina/agent-graph/blob/main/"
@@ -28,10 +30,7 @@ def utc(year: int, month: int, day: int, hour: int, minute: int = 0) -> datetime
28
30
 
29
31
  def load_research_fixture(filename: str) -> str:
30
32
  return (
31
- files("agentgraph")
32
- .joinpath("demo_fixtures", filename)
33
- .read_text(encoding="utf-8")
34
- .strip()
33
+ files("agentgraph").joinpath("demo_fixtures", filename).read_text(encoding="utf-8").strip()
35
34
  )
36
35
 
37
36
 
@@ -44,6 +43,7 @@ def build_demo_batch() -> EntityBatch:
44
43
  drive_folder_id = "atlas-program-folder"
45
44
  drive_plan_id = "atlas-integration-plan"
46
45
 
46
+ fixture_metadata = {DEMO_FIXTURE_METADATA_KEY: DEMO_FIXTURE_METADATA_VALUE}
47
47
  entities = [
48
48
  EntityRecord(
49
49
  entity_type="Channel",
@@ -53,6 +53,7 @@ def build_demo_batch() -> EntityBatch:
53
53
  source_created_at=utc(2026, 7, 1, 9),
54
54
  source_updated_at=utc(2026, 8, 10, 14),
55
55
  metadata={
56
+ **fixture_metadata,
56
57
  "web_url": "https://app.slack.com/client/TDEMO/CATLAS",
57
58
  "workspace_name": "AgentGraph Demo",
58
59
  },
@@ -69,7 +70,10 @@ def build_demo_batch() -> EntityBatch:
69
70
  ),
70
71
  source_created_at=utc(2026, 8, 10, 10),
71
72
  source_updated_at=utc(2026, 8, 10, 10),
72
- metadata={"web_url": "https://app.slack.com/client/TDEMO/CATLAS/thread-1"},
73
+ metadata={
74
+ **fixture_metadata,
75
+ "web_url": "https://app.slack.com/client/TDEMO/CATLAS/thread-1",
76
+ },
73
77
  retention_policy="owned",
74
78
  retention_parent_platform_entity_id=channel_id,
75
79
  ),
@@ -83,7 +87,10 @@ def build_demo_batch() -> EntityBatch:
83
87
  ),
84
88
  source_created_at=utc(2026, 8, 10, 10, 30),
85
89
  source_updated_at=utc(2026, 8, 10, 10, 30),
86
- metadata={"web_url": "https://app.slack.com/client/TDEMO/CATLAS/thread-1-reply-1"},
90
+ metadata={
91
+ **fixture_metadata,
92
+ "web_url": "https://app.slack.com/client/TDEMO/CATLAS/thread-1-reply-1",
93
+ },
87
94
  retention_policy="owned",
88
95
  retention_parent_platform_entity_id=channel_id,
89
96
  ),
@@ -97,7 +104,10 @@ def build_demo_batch() -> EntityBatch:
97
104
  ),
98
105
  source_created_at=utc(2026, 8, 10, 11),
99
106
  source_updated_at=utc(2026, 8, 10, 11),
100
- metadata={"web_url": "https://app.slack.com/client/TDEMO/CATLAS/thread-1-reply-2"},
107
+ metadata={
108
+ **fixture_metadata,
109
+ "web_url": "https://app.slack.com/client/TDEMO/CATLAS/thread-1-reply-2",
110
+ },
101
111
  retention_policy="owned",
102
112
  retention_parent_platform_entity_id=channel_id,
103
113
  ),
@@ -116,6 +126,7 @@ def build_demo_batch() -> EntityBatch:
116
126
  source_created_at=utc(2026, 8, 9, 15),
117
127
  source_updated_at=utc(2026, 8, 9, 15),
118
128
  metadata={
129
+ **fixture_metadata,
119
130
  "web_url": "https://mail.google.com/mail/u/0/#all/atlas-maya-thread",
120
131
  "snippet": "For the September 30 Atlas launch...",
121
132
  },
@@ -127,7 +138,10 @@ def build_demo_batch() -> EntityBatch:
127
138
  title="Atlas program",
128
139
  source_created_at=utc(2026, 7, 1, 8),
129
140
  source_updated_at=utc(2026, 8, 6, 9),
130
- metadata={"web_url": "https://drive.google.com/drive/folders/atlas-program-folder"},
141
+ metadata={
142
+ **fixture_metadata,
143
+ "web_url": "https://drive.google.com/drive/folders/atlas-program-folder",
144
+ },
131
145
  ),
132
146
  EntityRecord(
133
147
  entity_type="Document",
@@ -143,6 +157,7 @@ def build_demo_batch() -> EntityBatch:
143
157
  source_created_at=utc(2026, 8, 2, 9),
144
158
  source_updated_at=utc(2026, 8, 6, 16),
145
159
  metadata={
160
+ **fixture_metadata,
146
161
  "web_url": "https://drive.google.com/file/d/atlas-integration-plan/view",
147
162
  "mime_type": "text/markdown",
148
163
  },
@@ -155,7 +170,7 @@ def build_demo_batch() -> EntityBatch:
155
170
  content=load_research_fixture("reliable-webhooks.md"),
156
171
  source_created_at=utc(2026, 8, 4, 9),
157
172
  source_updated_at=utc(2026, 8, 4, 9),
158
- metadata={"web_url": WEBHOOK_ARTICLE_URL, "author": "Nadia Okafor"},
173
+ metadata={**fixture_metadata, "web_url": WEBHOOK_ARTICLE_URL, "author": "Nadia Okafor"},
159
174
  ),
160
175
  EntityRecord(
161
176
  entity_type="Document",
@@ -165,7 +180,7 @@ def build_demo_batch() -> EntityBatch:
165
180
  content=load_research_fixture("retry-guidance.md"),
166
181
  source_created_at=utc(2026, 8, 8, 9),
167
182
  source_updated_at=utc(2026, 8, 8, 9),
168
- metadata={"web_url": RETRY_GUIDE_URL, "revision": "3"},
183
+ metadata={**fixture_metadata, "web_url": RETRY_GUIDE_URL, "revision": "3"},
169
184
  ),
170
185
  ]
171
186
 
@@ -176,12 +191,14 @@ def build_demo_batch() -> EntityBatch:
176
191
  platform_username="alex",
177
192
  canonical_email="alex@agentgraph.demo",
178
193
  display_name="Alex Chen",
194
+ metadata=fixture_metadata,
179
195
  ),
180
196
  PersonRecord(
181
197
  platform="gdrive",
182
198
  platform_user_id="alex@agentgraph.demo",
183
199
  canonical_email="alex@agentgraph.demo",
184
200
  display_name="Alex Chen",
201
+ metadata=fixture_metadata,
185
202
  ),
186
203
  PersonRecord(
187
204
  platform="slack",
@@ -189,12 +206,14 @@ def build_demo_batch() -> EntityBatch:
189
206
  platform_username="priya",
190
207
  canonical_email="priya@agentgraph.demo",
191
208
  display_name="Priya Raman",
209
+ metadata=fixture_metadata,
192
210
  ),
193
211
  PersonRecord(
194
212
  platform="gmail",
195
213
  platform_user_id="maya@customer.example",
196
214
  canonical_email="maya@customer.example",
197
215
  display_name="Maya Patel",
216
+ metadata=fixture_metadata,
198
217
  ),
199
218
  ]
200
219
 
@@ -284,21 +303,6 @@ def build_demo_batch() -> EntityBatch:
284
303
  return EntityBatch(entities=entities, persons=persons, edges=edges)
285
304
 
286
305
 
287
- def validate_demo_config_dir(config_dir: Path) -> Path:
288
- resolved = config_dir.expanduser().resolve()
289
- default_dir = (Path.home() / ".agentgraph").resolve()
290
- if resolved == default_dir:
291
- raise ValueError("Refusing to seed the default ~/.agentgraph directory")
292
- return resolved
293
-
294
-
295
- def remove_demo_database(database_path: Path) -> None:
296
- for suffix in ("", "-wal", "-shm"):
297
- candidate = Path(f"{database_path}{suffix}")
298
- if candidate.exists():
299
- candidate.unlink()
300
-
301
-
302
306
  def apply_demo_timestamps(database_path: Path) -> None:
303
307
  observations = {
304
308
  ("slack", "TDEMO/CATLAS"): "2026-08-10T11:05:00Z",
@@ -308,29 +312,41 @@ def apply_demo_timestamps(database_path: Path) -> None:
308
312
  }
309
313
  with sqlite3.connect(database_path) as conn:
310
314
  conn.execute(
311
- "UPDATE entities SET created_at = ?, updated_at = ?, synced_at = ?",
312
- [DEMO_CREATED_AT, DEMO_CREATED_AT, DEMO_CREATED_AT],
315
+ """
316
+ UPDATE entities
317
+ SET created_at = ?, updated_at = ?, synced_at = ?
318
+ WHERE json_extract(metadata, ?) = ?
319
+ """,
320
+ [
321
+ DEMO_CREATED_AT,
322
+ DEMO_CREATED_AT,
323
+ DEMO_CREATED_AT,
324
+ f"$.{DEMO_FIXTURE_METADATA_KEY}",
325
+ DEMO_FIXTURE_METADATA_VALUE,
326
+ ],
313
327
  )
314
328
  for (platform, platform_entity_id), observed_at in observations.items():
315
329
  conn.execute(
316
330
  """
317
331
  UPDATE entities
318
332
  SET observed_at = ?, cumulative_observation_duration_ms = 3000
319
- WHERE platform = ? AND platform_entity_id = ?
333
+ WHERE platform = ?
334
+ AND platform_entity_id = ?
335
+ AND json_extract(metadata, ?) = ?
320
336
  """,
321
- [observed_at, platform, platform_entity_id],
337
+ [
338
+ observed_at,
339
+ platform,
340
+ platform_entity_id,
341
+ f"$.{DEMO_FIXTURE_METADATA_KEY}",
342
+ DEMO_FIXTURE_METADATA_VALUE,
343
+ ],
322
344
  )
323
345
 
324
346
 
325
- async def seed_demo(config_dir: Path, *, force: bool = False) -> dict[str, object]:
326
- safe_dir = validate_demo_config_dir(config_dir)
327
- database_path = safe_dir / DEMO_DATABASE_NAME
328
- if database_path.exists() and not force:
329
- raise FileExistsError(f"Demo database already exists: {database_path}. Use --force to replace it.")
330
- safe_dir.mkdir(parents=True, exist_ok=True)
331
- if force:
332
- remove_demo_database(database_path)
333
-
347
+ async def add_demo(config_dir: Path) -> dict[str, object]:
348
+ database_path = config_dir / DEMO_DATABASE_NAME
349
+ config_dir.mkdir(parents=True, exist_ok=True)
334
350
  batch = build_demo_batch()
335
351
  backend = SQLiteBackend(str(database_path), vector_mode="bm25-only")
336
352
  await backend.initialize()
@@ -339,13 +355,11 @@ async def seed_demo(config_dir: Path, *, force: bool = False) -> dict[str, objec
339
355
  finally:
340
356
  await backend.close()
341
357
  apply_demo_timestamps(database_path)
342
-
343
- with sqlite3.connect(database_path) as conn:
344
- entity_count = int(conn.execute("SELECT count(*) FROM entities").fetchone()[0])
345
- person_count = int(
346
- conn.execute("SELECT count(*) FROM entities WHERE entity_type = 'Person'").fetchone()[0]
347
- )
348
- edge_count = int(conn.execute("SELECT count(*) FROM edges").fetchone()[0])
358
+ entity_count = len(batch.entities)
359
+ person_count = len(
360
+ {p.canonical_email or f"{p.platform}:{p.platform_user_id}" for p in batch.persons}
361
+ )
362
+ edge_count = len(batch.edges)
349
363
  return {
350
364
  "database": str(database_path),
351
365
  "entities": entity_count,
@@ -354,3 +368,29 @@ async def seed_demo(config_dir: Path, *, force: bool = False) -> dict[str, objec
354
368
  "webhook_article": WEBHOOK_ARTICLE_URL,
355
369
  "retry_guide": RETRY_GUIDE_URL,
356
370
  }
371
+
372
+
373
+ async def remove_demo(config_dir: Path) -> dict[str, object]:
374
+ database_path = config_dir / DEMO_DATABASE_NAME
375
+ if not database_path.exists():
376
+ return {"database": str(database_path), "removed": 0}
377
+
378
+ with sqlite3.connect(database_path) as conn:
379
+ rows = conn.execute(
380
+ """
381
+ SELECT id FROM entities
382
+ WHERE json_extract(metadata, ?) = ?
383
+ """,
384
+ [f"$.{DEMO_FIXTURE_METADATA_KEY}", DEMO_FIXTURE_METADATA_VALUE],
385
+ ).fetchall()
386
+ entity_ids = [str(row[0]) for row in rows]
387
+
388
+ backend = SQLiteBackend(str(database_path), vector_mode="bm25-only")
389
+ await backend.initialize()
390
+ try:
391
+ for entity_id in entity_ids:
392
+ if await backend.get_entity_by_id(entity_id) is not None:
393
+ await backend.delete_entity(entity_id)
394
+ finally:
395
+ await backend.close()
396
+ return {"database": str(database_path), "removed": len(entity_ids)}
@@ -326,6 +326,40 @@ async def run_connector_command_tool(source: str, args: list[str]) -> str:
326
326
  return json.dumps({"error": str(exc)})
327
327
 
328
328
 
329
+ @mcp.tool(
330
+ annotations=_tool_annotations(
331
+ "Add AgentGraph demo fixtures",
332
+ read_only=False,
333
+ destructive=False,
334
+ idempotent=True,
335
+ open_world=False,
336
+ )
337
+ )
338
+ async def add_demo_tool() -> str:
339
+ """Add the fictional Atlas demo fixtures to the configured database."""
340
+ from agentgraph.config import get_config_paths
341
+ from agentgraph.demo import add_demo
342
+
343
+ return json.dumps(await add_demo(get_config_paths()[0]))
344
+
345
+
346
+ @mcp.tool(
347
+ annotations=_tool_annotations(
348
+ "Remove AgentGraph demo fixtures",
349
+ read_only=False,
350
+ destructive=True,
351
+ idempotent=True,
352
+ open_world=False,
353
+ )
354
+ )
355
+ async def remove_demo_tool() -> str:
356
+ """Remove marked Atlas demo fixtures from the configured database."""
357
+ from agentgraph.config import get_config_paths
358
+ from agentgraph.demo import remove_demo
359
+
360
+ return json.dumps(await remove_demo(get_config_paths()[0]))
361
+
362
+
329
363
  async def _enrich_results(results: list[dict[str, Any]]) -> None:
330
364
  """Let each owning connector apply result presentation fixes in-place."""
331
365
  from agentgraph.connectors.registry import bootstrap, get_connector
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentgraph-server
3
- Version: 0.5.3
3
+ Version: 0.5.4
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