sessions: list / tree / FTS5 search views via arborist.read.SessionsView
Mounts the sessions shard (~/.arborist/sessions.db by default) as a
read-only data source alongside the corpus shards. Imports stay
behind the arborist.read seam — viz never touches the schema directly.
Wire:
- storage.py: get_sessions(path) — process-cached SessionsView,
parallel structure to get_shards(paths). close_all() now closes
both caches.
- __init__.py: ARBORIST_VIZ_SESSIONS_DB env (or arborist.sessions_db
ini) → request.sessions request property. Defaults to
~/.arborist/sessions.db.
- routes.py: /sessions (list), /sessions/{sid} (tree),
/sessions/find (FTS5 search) + matching /api/sessions/... JSON.
- views/sessions.py: HTML + JSON handlers.
- templates/sessions_{list,tree,find}.jinja2: minimal views — table
for list, recursive tree macro for tree, table for find results.
- static/css/viz.css: cmd-table / cmd-tree / cmd-audit-* styles.
Endpoint summary:
/sessions list every session
/sessions/{sid} one session's tree (synthetic root
collapsed, cross-session children show
[from <sid>] tag)
/sessions/find?q=...&limit= FTS5 across all sessions
/api/sessions JSON list
/api/sessions/{sid} JSON tree
/api/sessions/find?q=... JSON search
Audit_mode is rendered as a colored chip (strict/hybrid/ungrounded/
canonical_projection); cited titles surface as inline pills. Privacy
contract from base.jinja2 inherits unchanged — sessions shard is
local-private per ticket #000069 §14.
This commit is contained in:
parent
44bec3503d
commit
60e8640d86
8 changed files with 428 additions and 0 deletions
|
|
@ -146,6 +146,24 @@ def main(global_config, **settings):
|
||||||
return _storage.get_shards(req.arborist_shards)
|
return _storage.get_shards(req.arborist_shards)
|
||||||
config.add_request_method(_shards, "shards", reify=True)
|
config.add_request_method(_shards, "shards", reify=True)
|
||||||
|
|
||||||
|
# Sessions shard — single-file conversation-forest journal. The
|
||||||
|
# corpus shards (Shards above) and the sessions shard are unrelated
|
||||||
|
# data layers, so they get separate request properties + separate
|
||||||
|
# process caches. Env var ARBORIST_VIZ_SESSIONS_DB overrides the
|
||||||
|
# .ini default ("arborist.sessions_db"); both default to
|
||||||
|
# ~/.arborist/sessions.db when unset.
|
||||||
|
sessions_db = (
|
||||||
|
os.environ.get("ARBORIST_VIZ_SESSIONS_DB")
|
||||||
|
or settings.get("arborist.sessions_db", "")
|
||||||
|
or str(os.path.expanduser("~/.arborist/sessions.db"))
|
||||||
|
)
|
||||||
|
config.add_request_method(
|
||||||
|
lambda r, _v=sessions_db: _v, "arborist_sessions_db", reify=True
|
||||||
|
)
|
||||||
|
def _sessions(req):
|
||||||
|
return _storage.get_sessions(req.arborist_sessions_db)
|
||||||
|
config.add_request_method(_sessions, "sessions", reify=True)
|
||||||
|
|
||||||
config.include(".routes")
|
config.include(".routes")
|
||||||
config.scan("arborist_viz.views")
|
config.scan("arborist_viz.views")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,16 @@ def includeme(config):
|
||||||
config.add_route("api_diff", "/api/diff/{root_a}/{root_b}")
|
config.add_route("api_diff", "/api/diff/{root_a}/{root_b}")
|
||||||
config.add_route("api_graph_snapshot", "/api/graph/snapshot")
|
config.add_route("api_graph_snapshot", "/api/graph/snapshot")
|
||||||
|
|
||||||
|
# sessions — Merkle conversation forest (single-shard, per-user
|
||||||
|
# journal at ~/.arborist/sessions.db). Read-only consumer of
|
||||||
|
# arborist.read.SessionsView.
|
||||||
|
config.add_route("sessions_list", "/sessions")
|
||||||
|
config.add_route("sessions_find", "/sessions/find")
|
||||||
|
config.add_route("sessions_tree", "/sessions/{sid}")
|
||||||
|
config.add_route("api_sessions_list", "/api/sessions")
|
||||||
|
config.add_route("api_sessions_find", "/api/sessions/find")
|
||||||
|
config.add_route("api_sessions_tree", "/api/sessions/{sid}")
|
||||||
|
|
||||||
# SSE — §3-corrected replaces WS routes with text/event-stream.
|
# SSE — §3-corrected replaces WS routes with text/event-stream.
|
||||||
config.add_route("sse_audit_live", "/sse/audit/live")
|
config.add_route("sse_audit_live", "/sse/audit/live")
|
||||||
config.add_route("sse_run", "/sse/run/{run_id}")
|
config.add_route("sse_run", "/sse/run/{run_id}")
|
||||||
|
|
|
||||||
|
|
@ -629,3 +629,47 @@ code, pre { font-family: var(--mono); font-size: 12px; }
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- sessions views (ticket #000069 sessions surface) ---------------- */
|
||||||
|
|
||||||
|
.cmd-table { width: 100%; border-collapse: collapse; margin-top: 1em; }
|
||||||
|
.cmd-table th, .cmd-table td {
|
||||||
|
text-align: left; padding: 0.4em 0.8em;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.cmd-table th { font-weight: 600; opacity: 0.8; }
|
||||||
|
.cmd-table code { font-size: 0.85em; }
|
||||||
|
.cmd-hash { word-break: break-all; }
|
||||||
|
|
||||||
|
.cmd-inline-form { display: inline-flex; gap: 0.5em; margin-top: 0.5em; }
|
||||||
|
.cmd-inline-form input[type=text] { min-width: 24em; }
|
||||||
|
|
||||||
|
.cmd-empty { opacity: 0.6; font-style: italic; padding: 2em 0; }
|
||||||
|
|
||||||
|
.cmd-tree { list-style: none; padding-left: 0; }
|
||||||
|
.cmd-tree-children { list-style: none; padding-left: 1.5em;
|
||||||
|
border-left: 1px solid rgba(255,255,255,0.15); margin-left: 0.4em; }
|
||||||
|
.cmd-tree-node { margin: 0.4em 0; }
|
||||||
|
.cmd-tree-line { display: flex; gap: 0.6em; align-items: baseline;
|
||||||
|
flex-wrap: wrap; }
|
||||||
|
.cmd-tree-bates { font-size: 0.85em; }
|
||||||
|
.cmd-tree-audit { font-size: 0.75em; padding: 0.05em 0.4em;
|
||||||
|
border-radius: 0.2em; font-weight: 600; }
|
||||||
|
.cmd-audit-strict { background: rgba(80, 200, 120, 0.25); }
|
||||||
|
.cmd-audit-hybrid { background: rgba(220, 180, 80, 0.25); }
|
||||||
|
.cmd-audit-ungrounded { background: rgba(220, 100, 100, 0.25); }
|
||||||
|
.cmd-audit-canonical_projection { background: rgba(120, 160, 220, 0.25); }
|
||||||
|
.cmd-tree-from { font-size: 0.75em; opacity: 0.6; font-style: italic; }
|
||||||
|
.cmd-tree-label { font-size: 0.85em; opacity: 0.7; }
|
||||||
|
.cmd-tree-q { font-size: 0.95em; }
|
||||||
|
.cmd-tree-current > .cmd-tree-line .cmd-tree-bates { font-weight: 700; }
|
||||||
|
.cmd-tree-current > .cmd-tree-line::after {
|
||||||
|
content: '← current'; font-size: 0.8em; opacity: 0.7;
|
||||||
|
}
|
||||||
|
.cmd-tree-branch > .cmd-tree-line .cmd-tree-bates { color: #8af; }
|
||||||
|
.cmd-tree-cites { display: flex; gap: 0.3em; flex-wrap: wrap;
|
||||||
|
padding-left: 1.5em; margin-top: 0.2em; }
|
||||||
|
.cmd-cite { font-size: 0.75em; opacity: 0.7;
|
||||||
|
padding: 0.05em 0.4em; border-radius: 0.2em;
|
||||||
|
background: rgba(255,255,255,0.06); }
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,14 @@ except Exception: # pragma: no cover — fixture mode only
|
||||||
Shards = None # type: ignore
|
Shards = None # type: ignore
|
||||||
open_shards = None # type: ignore
|
open_shards = None # type: ignore
|
||||||
|
|
||||||
|
try:
|
||||||
|
from arborist.read import SessionsView, open_sessions
|
||||||
|
HAVE_ARBORIST_SESSIONS = True
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
HAVE_ARBORIST_SESSIONS = False
|
||||||
|
SessionsView = None # type: ignore
|
||||||
|
open_sessions = None # type: ignore
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -63,3 +71,30 @@ def close_all() -> None:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
_HANDLES.clear()
|
_HANDLES.clear()
|
||||||
|
for v in _SESSIONS_HANDLES.values():
|
||||||
|
try:
|
||||||
|
v.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_SESSIONS_HANDLES.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- sessions (conversation forest, single per-user shard) ----------
|
||||||
|
|
||||||
|
_SESSIONS_HANDLES: dict[str, "SessionsView"] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_sessions(path: str | None) -> Optional["SessionsView"]:
|
||||||
|
"""Return the cached :class:`arborist.read.SessionsView` for the
|
||||||
|
sessions shard at ``path``. When ``path`` is falsy or arborist
|
||||||
|
isn't importable, returns ``None`` — callers should render the
|
||||||
|
"no sessions shard" fallback state.
|
||||||
|
"""
|
||||||
|
if not HAVE_ARBORIST_SESSIONS or not path:
|
||||||
|
return None
|
||||||
|
v = _SESSIONS_HANDLES.get(path)
|
||||||
|
if v is None or not v.is_open:
|
||||||
|
v = open_sessions(path)
|
||||||
|
_SESSIONS_HANDLES[path] = v
|
||||||
|
log.info("arborist.read.sessions: opened %s", path)
|
||||||
|
return v
|
||||||
|
|
|
||||||
44
arborist_viz/templates/sessions_find.jinja2
Normal file
44
arborist_viz/templates/sessions_find.jinja2
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block body %}
|
||||||
|
<section class="cmd-panel">
|
||||||
|
<header class="cmd-panel-head">
|
||||||
|
<h1>Search sessions</h1>
|
||||||
|
<form action="/sessions/find" method="get" class="cmd-inline-form">
|
||||||
|
<input type="text" name="q" value="{{ query or '' }}" placeholder="FTS5 across question + answer + cited titles" autocomplete="off" autofocus>
|
||||||
|
<input type="number" name="limit" value="{{ limit }}" min="1" max="100" style="width: 5em;">
|
||||||
|
<button type="submit">find</button>
|
||||||
|
</form>
|
||||||
|
<p><a href="/sessions">← all sessions</a></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if query %}
|
||||||
|
<p class="cmd-panel-sub">{{ hits|length }} match(es) for <code>{{ query }}</code></p>
|
||||||
|
{% if hits %}
|
||||||
|
<table class="cmd-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>bates</th>
|
||||||
|
<th>session</th>
|
||||||
|
<th>audit</th>
|
||||||
|
<th>question</th>
|
||||||
|
<th>cited titles</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for n in hits %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/sessions/{{ n.sid }}"><code>{{ n.bates }}</code></a></td>
|
||||||
|
<td><code>{{ n.sid }}</code></td>
|
||||||
|
<td><span class="cmd-tree-audit cmd-audit-{{ n.audit_mode|lower }}">{{ n.audit_mode }}</span></td>
|
||||||
|
<td>{{ (n.question or '')[:120] }}</td>
|
||||||
|
<td>{% for t in n.cited_titles %}<span class="cmd-cite">{{ t }}</span>{% endfor %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="cmd-empty">no matches</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
52
arborist_viz/templates/sessions_list.jinja2
Normal file
52
arborist_viz/templates/sessions_list.jinja2
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block body %}
|
||||||
|
<section class="cmd-panel">
|
||||||
|
<header class="cmd-panel-head">
|
||||||
|
<h1>Sessions</h1>
|
||||||
|
<p class="cmd-panel-sub">
|
||||||
|
Merkle conversation forest — single shard,
|
||||||
|
{% if sessions_stats.open %}
|
||||||
|
{{ sessions_stats.n_sessions }} session(s) ·
|
||||||
|
{{ sessions_stats.n_nodes }} node(s) ·
|
||||||
|
{{ sessions_stats.n_audit_events }} audit event(s) ·
|
||||||
|
<code>{{ sessions_stats.path }}</code>
|
||||||
|
{% else %}
|
||||||
|
no sessions shard configured
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<form action="/sessions/find" method="get" class="cmd-inline-form">
|
||||||
|
<input type="text" name="q" placeholder="search across all sessions (FTS5)" autocomplete="off">
|
||||||
|
<button type="submit">find</button>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if rows %}
|
||||||
|
<table class="cmd-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>sid</th>
|
||||||
|
<th>nodes</th>
|
||||||
|
<th>session_root</th>
|
||||||
|
<th>current_bates</th>
|
||||||
|
<th>updated</th>
|
||||||
|
<th>label</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in rows %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/sessions/{{ s.sid }}"><code>{{ s.sid }}</code></a></td>
|
||||||
|
<td>{{ s.n_nodes }}</td>
|
||||||
|
<td><code class="cmd-hash">{{ s.session_root }}</code></td>
|
||||||
|
<td><code>{{ s.current_bates }}</code></td>
|
||||||
|
<td>{{ s.updated_at }}</td>
|
||||||
|
<td>{{ s.label or '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="cmd-empty">No sessions yet. Run <code>make session</code> to start one.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
52
arborist_viz/templates/sessions_tree.jinja2
Normal file
52
arborist_viz/templates/sessions_tree.jinja2
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "base.jinja2" %}
|
||||||
|
|
||||||
|
{% macro render_node(node, kids_of, branch_set, current_bates, this_sid, depth=0) -%}
|
||||||
|
<li class="cmd-tree-node {% if node.bates == current_bates %}cmd-tree-current{% endif %} {% if node.bates in branch_set %}cmd-tree-branch{% endif %}">
|
||||||
|
<div class="cmd-tree-line">
|
||||||
|
<span class="cmd-tree-bates"><code>{{ node.bates }}</code></span>
|
||||||
|
<span class="cmd-tree-audit cmd-audit-{{ node.audit_mode|lower }}">{{ node.audit_mode }}</span>
|
||||||
|
{% if node.sid != this_sid %}<span class="cmd-tree-from">[from {{ node.sid }}]</span>{% endif %}
|
||||||
|
{% if node.label %}<span class="cmd-tree-label">{{ '{' + node.label + '}' }}</span>{% endif %}
|
||||||
|
<span class="cmd-tree-q">{{ (node.question or '')[:120] }}</span>
|
||||||
|
</div>
|
||||||
|
{% if node.cited_titles %}
|
||||||
|
<div class="cmd-tree-cites">
|
||||||
|
{% for t in node.cited_titles %}<span class="cmd-cite">{{ t }}</span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% set kids = kids_of.get(node.bates, []) %}
|
||||||
|
{% if kids %}
|
||||||
|
<ul class="cmd-tree-children">
|
||||||
|
{% for c in kids %}
|
||||||
|
{{ render_node(c, kids_of, branch_set, current_bates, this_sid, depth + 1) }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<section class="cmd-panel">
|
||||||
|
<header class="cmd-panel-head">
|
||||||
|
<h1>session <code>{{ session.sid }}</code></h1>
|
||||||
|
<p class="cmd-panel-sub">
|
||||||
|
{{ session.n_nodes }} node(s) · {{ branches|length }} branch point(s) ·
|
||||||
|
<code class="cmd-hash">root={{ session.session_root }}</code> ·
|
||||||
|
current → <code>{{ session.current_bates }}</code>
|
||||||
|
</p>
|
||||||
|
<p><a href="/sessions">← all sessions</a> · <a href="/api/sessions/{{ session.sid }}">JSON</a></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% set kids_of = children %}
|
||||||
|
{% set root_node = nodes[0] if nodes and nodes[0].seq == 0 else None %}
|
||||||
|
{% if root_node %}
|
||||||
|
<ul class="cmd-tree">
|
||||||
|
{% for child in kids_of.get(root_node.bates, []) %}
|
||||||
|
{{ render_node(child, kids_of, branches, session.current_bates, session.sid) }}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="cmd-empty">empty session</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
173
arborist_viz/views/sessions.py
Normal file
173
arborist_viz/views/sessions.py
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
"""Sessions — Merkle conversation forest dashboard.
|
||||||
|
|
||||||
|
Read-only view of the sessions shard (one file per user, at
|
||||||
|
``~/.arborist/sessions.db`` by default; overridable via env or .ini).
|
||||||
|
Three surfaces:
|
||||||
|
|
||||||
|
- /sessions list all sessions
|
||||||
|
- /sessions/{sid} one session's tree
|
||||||
|
- /sessions/find?q=... FTS5 search across every session
|
||||||
|
- /api/sessions JSON list
|
||||||
|
- /api/sessions/{sid} JSON tree
|
||||||
|
- /api/sessions/find?q=... JSON search
|
||||||
|
|
||||||
|
All schema knowledge lives behind :mod:`arborist.read.SessionsView`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPNotFound
|
||||||
|
from pyramid.view import view_config
|
||||||
|
|
||||||
|
from ..views.home import _common_ctx
|
||||||
|
|
||||||
|
|
||||||
|
def _sessions_ctx(request) -> dict:
|
||||||
|
"""Common template context with sessions stats."""
|
||||||
|
sessions = request.sessions
|
||||||
|
if sessions is None or not sessions.is_open:
|
||||||
|
s_stats = {"n_sessions": 0, "n_nodes": 0,
|
||||||
|
"n_audit_events": 0, "open": False}
|
||||||
|
else:
|
||||||
|
c = sessions.counts()
|
||||||
|
s_stats = {
|
||||||
|
"n_sessions": c.n_sessions,
|
||||||
|
"n_nodes": c.n_nodes,
|
||||||
|
"n_audit_events": c.n_audit_events,
|
||||||
|
"open": True,
|
||||||
|
"path": c.shard_path,
|
||||||
|
}
|
||||||
|
ctx = _common_ctx(request)
|
||||||
|
ctx["sessions_stats"] = s_stats
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="sessions_list", renderer="sessions_list.jinja2")
|
||||||
|
def sessions_list(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
rows = sessions.list_sessions() if sessions else []
|
||||||
|
ctx = _sessions_ctx(request)
|
||||||
|
ctx["title"] = "Sessions — Arborist VIZ"
|
||||||
|
ctx["rows"] = rows
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="sessions_tree", renderer="sessions_tree.jinja2")
|
||||||
|
def sessions_tree(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
if sessions is None:
|
||||||
|
raise HTTPNotFound("no sessions shard configured")
|
||||||
|
sid = request.matchdict["sid"]
|
||||||
|
meta = sessions.get_session(sid)
|
||||||
|
if meta is None:
|
||||||
|
raise HTTPNotFound(f"no such session: {sid}")
|
||||||
|
nodes = sessions.all_nodes_in_session(sid)
|
||||||
|
# Materialize a children-map per Bates so the template can render
|
||||||
|
# the tree without N round-trips into SessionsView.
|
||||||
|
children: dict[str, list] = {}
|
||||||
|
for n in nodes:
|
||||||
|
if n.parent_bates:
|
||||||
|
children.setdefault(n.parent_bates, []).append(n)
|
||||||
|
# Also include cross-session children whose parent_bates points
|
||||||
|
# into this session.
|
||||||
|
for parent_bates in list(children.keys()):
|
||||||
|
pass # already populated by the loop above
|
||||||
|
branches = sessions.branches_in_session(sid)
|
||||||
|
ctx = _sessions_ctx(request)
|
||||||
|
ctx["title"] = f"session {sid} — Arborist VIZ"
|
||||||
|
ctx["session"] = meta
|
||||||
|
ctx["nodes"] = nodes
|
||||||
|
ctx["children"] = children
|
||||||
|
ctx["branches"] = set(branches)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="sessions_find", renderer="sessions_find.jinja2")
|
||||||
|
def sessions_find(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
q = (request.params.get("q") or "").strip()
|
||||||
|
try:
|
||||||
|
limit = max(1, min(100, int(request.params.get("limit") or "20")))
|
||||||
|
except ValueError:
|
||||||
|
limit = 20
|
||||||
|
hits = sessions.find(q, limit=limit) if (sessions and q) else []
|
||||||
|
ctx = _sessions_ctx(request)
|
||||||
|
ctx["title"] = f"search — Arborist VIZ" + (f" — {q}" if q else "")
|
||||||
|
ctx["query"] = q
|
||||||
|
ctx["limit"] = limit
|
||||||
|
ctx["hits"] = hits
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
# ---- JSON APIs ------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _node_to_json(n) -> dict:
|
||||||
|
return {
|
||||||
|
"bates": n.bates, "sid": n.sid, "seq": n.seq,
|
||||||
|
"parent_bates": n.parent_bates,
|
||||||
|
"question": n.question,
|
||||||
|
"answer_text": n.answer_text,
|
||||||
|
"cache_key": n.cache_key,
|
||||||
|
"audit_mode": n.audit_mode,
|
||||||
|
"cited_titles": n.cited_titles,
|
||||||
|
"n_cited_sources": n.n_cited_sources,
|
||||||
|
"node_hash": n.node_hash,
|
||||||
|
"subtree_hash": n.subtree_hash,
|
||||||
|
"created_at": n.created_at,
|
||||||
|
"label": n.label,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="api_sessions_list", renderer="json")
|
||||||
|
def api_sessions_list(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
if sessions is None:
|
||||||
|
return {"sessions": [], "open": False}
|
||||||
|
return {
|
||||||
|
"sessions": [
|
||||||
|
{
|
||||||
|
"sid": s.sid, "root_bates": s.root_bates,
|
||||||
|
"current_bates": s.current_bates,
|
||||||
|
"label": s.label, "n_nodes": s.n_nodes,
|
||||||
|
"session_root": s.session_root,
|
||||||
|
"created_at": s.created_at,
|
||||||
|
"updated_at": s.updated_at,
|
||||||
|
}
|
||||||
|
for s in sessions.list_sessions()
|
||||||
|
],
|
||||||
|
"open": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="api_sessions_tree", renderer="json")
|
||||||
|
def api_sessions_tree(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
if sessions is None:
|
||||||
|
raise HTTPNotFound("no sessions shard")
|
||||||
|
sid = request.matchdict["sid"]
|
||||||
|
meta = sessions.get_session(sid)
|
||||||
|
if meta is None:
|
||||||
|
raise HTTPNotFound(f"no such session: {sid}")
|
||||||
|
nodes = sessions.all_nodes_in_session(sid)
|
||||||
|
return {
|
||||||
|
"sid": sid,
|
||||||
|
"session_root": meta.session_root,
|
||||||
|
"current_bates": meta.current_bates,
|
||||||
|
"branches": sessions.branches_in_session(sid),
|
||||||
|
"nodes": [_node_to_json(n) for n in nodes],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@view_config(route_name="api_sessions_find", renderer="json")
|
||||||
|
def api_sessions_find(request):
|
||||||
|
sessions = request.sessions
|
||||||
|
q = (request.params.get("q") or "").strip()
|
||||||
|
try:
|
||||||
|
limit = max(1, min(100, int(request.params.get("limit") or "20")))
|
||||||
|
except ValueError:
|
||||||
|
limit = 20
|
||||||
|
hits = sessions.find(q, limit=limit) if (sessions and q) else []
|
||||||
|
return {
|
||||||
|
"query": q, "limit": limit,
|
||||||
|
"hits": [_node_to_json(n) for n in hits],
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue