agent-kg 0.7.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.
- agent_kg/__init__.py +70 -0
- agent_kg/app.py +543 -0
- agent_kg/assemble.py +132 -0
- agent_kg/cli/__init__.py +4 -0
- agent_kg/cli/main.py +1042 -0
- agent_kg/consolidate.py +93 -0
- agent_kg/graph.py +349 -0
- agent_kg/hooks/agent_kg_precompact_hook.sh +53 -0
- agent_kg/hooks/agent_kg_stop_hook.sh +97 -0
- agent_kg/hooks/agent_kg_user_prompt_hook.sh +39 -0
- agent_kg/index.py +135 -0
- agent_kg/ingest.py +361 -0
- agent_kg/kg.py +13 -0
- agent_kg/mcp/__init__.py +4 -0
- agent_kg/mcp/server.py +349 -0
- agent_kg/nlp/__init__.py +18 -0
- agent_kg/nlp/entities.py +190 -0
- agent_kg/nlp/intent.py +208 -0
- agent_kg/nlp/preferences.py +142 -0
- agent_kg/nlp/topics.py +152 -0
- agent_kg/onboard.py +259 -0
- agent_kg/prune.py +288 -0
- agent_kg/query.py +103 -0
- agent_kg/schema.py +247 -0
- agent_kg/session.py +108 -0
- agent_kg/snapshots.py +123 -0
- agent_kg/store.py +597 -0
- agent_kg/summarize.py +169 -0
- agent_kg/user_profile.py +493 -0
- agent_kg/viz.py +413 -0
- agent_kg-0.7.0.dist-info/METADATA +377 -0
- agent_kg-0.7.0.dist-info/RECORD +35 -0
- agent_kg-0.7.0.dist-info/WHEEL +4 -0
- agent_kg-0.7.0.dist-info/entry_points.txt +18 -0
- agent_kg-0.7.0.dist-info/licenses/LICENSE +94 -0
agent_kg/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright (c) 2026 Eric G. Suchanek, PhD. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Elastic-2.0
|
|
3
|
+
|
|
4
|
+
"""agent_kg — Conversational memory as a live, queryable knowledge graph.
|
|
5
|
+
|
|
6
|
+
AgentKG gives AI agents persistent, semantically searchable memory that
|
|
7
|
+
survives across sessions and defeats context rot via KG Context Pruning.
|
|
8
|
+
|
|
9
|
+
Quick start::
|
|
10
|
+
|
|
11
|
+
from agent_kg import AgentKG
|
|
12
|
+
|
|
13
|
+
kg = AgentKG(repo_path="/path/to/repo", person_id="alice")
|
|
14
|
+
kg.ingest("We decided to use OAuth for authentication.", role="user")
|
|
15
|
+
kg.ingest("Understood. I'll use OAuth.", role="assistant")
|
|
16
|
+
|
|
17
|
+
ctx = kg.assemble_context("authentication strategy", budget=2000)
|
|
18
|
+
print(ctx)
|
|
19
|
+
|
|
20
|
+
kg.close()
|
|
21
|
+
|
|
22
|
+
Architecture:
|
|
23
|
+
- Conversation tree: Turn, Topic, Intent, Entity, Task, Summary nodes
|
|
24
|
+
- UserProfile tree: Preference, Style, Interest, Expertise, Commitment nodes
|
|
25
|
+
- Storage: SQLite (topology) + LanceDB (embeddings)
|
|
26
|
+
- KG Context Pruning: LLM-compresses old turns into Summary nodes
|
|
27
|
+
- MCP server: Exposes all tools to Claude Code and other MCP clients
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from agent_kg import assemble, consolidate, prune, query, snapshots
|
|
31
|
+
from agent_kg.graph import AgentKG
|
|
32
|
+
from agent_kg.index import ConversationIndex
|
|
33
|
+
from agent_kg.schema import (
|
|
34
|
+
Edge,
|
|
35
|
+
EdgeRelation,
|
|
36
|
+
IntentCategory,
|
|
37
|
+
Node,
|
|
38
|
+
NodeKind,
|
|
39
|
+
PruneReport,
|
|
40
|
+
TaskStatus,
|
|
41
|
+
)
|
|
42
|
+
from agent_kg.session import Session
|
|
43
|
+
from agent_kg.store import AgentKGStore
|
|
44
|
+
from agent_kg.summarize import Summarizer, SummarizerConfig
|
|
45
|
+
from agent_kg.user_profile import UserProfileStore
|
|
46
|
+
|
|
47
|
+
__version__ = "0.7.0"
|
|
48
|
+
__author__ = "Eric G. Suchanek, PhD"
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"AgentKG",
|
|
52
|
+
"AgentKGStore",
|
|
53
|
+
"ConversationIndex",
|
|
54
|
+
"UserProfileStore",
|
|
55
|
+
"Session",
|
|
56
|
+
"Summarizer",
|
|
57
|
+
"SummarizerConfig",
|
|
58
|
+
"Node",
|
|
59
|
+
"Edge",
|
|
60
|
+
"NodeKind",
|
|
61
|
+
"EdgeRelation",
|
|
62
|
+
"TaskStatus",
|
|
63
|
+
"IntentCategory",
|
|
64
|
+
"PruneReport",
|
|
65
|
+
"assemble",
|
|
66
|
+
"consolidate",
|
|
67
|
+
"prune",
|
|
68
|
+
"query",
|
|
69
|
+
"snapshots",
|
|
70
|
+
]
|
agent_kg/app.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# Copyright (c) 2026 Eric G. Suchanek, PhD. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Elastic-2.0
|
|
3
|
+
|
|
4
|
+
"""app.py — AgentKG Streamlit Explorer
|
|
5
|
+
|
|
6
|
+
Interactive visualizer for the AgentKG conversation and UserProfile trees.
|
|
7
|
+
|
|
8
|
+
Tabs:
|
|
9
|
+
Agent Graph — pyvis interactive graph of the conversation tree
|
|
10
|
+
Profile Graph — pyvis interactive graph of the UserProfile tree
|
|
11
|
+
Query — hybrid semantic search + context assembly
|
|
12
|
+
Stats — node/edge counts, session list, profile breakdown
|
|
13
|
+
|
|
14
|
+
Run with:
|
|
15
|
+
agentkg viz --serve
|
|
16
|
+
# or directly:
|
|
17
|
+
streamlit run src/agent_kg/app.py -- --repo . --person egs
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import getpass
|
|
24
|
+
import sqlite3
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import streamlit as st
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Page config (must be first Streamlit call)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
st.set_page_config(
|
|
35
|
+
page_title="AgentKG Explorer",
|
|
36
|
+
page_icon="🧠",
|
|
37
|
+
layout="wide",
|
|
38
|
+
initial_sidebar_state="expanded",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
st.markdown(
|
|
42
|
+
"""
|
|
43
|
+
<style>
|
|
44
|
+
.stTabs [data-baseweb="tab-list"] { gap: 12px; }
|
|
45
|
+
.stTabs [data-baseweb="tab"] { font-size: 1rem; padding: 6px 18px; }
|
|
46
|
+
.hit-card {
|
|
47
|
+
background: #1e1e2e;
|
|
48
|
+
border-left: 4px solid #4A90D9;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
padding: 10px 14px;
|
|
51
|
+
margin-bottom: 8px;
|
|
52
|
+
font-family: monospace;
|
|
53
|
+
font-size: 0.85rem;
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
56
|
+
""",
|
|
57
|
+
unsafe_allow_html=True,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Kind colours (matches viz.py)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
_KIND_COLOR: dict[str, str] = {
|
|
65
|
+
"turn": "#4A90D9",
|
|
66
|
+
"topic": "#27AE60",
|
|
67
|
+
"entity": "#E67E22",
|
|
68
|
+
"intent": "#8E44AD",
|
|
69
|
+
"task": "#E74C3C",
|
|
70
|
+
"summary": "#16A085",
|
|
71
|
+
"preference": "#F39C12",
|
|
72
|
+
"commitment": "#C0392B",
|
|
73
|
+
"expertise": "#2980B9",
|
|
74
|
+
"interest": "#27AE60",
|
|
75
|
+
"style": "#8E44AD",
|
|
76
|
+
"context": "#7F8C8D",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Argument parsing — Streamlit passes args after "--"
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_args() -> argparse.Namespace:
|
|
85
|
+
p = argparse.ArgumentParser(add_help=False)
|
|
86
|
+
p.add_argument("--repo", default=".")
|
|
87
|
+
p.add_argument("--person", default=getpass.getuser())
|
|
88
|
+
args, _ = p.parse_known_args(sys.argv[1:])
|
|
89
|
+
return args
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_args = _parse_args()
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Session-state initialisation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _init_state() -> None:
|
|
100
|
+
defaults = {
|
|
101
|
+
"query_results": None,
|
|
102
|
+
"query_context": None,
|
|
103
|
+
"last_query": "",
|
|
104
|
+
}
|
|
105
|
+
for k, v in defaults.items():
|
|
106
|
+
if k not in st.session_state:
|
|
107
|
+
st.session_state[k] = v
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_init_state()
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# Sidebar
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
with st.sidebar:
|
|
117
|
+
if Path("docs/logo.png").exists():
|
|
118
|
+
st.image("docs/logo.png", use_container_width=True)
|
|
119
|
+
st.title("AgentKG Explorer")
|
|
120
|
+
st.caption("Conversational memory as a knowledge graph")
|
|
121
|
+
st.divider()
|
|
122
|
+
|
|
123
|
+
repo = st.text_input("Repo root", value=_args.repo)
|
|
124
|
+
person = st.text_input("Person ID", value=_args.person)
|
|
125
|
+
|
|
126
|
+
agent_db = Path(repo) / ".agentkg" / "graph.sqlite"
|
|
127
|
+
profile_db = Path.home() / ".kgrag" / "profiles" / person / "userprofile.sqlite"
|
|
128
|
+
|
|
129
|
+
st.divider()
|
|
130
|
+
st.caption(f"**Agent DB:** `{agent_db}`")
|
|
131
|
+
st.caption(f" exists: {'✓' if agent_db.exists() else '✗'}")
|
|
132
|
+
st.caption(f"**Profile DB:** `{profile_db}`")
|
|
133
|
+
st.caption(f" exists: {'✓' if profile_db.exists() else '✗'}")
|
|
134
|
+
|
|
135
|
+
st.divider()
|
|
136
|
+
st.subheader("Query settings")
|
|
137
|
+
k = st.slider("Top-K results", 1, 30, 8)
|
|
138
|
+
budget = st.slider("Context budget (tokens)", 500, 8000, 4000, step=500)
|
|
139
|
+
include_profile = st.checkbox(
|
|
140
|
+
"Include profile nodes",
|
|
141
|
+
value=False,
|
|
142
|
+
help="Also search UserProfile for preferences, commitments, style",
|
|
143
|
+
)
|
|
144
|
+
kind_filter = st.selectbox(
|
|
145
|
+
"Kind filter",
|
|
146
|
+
["(all)", "turn", "topic", "entity", "intent", "task", "summary"],
|
|
147
|
+
index=0,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
st.divider()
|
|
151
|
+
physics = st.toggle("Physics simulation", value=True)
|
|
152
|
+
max_nodes = st.slider("Max nodes (agent graph)", 50, 500, 200, step=50)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# Data loaders
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@st.cache_data(show_spinner="Loading agent graph…")
|
|
161
|
+
def _load_agent(db_path: str, _max_nodes: int):
|
|
162
|
+
p = Path(db_path)
|
|
163
|
+
if not p.exists():
|
|
164
|
+
return [], [], []
|
|
165
|
+
db = sqlite3.connect(str(p))
|
|
166
|
+
db.row_factory = sqlite3.Row
|
|
167
|
+
nodes = [
|
|
168
|
+
dict(r)
|
|
169
|
+
for r in db.execute(
|
|
170
|
+
"SELECT * FROM nodes ORDER BY created_at DESC LIMIT ?", (_max_nodes,)
|
|
171
|
+
).fetchall()
|
|
172
|
+
]
|
|
173
|
+
node_ids = {n["id"] for n in nodes}
|
|
174
|
+
edges = [
|
|
175
|
+
dict(r)
|
|
176
|
+
for r in db.execute("SELECT * FROM edges").fetchall()
|
|
177
|
+
if r["source_id"] in node_ids and r["target_id"] in node_ids
|
|
178
|
+
]
|
|
179
|
+
sessions = [
|
|
180
|
+
dict(r) for r in db.execute("SELECT * FROM sessions ORDER BY start_time DESC").fetchall()
|
|
181
|
+
]
|
|
182
|
+
db.close()
|
|
183
|
+
return nodes, edges, sessions
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@st.cache_data(show_spinner="Loading user profile…")
|
|
187
|
+
def _load_profile(db_path: str):
|
|
188
|
+
p = Path(db_path)
|
|
189
|
+
if not p.exists():
|
|
190
|
+
return []
|
|
191
|
+
db = sqlite3.connect(str(p))
|
|
192
|
+
db.row_factory = sqlite3.Row
|
|
193
|
+
rows = [
|
|
194
|
+
dict(r)
|
|
195
|
+
for r in db.execute("SELECT * FROM profile_nodes ORDER BY kind, confidence DESC").fetchall()
|
|
196
|
+
]
|
|
197
|
+
db.close()
|
|
198
|
+
return rows
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# pyvis HTML builders
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _agent_html(nodes, edges, physics_on: bool) -> str:
|
|
207
|
+
from agent_kg.viz import (
|
|
208
|
+
_KIND_COLOR as VIZ_COLOR,
|
|
209
|
+
)
|
|
210
|
+
from agent_kg.viz import (
|
|
211
|
+
_KIND_SHAPE,
|
|
212
|
+
_REL_COLOR,
|
|
213
|
+
_node_tooltip,
|
|
214
|
+
_require_pyvis,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
_require_pyvis()
|
|
218
|
+
import tempfile
|
|
219
|
+
|
|
220
|
+
from pyvis.network import Network
|
|
221
|
+
|
|
222
|
+
net = Network(height="650px", width="100%", bgcolor="#0d1117", font_color="#e0e0e0")
|
|
223
|
+
net.set_options(
|
|
224
|
+
'{"physics":{"enabled":' + str(physics_on).lower() + ',"stabilization":{"iterations":150}},'
|
|
225
|
+
'"interaction":{"hover":true,"tooltipDelay":100}}'
|
|
226
|
+
)
|
|
227
|
+
for n in nodes:
|
|
228
|
+
kind = n["kind"]
|
|
229
|
+
label = (n.get("label") or n.get("text") or kind)[:30]
|
|
230
|
+
net.add_node(
|
|
231
|
+
n["id"],
|
|
232
|
+
label=label,
|
|
233
|
+
color=VIZ_COLOR.get(kind, "#888"),
|
|
234
|
+
shape=_KIND_SHAPE.get(kind, "dot"),
|
|
235
|
+
size=20 if kind == "turn" else 10,
|
|
236
|
+
title=_node_tooltip(kind, label, n.get("text") or "", n.get("role") or ""),
|
|
237
|
+
)
|
|
238
|
+
for e in edges:
|
|
239
|
+
net.add_edge(
|
|
240
|
+
e["source_id"],
|
|
241
|
+
e["target_id"],
|
|
242
|
+
title=e["relation"],
|
|
243
|
+
color=_REL_COLOR.get(e["relation"], "#555"),
|
|
244
|
+
arrows="to",
|
|
245
|
+
)
|
|
246
|
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
|
247
|
+
tmp = Path(f.name)
|
|
248
|
+
net.save_graph(str(tmp))
|
|
249
|
+
html = tmp.read_text(encoding="utf-8")
|
|
250
|
+
tmp.unlink(missing_ok=True)
|
|
251
|
+
return html
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _profile_html(rows: list[dict]) -> str:
|
|
255
|
+
from itertools import groupby
|
|
256
|
+
|
|
257
|
+
from agent_kg.viz import (
|
|
258
|
+
_KIND_COLOR as VIZ_COLOR,
|
|
259
|
+
)
|
|
260
|
+
from agent_kg.viz import (
|
|
261
|
+
_KIND_SHAPE,
|
|
262
|
+
_node_tooltip,
|
|
263
|
+
_require_pyvis,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
_require_pyvis()
|
|
267
|
+
import tempfile
|
|
268
|
+
|
|
269
|
+
from pyvis.network import Network
|
|
270
|
+
|
|
271
|
+
net = Network(height="600px", width="100%", bgcolor="#0d1117", font_color="#e0e0e0")
|
|
272
|
+
net.set_options(
|
|
273
|
+
'{"physics":{"enabled":true,"stabilization":{"iterations":100}},'
|
|
274
|
+
'"interaction":{"hover":true,"tooltipDelay":100}}'
|
|
275
|
+
)
|
|
276
|
+
net.add_node(
|
|
277
|
+
"__profile__",
|
|
278
|
+
label="UserProfile",
|
|
279
|
+
color="#ffffff",
|
|
280
|
+
shape="box",
|
|
281
|
+
size=30,
|
|
282
|
+
title="<b>UserProfile root</b>",
|
|
283
|
+
)
|
|
284
|
+
for kind, group in groupby(rows, key=lambda r: r["kind"]):
|
|
285
|
+
color = VIZ_COLOR.get(kind, "#888")
|
|
286
|
+
hub = f"__kind__{kind}"
|
|
287
|
+
net.add_node(hub, label=kind, color=color, shape="box", size=18)
|
|
288
|
+
net.add_edge("__profile__", hub, color="#444", arrows="to")
|
|
289
|
+
for node in group:
|
|
290
|
+
conf = int((node["confidence"] or 1.0) * 100)
|
|
291
|
+
label = (node["label"] or "")[:35]
|
|
292
|
+
net.add_node(
|
|
293
|
+
node["id"],
|
|
294
|
+
label=label,
|
|
295
|
+
color=color,
|
|
296
|
+
shape=_KIND_SHAPE.get(kind, "dot"),
|
|
297
|
+
size=10,
|
|
298
|
+
title=_node_tooltip(kind, label, node.get("text") or "", f"confidence: {conf}%"),
|
|
299
|
+
)
|
|
300
|
+
net.add_edge(hub, node["id"], color=color + "88", arrows="to")
|
|
301
|
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
|
302
|
+
tmp = Path(f.name)
|
|
303
|
+
net.save_graph(str(tmp))
|
|
304
|
+
html = tmp.read_text(encoding="utf-8")
|
|
305
|
+
tmp.unlink(missing_ok=True)
|
|
306
|
+
return html
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Query helpers
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _kind_badge(kind: str) -> str:
|
|
315
|
+
color = _KIND_COLOR.get(kind, "#888")
|
|
316
|
+
return (
|
|
317
|
+
f'<span style="background:{color};color:#fff;border-radius:4px;'
|
|
318
|
+
f'padding:2px 8px;font-size:11px;font-weight:bold;font-family:monospace;">'
|
|
319
|
+
f"{kind}</span>"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _render_hit(hit: dict, idx: int) -> None:
|
|
324
|
+
kind = hit.get("kind", "")
|
|
325
|
+
score = hit.get("score", 0.0)
|
|
326
|
+
text = hit.get("text") or hit.get("label") or ""
|
|
327
|
+
label = hit.get("label") or ""
|
|
328
|
+
role = hit.get("role") or hit.get("source") or ""
|
|
329
|
+
source_tag = f" · <i>{role}</i>" if role else ""
|
|
330
|
+
preview = text[:300].replace("<", "<").replace(">", ">")
|
|
331
|
+
if len(text) > 300:
|
|
332
|
+
preview += "…"
|
|
333
|
+
color = _KIND_COLOR.get(kind, "#888")
|
|
334
|
+
|
|
335
|
+
st.markdown(
|
|
336
|
+
f"""
|
|
337
|
+
<div style="background:#1e1e2e;border-left:4px solid {color};
|
|
338
|
+
border-radius:6px;padding:10px 14px;margin-bottom:8px;">
|
|
339
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
|
|
340
|
+
<span>{_kind_badge(kind)}{source_tag}</span>
|
|
341
|
+
<span style="font-family:monospace;font-size:12px;color:#888;">
|
|
342
|
+
score: <b style="color:#e0e0e0;">{score:.3f}</b>
|
|
343
|
+
#{idx + 1}
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
{
|
|
347
|
+
'<div style="font-size:12px;color:#aaa;font-family:monospace;margin-bottom:2px;">'
|
|
348
|
+
+ label[:80]
|
|
349
|
+
+ "</div>"
|
|
350
|
+
if label and label != text[: len(label)]
|
|
351
|
+
else ""
|
|
352
|
+
}
|
|
353
|
+
<div style="font-size:13px;color:#c9d1d9;
|
|
354
|
+
white-space:pre-wrap;word-break:break-word;">{preview}</div>
|
|
355
|
+
</div>
|
|
356
|
+
""",
|
|
357
|
+
unsafe_allow_html=True,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _run_query(
|
|
362
|
+
db_path: str,
|
|
363
|
+
profile_path: str,
|
|
364
|
+
q: str,
|
|
365
|
+
k: int,
|
|
366
|
+
budget: int,
|
|
367
|
+
include_profile: bool,
|
|
368
|
+
kind_filter: str | None,
|
|
369
|
+
) -> tuple[list[dict], str]:
|
|
370
|
+
"""Execute query and context assembly; return (hits, context_md)."""
|
|
371
|
+
from agent_kg.assemble import assemble_context
|
|
372
|
+
from agent_kg.query import query as kg_query
|
|
373
|
+
from agent_kg.store import AgentKGStore
|
|
374
|
+
|
|
375
|
+
agentkg_dir = Path(db_path).parent
|
|
376
|
+
store = AgentKGStore(agentkg_dir / "graph.sqlite", agentkg_dir / "lancedb")
|
|
377
|
+
hits = kg_query(store, q, k=k, kind_filter=kind_filter)
|
|
378
|
+
|
|
379
|
+
if include_profile:
|
|
380
|
+
from agent_kg.user_profile import UserProfileStore
|
|
381
|
+
|
|
382
|
+
profile = UserProfileStore(Path(profile_path).parent)
|
|
383
|
+
profile_hits = profile.search(q, k=k)
|
|
384
|
+
hits = hits + profile_hits
|
|
385
|
+
|
|
386
|
+
hits.sort(key=lambda h: h.get("score", 0.0), reverse=True)
|
|
387
|
+
|
|
388
|
+
context_md = assemble_context(store, q, budget=budget)
|
|
389
|
+
return hits, context_md
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
# Main UI — load data
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
nodes, edges, sessions = _load_agent(str(agent_db), max_nodes)
|
|
397
|
+
profile_rows = _load_profile(str(profile_db))
|
|
398
|
+
|
|
399
|
+
tab_agent, tab_profile, tab_query, tab_stats = st.tabs(
|
|
400
|
+
["Agent Graph", "Profile Graph", "Query", "Stats"]
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# ── Agent Graph ──────────────────────────────────────────────────────────────
|
|
404
|
+
with tab_agent:
|
|
405
|
+
st.subheader(f"Conversation Graph — {repo}")
|
|
406
|
+
if not nodes:
|
|
407
|
+
st.info(f"No agent graph at `{agent_db}`. Run `agentkg ingest` to populate it.")
|
|
408
|
+
else:
|
|
409
|
+
try:
|
|
410
|
+
html = _agent_html(nodes, edges, physics)
|
|
411
|
+
st.components.v1.html(html, height=670, scrolling=False)
|
|
412
|
+
st.caption(f"{len(nodes)} nodes · {len(edges)} edges (capped at {max_nodes})")
|
|
413
|
+
except ImportError as exc:
|
|
414
|
+
st.error(str(exc))
|
|
415
|
+
|
|
416
|
+
# ── Profile Graph ─────────────────────────────────────────────────────────────
|
|
417
|
+
with tab_profile:
|
|
418
|
+
st.subheader(f"UserProfile — {person}")
|
|
419
|
+
if not profile_rows:
|
|
420
|
+
st.info(
|
|
421
|
+
f"No profile at `{profile_db}`. Run `agentkg onboard --person {person}` to build one."
|
|
422
|
+
)
|
|
423
|
+
else:
|
|
424
|
+
try:
|
|
425
|
+
html = _profile_html(profile_rows)
|
|
426
|
+
st.components.v1.html(html, height=620, scrolling=False)
|
|
427
|
+
st.caption(f"{len(profile_rows)} profile nodes")
|
|
428
|
+
except ImportError as exc:
|
|
429
|
+
st.error(str(exc))
|
|
430
|
+
|
|
431
|
+
# ── Query ─────────────────────────────────────────────────────────────────────
|
|
432
|
+
with tab_query:
|
|
433
|
+
st.subheader("Hybrid Semantic Query")
|
|
434
|
+
|
|
435
|
+
if not agent_db.exists():
|
|
436
|
+
st.warning(f"No agent graph at `{agent_db}`. Run `agentkg ingest` first.")
|
|
437
|
+
else:
|
|
438
|
+
q_col, btn_col = st.columns([5, 1])
|
|
439
|
+
with q_col:
|
|
440
|
+
query_text = st.text_input(
|
|
441
|
+
"Query",
|
|
442
|
+
placeholder="e.g. authentication strategy",
|
|
443
|
+
label_visibility="collapsed",
|
|
444
|
+
key="query_input",
|
|
445
|
+
)
|
|
446
|
+
with btn_col:
|
|
447
|
+
run_btn = st.button("▶ Run", type="primary", use_container_width=True)
|
|
448
|
+
|
|
449
|
+
if run_btn and query_text.strip():
|
|
450
|
+
with st.spinner("Searching…"):
|
|
451
|
+
try:
|
|
452
|
+
kf = kind_filter if kind_filter != "(all)" else None
|
|
453
|
+
hits, context_md = _run_query(
|
|
454
|
+
str(agent_db),
|
|
455
|
+
str(profile_db),
|
|
456
|
+
query_text.strip(),
|
|
457
|
+
k=k,
|
|
458
|
+
budget=budget,
|
|
459
|
+
include_profile=include_profile,
|
|
460
|
+
kind_filter=kf,
|
|
461
|
+
)
|
|
462
|
+
st.session_state.query_results = hits
|
|
463
|
+
st.session_state.query_context = context_md
|
|
464
|
+
st.session_state.last_query = query_text.strip()
|
|
465
|
+
except Exception as exc:
|
|
466
|
+
st.error(f"Query failed: {exc}")
|
|
467
|
+
|
|
468
|
+
results = st.session_state.query_results
|
|
469
|
+
context_md = st.session_state.query_context
|
|
470
|
+
|
|
471
|
+
if results is None:
|
|
472
|
+
st.info("Enter a query above and click **▶ Run**.")
|
|
473
|
+
elif not results:
|
|
474
|
+
st.warning("No results found.")
|
|
475
|
+
else:
|
|
476
|
+
last_q = st.session_state.last_query
|
|
477
|
+
res_tab, ctx_tab = st.tabs([f"Results ({len(results)})", "Assembled Context"])
|
|
478
|
+
|
|
479
|
+
with res_tab:
|
|
480
|
+
st.caption(
|
|
481
|
+
f"Query: **{last_q}** · k={k}"
|
|
482
|
+
+ (" · +profile" if include_profile else "")
|
|
483
|
+
+ (f" · kind={kind_filter}" if kind_filter != "(all)" else "")
|
|
484
|
+
)
|
|
485
|
+
for i, hit in enumerate(results):
|
|
486
|
+
_render_hit(hit, i)
|
|
487
|
+
|
|
488
|
+
with ctx_tab:
|
|
489
|
+
st.caption(f"Token budget: ~{budget} · query: **{last_q}**")
|
|
490
|
+
st.download_button(
|
|
491
|
+
"⬇ Download context",
|
|
492
|
+
data=context_md,
|
|
493
|
+
file_name="agentkg_context.md",
|
|
494
|
+
mime="text/markdown",
|
|
495
|
+
)
|
|
496
|
+
st.markdown(context_md)
|
|
497
|
+
|
|
498
|
+
# ── Stats ─────────────────────────────────────────────────────────────────────
|
|
499
|
+
with tab_stats:
|
|
500
|
+
col1, col2 = st.columns(2)
|
|
501
|
+
|
|
502
|
+
with col1:
|
|
503
|
+
st.subheader("Agent Graph")
|
|
504
|
+
if nodes:
|
|
505
|
+
from collections import Counter
|
|
506
|
+
|
|
507
|
+
kind_counts = Counter(n["kind"] for n in nodes)
|
|
508
|
+
st.dataframe(
|
|
509
|
+
[{"kind": k, "count": v} for k, v in sorted(kind_counts.items())],
|
|
510
|
+
use_container_width=True,
|
|
511
|
+
hide_index=True,
|
|
512
|
+
)
|
|
513
|
+
st.caption(f"**Sessions:** {len(sessions)}")
|
|
514
|
+
if sessions:
|
|
515
|
+
st.dataframe(
|
|
516
|
+
[
|
|
517
|
+
{
|
|
518
|
+
"id": s["id"][:8],
|
|
519
|
+
"start": (s["start_time"] or "")[:16],
|
|
520
|
+
"turns": s["turn_count"],
|
|
521
|
+
"prune passes": s["pruning_passes"],
|
|
522
|
+
}
|
|
523
|
+
for s in sessions
|
|
524
|
+
],
|
|
525
|
+
use_container_width=True,
|
|
526
|
+
hide_index=True,
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
st.info("No agent graph loaded.")
|
|
530
|
+
|
|
531
|
+
with col2:
|
|
532
|
+
st.subheader("UserProfile")
|
|
533
|
+
if profile_rows:
|
|
534
|
+
from collections import Counter
|
|
535
|
+
|
|
536
|
+
kind_counts = Counter(r["kind"] for r in profile_rows)
|
|
537
|
+
st.dataframe(
|
|
538
|
+
[{"kind": k, "count": v} for k, v in sorted(kind_counts.items())],
|
|
539
|
+
use_container_width=True,
|
|
540
|
+
hide_index=True,
|
|
541
|
+
)
|
|
542
|
+
else:
|
|
543
|
+
st.info("No profile loaded.")
|