agentgraph-server 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentgraph/__init__.py +1 -0
- agentgraph/auth/__init__.py +0 -0
- agentgraph/auth/credentials.py +224 -0
- agentgraph/backends/__init__.py +50 -0
- agentgraph/backends/sqlite/__init__.py +1 -0
- agentgraph/backends/sqlite/backend.py +1471 -0
- agentgraph/backends/sqlite/vector.py +142 -0
- agentgraph/cli.py +721 -0
- agentgraph/cli_query.py +519 -0
- agentgraph/config.py +90 -0
- agentgraph/connectors/__init__.py +0 -0
- agentgraph/connectors/base.py +455 -0
- agentgraph/connectors/registry.py +78 -0
- agentgraph/connectors/status.py +244 -0
- agentgraph/core/__init__.py +0 -0
- agentgraph/core/context.py +26 -0
- agentgraph/core/runtime.py +36 -0
- agentgraph/core/storage.py +240 -0
- agentgraph/graph/__init__.py +1 -0
- agentgraph/graph/bookmark.py +87 -0
- agentgraph/graph/delete.py +17 -0
- agentgraph/graph/download.py +35 -0
- agentgraph/graph/embeddings.py +58 -0
- agentgraph/graph/fetch.py +53 -0
- agentgraph/graph/gc.py +26 -0
- agentgraph/graph/link.py +63 -0
- agentgraph/graph/person.py +40 -0
- agentgraph/graph/query.py +244 -0
- agentgraph/graph/upsert.py +49 -0
- agentgraph/logging.py +78 -0
- agentgraph/mcp/__init__.py +0 -0
- agentgraph/mcp/server.py +811 -0
- agentgraph/perf.py +43 -0
- agentgraph/server/__init__.py +0 -0
- agentgraph/server/app.py +133 -0
- agentgraph/server/cli_api.py +708 -0
- agentgraph/server/dwell.py +79 -0
- agentgraph/server/graph_api.py +46 -0
- agentgraph/server/router.py +47 -0
- agentgraph/server/sync.py +247 -0
- agentgraph/skills.py +93 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/graph/SKILL.md +159 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/slack-auth/SKILL.md +92 -0
- agentgraph_server-0.5.0.dist-info/METADATA +286 -0
- agentgraph_server-0.5.0.dist-info/RECORD +49 -0
- agentgraph_server-0.5.0.dist-info/WHEEL +5 -0
- agentgraph_server-0.5.0.dist-info/entry_points.txt +2 -0
- agentgraph_server-0.5.0.dist-info/licenses/LICENSE +21 -0
- agentgraph_server-0.5.0.dist-info/top_level.txt +1 -0
agentgraph/cli_query.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""CLI query commands backed by the running AgentGraph server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, NoReturn, cast
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from agentgraph.config import get_settings
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
GET_TIMEOUT = httpx.Timeout(10, connect=0.5)
|
|
16
|
+
POST_TIMEOUT = httpx.Timeout(30, connect=0.5)
|
|
17
|
+
FETCH_TIMEOUT = httpx.Timeout(15 * 60, connect=0.5)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _server_base() -> str:
|
|
21
|
+
s = get_settings()
|
|
22
|
+
return f"http://{s.server_host}:{s.server_port}/api/cli"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _server_unavailable(exc: Exception) -> NoReturn:
|
|
26
|
+
console.print(
|
|
27
|
+
f"[red]AgentGraph server is not available at {_server_base()}.[/red]\n"
|
|
28
|
+
"Start it with: [bold]agentgraph serve[/bold]",
|
|
29
|
+
)
|
|
30
|
+
raise SystemExit(1) from exc
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get(path: str, params: dict[str, Any] | None = None) -> Any:
|
|
34
|
+
"""GET from the server's CLI API and return parsed JSON."""
|
|
35
|
+
try:
|
|
36
|
+
resp = httpx.get(
|
|
37
|
+
f"{_server_base()}{path}",
|
|
38
|
+
params=params,
|
|
39
|
+
timeout=GET_TIMEOUT,
|
|
40
|
+
)
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
return resp.json()
|
|
43
|
+
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
44
|
+
_server_unavailable(exc)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _post(
|
|
48
|
+
path: str,
|
|
49
|
+
params: dict[str, Any] | None = None,
|
|
50
|
+
*,
|
|
51
|
+
timeout: httpx.Timeout = POST_TIMEOUT,
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""POST to the server's CLI API and return parsed JSON."""
|
|
54
|
+
try:
|
|
55
|
+
resp = httpx.post(
|
|
56
|
+
f"{_server_base()}{path}",
|
|
57
|
+
params=params,
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
return resp.json()
|
|
62
|
+
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
63
|
+
_server_unavailable(exc)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# search
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def cmd_search(
|
|
71
|
+
query: str,
|
|
72
|
+
entity_types: list[str],
|
|
73
|
+
limit: int,
|
|
74
|
+
min_score: float,
|
|
75
|
+
as_json: bool,
|
|
76
|
+
platform: str | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
params: dict[str, Any] = {"q": query, "limit": limit, "min_score": min_score}
|
|
79
|
+
if entity_types:
|
|
80
|
+
params["entity_type"] = entity_types
|
|
81
|
+
if platform:
|
|
82
|
+
params["platform"] = platform
|
|
83
|
+
|
|
84
|
+
results = _get("/search", params)
|
|
85
|
+
|
|
86
|
+
if as_json:
|
|
87
|
+
console.print_json(json.dumps(results, default=str))
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if not results:
|
|
91
|
+
console.print("[dim]No results.[/dim]")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
table = Table(title=f'Search: "{query}"', show_lines=True)
|
|
95
|
+
table.add_column("ID", style="dim", no_wrap=True, max_width=8)
|
|
96
|
+
table.add_column("Type")
|
|
97
|
+
table.add_column("Platform")
|
|
98
|
+
table.add_column("Title / Content", ratio=1)
|
|
99
|
+
table.add_column("Score", justify="right")
|
|
100
|
+
|
|
101
|
+
for r in results:
|
|
102
|
+
snippet = (r.get("title") or r.get("content") or "")[:120]
|
|
103
|
+
score = f"{r['score']:.4f}" if r.get("score") else "—"
|
|
104
|
+
table.add_row(str(r["id"])[:8], r["entity_type"], r["platform"], snippet, score)
|
|
105
|
+
|
|
106
|
+
console.print(table)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# get
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _is_stub(entity: dict[str, Any]) -> bool:
|
|
114
|
+
"""Return True if the entity has no meaningful content (stub / unfetched)."""
|
|
115
|
+
return not entity.get("title") and not entity.get("content")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _print_field(label: str, value: object) -> None:
|
|
119
|
+
console.print(f"\n[bold]{label}:[/bold] ", end="")
|
|
120
|
+
console.print(str(value), markup=False, highlight=False)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _print_entity(entity: dict[str, Any]) -> None:
|
|
124
|
+
"""Render an entity in the same detail format used by ``agentgraph get``."""
|
|
125
|
+
console.print(f"[bold]{entity['entity_type']}[/bold] — {entity['platform']}")
|
|
126
|
+
console.print(f"[dim]{entity['id']}[/dim]")
|
|
127
|
+
if entity.get("title"):
|
|
128
|
+
_print_field("Title", entity["title"])
|
|
129
|
+
if entity.get("content"):
|
|
130
|
+
console.print("\n[bold]Content:[/bold]")
|
|
131
|
+
console.print(str(entity["content"]), markup=False, highlight=False)
|
|
132
|
+
if entity.get("metadata"):
|
|
133
|
+
_print_field("Metadata", entity["metadata"])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_get(entity_id: str, as_json: bool, resolve: bool = False) -> None:
|
|
137
|
+
from agentgraph.graph.query import is_http_url
|
|
138
|
+
|
|
139
|
+
target_is_url = is_http_url(entity_id)
|
|
140
|
+
try:
|
|
141
|
+
entity = (
|
|
142
|
+
_get("/entity-by-url", {"url": entity_id})
|
|
143
|
+
if target_is_url
|
|
144
|
+
else _get(f"/entity/{entity_id}")
|
|
145
|
+
)
|
|
146
|
+
except httpx.HTTPStatusError as exc:
|
|
147
|
+
if exc.response.status_code != 404:
|
|
148
|
+
raise
|
|
149
|
+
entity = None
|
|
150
|
+
if entity is None:
|
|
151
|
+
console.print(f"[red]Entity {entity_id!r} not found.[/red]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if resolve and _is_stub(entity):
|
|
155
|
+
console.print("[dim]Stub entity — fetching from source…[/dim]")
|
|
156
|
+
_post("/fetch-entity", params={"entity_id": entity["id"]})
|
|
157
|
+
refreshed = _get(f"/entity/{entity['id']}")
|
|
158
|
+
if refreshed is not None:
|
|
159
|
+
entity = refreshed
|
|
160
|
+
|
|
161
|
+
if as_json:
|
|
162
|
+
console.print_json(json.dumps(entity, default=str))
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
_print_entity(entity)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# edges
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def cmd_edges(
|
|
173
|
+
entity_id: str, edge_type: str | None, direction: str, as_json: bool
|
|
174
|
+
) -> None:
|
|
175
|
+
params: dict[str, Any] = {"direction": direction}
|
|
176
|
+
if edge_type:
|
|
177
|
+
params["edge_type"] = edge_type
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
edges = _get(f"/edges/{entity_id}", params)
|
|
181
|
+
except httpx.HTTPStatusError as exc:
|
|
182
|
+
if exc.response.status_code != 404:
|
|
183
|
+
raise
|
|
184
|
+
console.print(f"[red]Entity {entity_id!r} not found.[/red]")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if as_json:
|
|
188
|
+
console.print_json(json.dumps(edges, default=str))
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if not edges:
|
|
192
|
+
console.print("[dim]No edges found.[/dim]")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
table = Table(title=f"Edges for {entity_id[:8]}…", show_lines=True)
|
|
196
|
+
table.add_column("Type")
|
|
197
|
+
table.add_column("Direction")
|
|
198
|
+
table.add_column("Other end")
|
|
199
|
+
table.add_column("Platform")
|
|
200
|
+
|
|
201
|
+
for e in edges:
|
|
202
|
+
if e.get("source_entity_id") == entity_id:
|
|
203
|
+
direction_label = "→ out"
|
|
204
|
+
other = e.get("target_ref") or e.get("target_entity_id") or "?"
|
|
205
|
+
else:
|
|
206
|
+
direction_label = "← in"
|
|
207
|
+
other = e.get("source_ref") or e.get("source_entity_id") or "?"
|
|
208
|
+
table.add_row(e["edge_type"], direction_label, str(other), e.get("platform") or "")
|
|
209
|
+
|
|
210
|
+
console.print(table)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# traverse
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def cmd_traverse(entity_id: str, max_depth: int, as_json: bool, resolve: bool = False) -> None:
|
|
218
|
+
try:
|
|
219
|
+
result = _get(f"/traverse/{entity_id}", {"depth": max_depth})
|
|
220
|
+
except httpx.HTTPStatusError as exc:
|
|
221
|
+
if exc.response.status_code != 404:
|
|
222
|
+
raise
|
|
223
|
+
console.print(f"[red]Entity {entity_id!r} not found.[/red]")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
if resolve and result:
|
|
227
|
+
stubs = [n for n in result.get("nodes", []) if _is_stub(n)]
|
|
228
|
+
if stubs:
|
|
229
|
+
console.print(f"[dim]Resolving {len(stubs)} stub node(s)…[/dim]")
|
|
230
|
+
for stub in stubs:
|
|
231
|
+
_post("/fetch-entity", params={"entity_id": stub["id"]})
|
|
232
|
+
refreshed = _get(f"/traverse/{entity_id}", {"depth": max_depth})
|
|
233
|
+
if refreshed is not None:
|
|
234
|
+
result = refreshed
|
|
235
|
+
|
|
236
|
+
if as_json:
|
|
237
|
+
console.print_json(json.dumps(result, default=str))
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
nodes = result.get("nodes", [])
|
|
241
|
+
edges = result.get("edges", [])
|
|
242
|
+
console.print(f"[bold]Traversal:[/bold] {len(nodes)} nodes, {len(edges)} edges")
|
|
243
|
+
|
|
244
|
+
table = Table(title="Nodes", show_lines=True)
|
|
245
|
+
table.add_column("ID", style="dim", max_width=8)
|
|
246
|
+
table.add_column("Type")
|
|
247
|
+
table.add_column("Platform")
|
|
248
|
+
table.add_column("Title")
|
|
249
|
+
|
|
250
|
+
for n in nodes:
|
|
251
|
+
stub_marker = " [dim](stub)[/dim]" if _is_stub(n) else ""
|
|
252
|
+
table.add_row(str(n["id"])[:8], n["entity_type"], n["platform"], (n.get("title") or "") + stub_marker)
|
|
253
|
+
|
|
254
|
+
console.print(table)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# query
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def cmd_fetch(platform: str, resource_id: str, as_json: bool) -> None:
|
|
262
|
+
try:
|
|
263
|
+
result = _post(
|
|
264
|
+
"/fetch",
|
|
265
|
+
params={"platform": platform, "resource_id": resource_id},
|
|
266
|
+
timeout=FETCH_TIMEOUT,
|
|
267
|
+
)
|
|
268
|
+
except httpx.HTTPStatusError as exc:
|
|
269
|
+
try:
|
|
270
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
271
|
+
except Exception:
|
|
272
|
+
detail = str(exc)
|
|
273
|
+
console.print(f"[red]{detail}[/red]")
|
|
274
|
+
return
|
|
275
|
+
if as_json:
|
|
276
|
+
console.print_json(json.dumps(result, default=str))
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
console.print(
|
|
280
|
+
f"[green]Fetched:[/green] {result['entities']} entities, "
|
|
281
|
+
f"{result['persons']} persons, {result['edges']} edges"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def cmd_fetch_entity(entity_id: str, as_json: bool) -> None:
|
|
286
|
+
try:
|
|
287
|
+
result = _post(
|
|
288
|
+
"/fetch-entity",
|
|
289
|
+
params={"entity_id": entity_id},
|
|
290
|
+
timeout=FETCH_TIMEOUT,
|
|
291
|
+
)
|
|
292
|
+
except httpx.HTTPStatusError as exc:
|
|
293
|
+
try:
|
|
294
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
295
|
+
except Exception:
|
|
296
|
+
detail = str(exc)
|
|
297
|
+
console.print(f"[red]{detail}[/red]")
|
|
298
|
+
return
|
|
299
|
+
if as_json:
|
|
300
|
+
console.print_json(json.dumps(result, default=str))
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
console.print(
|
|
304
|
+
f"[green]Fetched:[/green] {result['entities']} entities, "
|
|
305
|
+
f"{result['persons']} persons, {result['edges']} edges"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def cmd_download(entity_id: str, output_path: str | None, as_json: bool) -> None:
|
|
310
|
+
try:
|
|
311
|
+
params: dict[str, Any] = {"entity_id": entity_id}
|
|
312
|
+
if output_path is not None:
|
|
313
|
+
params["output_path"] = output_path
|
|
314
|
+
result = _post("/download", params=params)
|
|
315
|
+
except httpx.HTTPStatusError as exc:
|
|
316
|
+
try:
|
|
317
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
318
|
+
except Exception:
|
|
319
|
+
detail = str(exc)
|
|
320
|
+
console.print(f"[red]{detail}[/red]")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
if as_json:
|
|
324
|
+
console.print_json(json.dumps(result, default=str))
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
console.print(
|
|
328
|
+
f"[green]Downloaded:[/green] {result['filename']} "
|
|
329
|
+
f"({result['bytes']} bytes) → {result['path']}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def cmd_bookmark(target: str, bookmarked: bool, as_json: bool) -> None:
|
|
334
|
+
try:
|
|
335
|
+
result = _post("/bookmark", params={"target": target, "bookmarked": bookmarked})
|
|
336
|
+
except httpx.HTTPStatusError as exc:
|
|
337
|
+
try:
|
|
338
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
339
|
+
except Exception:
|
|
340
|
+
detail = str(exc)
|
|
341
|
+
console.print(f"[red]{detail}[/red]")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
if as_json:
|
|
345
|
+
console.print_json(json.dumps(result, default=str))
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
label = result.get("title") or result.get("platform_entity_id") or result["id"]
|
|
349
|
+
action = "Bookmarked" if bookmarked else "Bookmark removed"
|
|
350
|
+
console.print(f"[green]{action}:[/green] {label} [{result['id'][:8]}]")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def cmd_delete(target: str, as_json: bool) -> None:
|
|
354
|
+
try:
|
|
355
|
+
result = _post("/delete", params={"target": target})
|
|
356
|
+
except httpx.HTTPStatusError as exc:
|
|
357
|
+
try:
|
|
358
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
359
|
+
except Exception:
|
|
360
|
+
detail = str(exc)
|
|
361
|
+
console.print(f"[red]{detail}[/red]")
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
if as_json:
|
|
365
|
+
console.print_json(json.dumps(result, default=str))
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
entity = result["entity"]
|
|
369
|
+
label = entity.get("title") or entity.get("platform_entity_id") or entity["id"]
|
|
370
|
+
console.print(f"[green]Deleted:[/green] {label} [{entity['id'][:8]}]")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def cmd_unify_persons(
|
|
374
|
+
primary_entity_id: str,
|
|
375
|
+
duplicate_entity_ids: list[str],
|
|
376
|
+
as_json: bool,
|
|
377
|
+
) -> None:
|
|
378
|
+
try:
|
|
379
|
+
result = _post(
|
|
380
|
+
"/unify-persons",
|
|
381
|
+
params={"primary": primary_entity_id, "duplicate": duplicate_entity_ids},
|
|
382
|
+
)
|
|
383
|
+
except httpx.HTTPStatusError as exc:
|
|
384
|
+
try:
|
|
385
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
386
|
+
except Exception:
|
|
387
|
+
detail = str(exc)
|
|
388
|
+
console.print(f"[red]{detail}[/red]")
|
|
389
|
+
return
|
|
390
|
+
if as_json:
|
|
391
|
+
console.print_json(json.dumps(result, default=str))
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
console.print(
|
|
395
|
+
f"[green]Unified:[/green] {result['merged_count']} duplicate person(s). "
|
|
396
|
+
"Canonical person:"
|
|
397
|
+
)
|
|
398
|
+
_print_entity(result["primary"])
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def cmd_query(
|
|
402
|
+
entity_type: str,
|
|
403
|
+
filters: dict[str, str],
|
|
404
|
+
limit: int,
|
|
405
|
+
order_by: str,
|
|
406
|
+
since: str | None,
|
|
407
|
+
authored_by_me: bool,
|
|
408
|
+
as_json: bool,
|
|
409
|
+
has_attachments: bool = False,
|
|
410
|
+
) -> None:
|
|
411
|
+
params: dict[str, Any] = {
|
|
412
|
+
"entity_type": entity_type,
|
|
413
|
+
"limit": limit,
|
|
414
|
+
"order_by": order_by,
|
|
415
|
+
"mine": authored_by_me,
|
|
416
|
+
}
|
|
417
|
+
if since:
|
|
418
|
+
params["since"] = since
|
|
419
|
+
if filters:
|
|
420
|
+
params["filter"] = [f"{k}={v}" for k, v in filters.items()]
|
|
421
|
+
if has_attachments:
|
|
422
|
+
params["has_attachments"] = True
|
|
423
|
+
|
|
424
|
+
results = _get("/query", params)
|
|
425
|
+
|
|
426
|
+
if as_json:
|
|
427
|
+
console.print_json(json.dumps(results, default=str))
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
if not results:
|
|
431
|
+
console.print("[dim]No results.[/dim]")
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
table = Table(title=f"Query: {entity_type}", show_lines=True)
|
|
435
|
+
table.add_column("ID", style="dim", max_width=8)
|
|
436
|
+
table.add_column("Platform")
|
|
437
|
+
table.add_column("Title / Content", ratio=1)
|
|
438
|
+
|
|
439
|
+
for r in results:
|
|
440
|
+
snippet = (r.get("title") or r.get("content") or "")[:120]
|
|
441
|
+
table.add_row(str(r["id"])[:8], r["platform"], snippet)
|
|
442
|
+
|
|
443
|
+
console.print(table)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def cmd_poll(source: str | None, as_json: bool) -> None:
|
|
447
|
+
params: dict[str, Any] = {}
|
|
448
|
+
if source:
|
|
449
|
+
params["source"] = source
|
|
450
|
+
try:
|
|
451
|
+
result = _post("/poll", params=params)
|
|
452
|
+
except httpx.HTTPStatusError as exc:
|
|
453
|
+
try:
|
|
454
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
455
|
+
except Exception:
|
|
456
|
+
detail = str(exc)
|
|
457
|
+
console.print(f"[red]{detail}[/red]")
|
|
458
|
+
return
|
|
459
|
+
if as_json:
|
|
460
|
+
console.print_json(json.dumps(result, default=str))
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
polled: list[str] = result.get("polled", [])
|
|
464
|
+
already_running: list[str] = result.get("already_running", [])
|
|
465
|
+
skipped: list[dict[str, str | None]] = result.get("skipped", [])
|
|
466
|
+
if polled:
|
|
467
|
+
console.print(f"[green]Queued poll:[/green] {', '.join(polled)}")
|
|
468
|
+
if already_running:
|
|
469
|
+
console.print(f"[yellow]Already running:[/yellow] {', '.join(already_running)}")
|
|
470
|
+
for item in skipped:
|
|
471
|
+
source_name = item.get("source") or "unknown"
|
|
472
|
+
reason = item.get("reason") or "not available"
|
|
473
|
+
console.print(f"[yellow]Skipped:[/yellow] {source_name} — {reason}")
|
|
474
|
+
if not polled and not already_running and not skipped:
|
|
475
|
+
console.print("[dim]No connectors polled (none matched or none have poll_interval set).[/dim]")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def queue_connector_poll(source: str) -> dict[str, str | None]:
|
|
479
|
+
"""Queue one connector through the server and normalize its schedule result."""
|
|
480
|
+
try:
|
|
481
|
+
result = cast(dict[str, Any], _post("/poll", params={"source": source}))
|
|
482
|
+
except httpx.HTTPStatusError as exc:
|
|
483
|
+
try:
|
|
484
|
+
detail = str(exc.response.json().get("detail", str(exc)))
|
|
485
|
+
except Exception:
|
|
486
|
+
detail = str(exc)
|
|
487
|
+
raise ValueError(detail) from exc
|
|
488
|
+
|
|
489
|
+
if source in cast(list[str], result.get("polled", [])):
|
|
490
|
+
return {"source": source, "status": "queued", "reason": None}
|
|
491
|
+
if source in cast(list[str], result.get("already_running", [])):
|
|
492
|
+
return {"source": source, "status": "already_running", "reason": None}
|
|
493
|
+
|
|
494
|
+
skipped = cast(list[dict[str, str | None]], result.get("skipped", []))
|
|
495
|
+
reason = next(
|
|
496
|
+
(item.get("reason") for item in skipped if item.get("source") == source),
|
|
497
|
+
"poll was not queued",
|
|
498
|
+
)
|
|
499
|
+
return {"source": source, "status": "skipped", "reason": reason}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def cmd_ingest(source: str, as_json: bool) -> None:
|
|
503
|
+
try:
|
|
504
|
+
result = _post("/ingest", params={"source": source})
|
|
505
|
+
except httpx.HTTPStatusError as exc:
|
|
506
|
+
try:
|
|
507
|
+
detail = exc.response.json().get("detail", str(exc))
|
|
508
|
+
except Exception:
|
|
509
|
+
detail = str(exc)
|
|
510
|
+
console.print(f"[red]{detail}[/red]")
|
|
511
|
+
return
|
|
512
|
+
if as_json:
|
|
513
|
+
console.print_json(json.dumps(result, default=str))
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
console.print(
|
|
517
|
+
f"[green]Ingest started[/green] for [bold]{result.get('source')}[/bold] — "
|
|
518
|
+
"progress in server logs (agentgraph serve)"
|
|
519
|
+
)
|
agentgraph/config.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Application configuration loaded from environment variables and the config directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _default_config_dir() -> Path:
|
|
13
|
+
raw = os.environ.get("AGENTGRAPH_CONFIG_DIR")
|
|
14
|
+
return Path(raw).expanduser() if raw else Path.home() / ".agentgraph"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CONFIG_DIR = _default_config_dir()
|
|
18
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
19
|
+
CONFIG_YAML_FILE = CONFIG_DIR / "config.yaml"
|
|
20
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
|
|
21
|
+
DEFAULT_SQLITE_PATH = str(CONFIG_DIR / "agentgraph.db")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Settings(BaseSettings):
|
|
25
|
+
model_config = SettingsConfigDict(
|
|
26
|
+
env_prefix="AGENTGRAPH_",
|
|
27
|
+
# User-level config is loaded first;
|
|
28
|
+
# project-local .env takes precedence.
|
|
29
|
+
env_file=[str(CONFIG_DIR / ".env"), ".env"],
|
|
30
|
+
env_file_encoding="utf-8",
|
|
31
|
+
extra="ignore",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Backend selection
|
|
35
|
+
backend: str = Field(
|
|
36
|
+
default="sqlite",
|
|
37
|
+
description="Persistence backend: 'sqlite' | any installed plugin",
|
|
38
|
+
)
|
|
39
|
+
backend_sqlite_path: str = Field(
|
|
40
|
+
default=DEFAULT_SQLITE_PATH,
|
|
41
|
+
description="Path to SQLite database file (only used when backend='sqlite')",
|
|
42
|
+
)
|
|
43
|
+
backend_sqlite_vector_mode: str = Field(
|
|
44
|
+
default="sqlite-vec",
|
|
45
|
+
description="SQLite vector search mode: 'sqlite-vec' | 'numpy' | 'bm25-only'",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Server
|
|
49
|
+
server_host: str = Field(default="127.0.0.1")
|
|
50
|
+
server_port: int = Field(default=8765)
|
|
51
|
+
|
|
52
|
+
# Dwell detection
|
|
53
|
+
dwell_threshold_seconds: int = Field(
|
|
54
|
+
default=3,
|
|
55
|
+
description="Seconds a focus event must persist without a blur before triggering a fetch",
|
|
56
|
+
)
|
|
57
|
+
dwell_poll_interval_seconds: float = Field(
|
|
58
|
+
default=1.0,
|
|
59
|
+
description="How often the dwell evaluator scans for mature focus events",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Knowledge graph
|
|
63
|
+
retention_days: int = Field(
|
|
64
|
+
default=90,
|
|
65
|
+
description="Days since last_accessed before an entity is garbage collected",
|
|
66
|
+
)
|
|
67
|
+
embedding_model: str = Field(
|
|
68
|
+
default="BAAI/bge-small-en-v1.5",
|
|
69
|
+
description="FastEmbed model name for content embeddings",
|
|
70
|
+
)
|
|
71
|
+
embedding_dimensions: int = Field(default=384)
|
|
72
|
+
|
|
73
|
+
# Connectors
|
|
74
|
+
slack_workspace_id: str | None = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
description="Slack workspace ID (e.g. T01ABC123) to observe; others are ignored",
|
|
77
|
+
)
|
|
78
|
+
# Logging
|
|
79
|
+
log_level: str = Field(default="INFO")
|
|
80
|
+
log_file: Path = Field(default=Path("/tmp/agentgraph.log"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_settings: Settings | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_settings() -> Settings:
|
|
87
|
+
global _settings
|
|
88
|
+
if _settings is None:
|
|
89
|
+
_settings = Settings()
|
|
90
|
+
return _settings
|
|
File without changes
|