ravmem-cli 0.1.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.
- ravmem/cli/__init__.py +0 -0
- ravmem/cli/analyze.py +484 -0
- ravmem/cli/auth.py +675 -0
- ravmem/cli/browse.py +144 -0
- ravmem/cli/config_cmd.py +69 -0
- ravmem/cli/diff.py +122 -0
- ravmem/cli/git_local.py +131 -0
- ravmem/cli/impact.py +106 -0
- ravmem/cli/info.py +82 -0
- ravmem/cli/kg.py +787 -0
- ravmem/cli/main.py +96 -0
- ravmem/cli/remote.py +549 -0
- ravmem/cli/search.py +91 -0
- ravmem/cli/serve.py +41 -0
- ravmem/client/__init__.py +3 -0
- ravmem/client/api.py +279 -0
- ravmem/client/config.py +52 -0
- ravmem/config.py +103 -0
- ravmem/config_loader.py +247 -0
- ravmem_cli-0.1.0.dist-info/METADATA +62 -0
- ravmem_cli-0.1.0.dist-info/RECORD +25 -0
- ravmem_cli-0.1.0.dist-info/WHEEL +5 -0
- ravmem_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ravmem_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ravmem_cli-0.1.0.dist-info/top_level.txt +1 -0
ravmem/cli/kg.py
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""ravmem kg — knowledge graph management subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich import box
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="kg",
|
|
17
|
+
help="Knowledge graph management — browse, curate, and query agent memory.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_console = Console()
|
|
22
|
+
_err = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Shared helpers
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def _load_server_cfg(remote: str) -> tuple[str, str | None]:
|
|
29
|
+
"""Return (server_url, api_key) from --remote override or ~/.ravmem/client.json."""
|
|
30
|
+
if remote:
|
|
31
|
+
return remote.rstrip("/"), None
|
|
32
|
+
import os
|
|
33
|
+
cfg_path = os.path.expanduser("~/.ravmem/client.json")
|
|
34
|
+
if not os.path.exists(cfg_path):
|
|
35
|
+
_err.print("[red]No server configured.[/red] Run: ravmem remote config --url <URL>")
|
|
36
|
+
raise typer.Exit(1)
|
|
37
|
+
with open(cfg_path) as f:
|
|
38
|
+
data = _json.load(f)
|
|
39
|
+
url = data.get("server_url", "").rstrip("/")
|
|
40
|
+
if not url:
|
|
41
|
+
_err.print("[red]server_url missing in client.json.[/red] Run: ravmem remote config --url <URL>")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
return url, data.get("api_key") or None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _kg_client(remote: str, team: str, branch: str):
|
|
47
|
+
"""Return an httpx.Client with KG session headers pre-set."""
|
|
48
|
+
import httpx
|
|
49
|
+
url, api_key = _load_server_cfg(remote)
|
|
50
|
+
headers = {
|
|
51
|
+
"X-KG-Team": team,
|
|
52
|
+
"X-KG-Branch": branch,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}
|
|
55
|
+
if api_key:
|
|
56
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
57
|
+
return url, httpx.Client(base_url=url, headers=headers, timeout=15.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _kg_get(remote: str, team: str, branch: str, path: str, params: dict | None = None) -> dict | list:
|
|
61
|
+
url, client = _kg_client(remote, team, branch)
|
|
62
|
+
with client:
|
|
63
|
+
resp = client.get(path, params=params or {})
|
|
64
|
+
if resp.status_code == 404:
|
|
65
|
+
_err.print(f"[yellow]Not found:[/yellow] {path}")
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
resp.raise_for_status()
|
|
68
|
+
return resp.json()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _kg_post(remote: str, team: str, branch: str, path: str, body: dict) -> dict:
|
|
72
|
+
url, client = _kg_client(remote, team, branch)
|
|
73
|
+
with client:
|
|
74
|
+
resp = client.post(path, json=body)
|
|
75
|
+
resp.raise_for_status()
|
|
76
|
+
return resp.json()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _kg_patch(remote: str, team: str, branch: str, path: str, body: dict) -> dict:
|
|
80
|
+
url, client = _kg_client(remote, team, branch)
|
|
81
|
+
with client:
|
|
82
|
+
resp = client.patch(path, json=body)
|
|
83
|
+
resp.raise_for_status()
|
|
84
|
+
return resp.json()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _kg_delete(remote: str, team: str, branch: str, path: str) -> dict:
|
|
88
|
+
url, client = _kg_client(remote, team, branch)
|
|
89
|
+
with client:
|
|
90
|
+
resp = client.delete(path)
|
|
91
|
+
resp.raise_for_status()
|
|
92
|
+
return resp.json()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _trunc(s: str, n: int) -> str:
|
|
96
|
+
s = str(s or "")
|
|
97
|
+
return s if len(s) <= n else s[: n - 1] + "…"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _print_node_table(nodes: list[dict], title: str = "") -> None:
|
|
101
|
+
t = Table(box=box.SIMPLE, title=title or None, show_header=True, header_style="bold")
|
|
102
|
+
t.add_column("ID", style="dim", width=10)
|
|
103
|
+
t.add_column("Type", width=12)
|
|
104
|
+
t.add_column("Label", width=55)
|
|
105
|
+
t.add_column("Conf", width=5, justify="right")
|
|
106
|
+
t.add_column("Source", width=8)
|
|
107
|
+
t.add_column("Created", width=19)
|
|
108
|
+
for n in nodes:
|
|
109
|
+
conf = n.get("confidence")
|
|
110
|
+
conf_str = f"{conf:.2f}" if conf is not None else "—"
|
|
111
|
+
t.add_row(
|
|
112
|
+
n.get("id", "")[:8],
|
|
113
|
+
n.get("type", ""),
|
|
114
|
+
_trunc(n.get("label", ""), 55),
|
|
115
|
+
conf_str,
|
|
116
|
+
n.get("source", ""),
|
|
117
|
+
str(n.get("created_at", ""))[:19],
|
|
118
|
+
)
|
|
119
|
+
_console.print(t)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _print_node_detail(node: dict) -> None:
|
|
123
|
+
lines = []
|
|
124
|
+
for k in ("id", "type", "label", "scope", "branch", "team_id", "source",
|
|
125
|
+
"author_id", "active", "confidence", "validated_by_human",
|
|
126
|
+
"origin_node_id", "created_at", "updated_at"):
|
|
127
|
+
v = node.get(k)
|
|
128
|
+
if v is not None:
|
|
129
|
+
lines.append(f"[bold]{k}[/bold]: {v}")
|
|
130
|
+
# Extra properties
|
|
131
|
+
skip = {"id", "type", "label", "scope", "branch", "team_id", "source",
|
|
132
|
+
"author_id", "active", "confidence", "validated_by_human",
|
|
133
|
+
"origin_node_id", "created_at", "updated_at"}
|
|
134
|
+
for k, v in node.items():
|
|
135
|
+
if k not in skip and v is not None:
|
|
136
|
+
lines.append(f"[bold]{k}[/bold]: {v}")
|
|
137
|
+
_console.print(Panel("\n".join(lines), title=f"[cyan]{node.get('type', 'Node')}[/cyan] {node.get('id', '')[:8]}"))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Existing commands (init, config) — unchanged
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
@app.command("init")
|
|
145
|
+
def init_cmd(
|
|
146
|
+
team: str = typer.Option("default", "--team", "-t", help="Team identifier for this KGConfig"),
|
|
147
|
+
branch: str = typer.Option("global", "--branch", "-b", help="Knowledge branch to configure"),
|
|
148
|
+
remote: str = typer.Option("", "--remote", "-r", help="Server URL (overrides client.json)"),
|
|
149
|
+
):
|
|
150
|
+
"""Interactive wizard to initialise or reconfigure the knowledge graph."""
|
|
151
|
+
from ravmem.knowledge.init import run_init
|
|
152
|
+
|
|
153
|
+
existing_cfg = None
|
|
154
|
+
server_url = remote
|
|
155
|
+
|
|
156
|
+
if not server_url:
|
|
157
|
+
try:
|
|
158
|
+
server_url, _ = _load_server_cfg("")
|
|
159
|
+
except typer.Exit:
|
|
160
|
+
server_url = ""
|
|
161
|
+
|
|
162
|
+
if server_url:
|
|
163
|
+
try:
|
|
164
|
+
import httpx
|
|
165
|
+
from ravmem.knowledge.config import KGConfig
|
|
166
|
+
resp = httpx.get(
|
|
167
|
+
f"{server_url}/api/knowledge/config",
|
|
168
|
+
params={"team_id": team, "branch": branch},
|
|
169
|
+
timeout=5,
|
|
170
|
+
)
|
|
171
|
+
if resp.status_code == 200:
|
|
172
|
+
data = resp.json()
|
|
173
|
+
existing_cfg = KGConfig(
|
|
174
|
+
team_id=data.get("team_id", team),
|
|
175
|
+
branch=data.get("branch", branch),
|
|
176
|
+
mode=data.get("mode", "hitl"),
|
|
177
|
+
confidence_enabled=data.get("confidence_enabled", True),
|
|
178
|
+
hitl_ai_cap=data.get("hitl_ai_cap", 0.70),
|
|
179
|
+
auto_ai_cap=data.get("auto_ai_cap", 1.0),
|
|
180
|
+
disabled_node_types=data.get("disabled_node_types", []),
|
|
181
|
+
)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def _save(cfg):
|
|
186
|
+
if not server_url:
|
|
187
|
+
typer.echo("Warning: no server configured — config not persisted.", err=True)
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
import httpx
|
|
191
|
+
resp = httpx.post(f"{server_url}/api/knowledge/config", json=cfg.to_dict(), timeout=5)
|
|
192
|
+
resp.raise_for_status()
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
typer.echo(f"Failed to save config: {exc}", err=True)
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
|
|
197
|
+
run_init(team_id=team, branch=branch, existing_cfg=existing_cfg,
|
|
198
|
+
save_fn=_save if server_url else None)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@app.command("config")
|
|
202
|
+
def config_cmd(
|
|
203
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
204
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
205
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
206
|
+
):
|
|
207
|
+
"""Show current knowledge graph configuration from the server."""
|
|
208
|
+
try:
|
|
209
|
+
data = _kg_get(remote, team, branch, "/api/knowledge/config",
|
|
210
|
+
params={"team_id": team, "branch": branch})
|
|
211
|
+
except typer.Exit:
|
|
212
|
+
typer.echo("No config found. Run: ravmem kg init")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
t = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
216
|
+
t.add_column("Setting", style="bold")
|
|
217
|
+
t.add_column("Value")
|
|
218
|
+
t.add_row("Team", data.get("team_id", team))
|
|
219
|
+
t.add_row("Branch", data.get("branch", branch))
|
|
220
|
+
t.add_row("Mode", data.get("mode", "?").upper())
|
|
221
|
+
t.add_row("Confidence scoring", "enabled" if data.get("confidence_enabled", True) else "disabled")
|
|
222
|
+
t.add_row("HITL AI cap", str(data.get("hitl_ai_cap", "?")))
|
|
223
|
+
t.add_row("Auto AI cap", str(data.get("auto_ai_cap", "?")))
|
|
224
|
+
disabled = data.get("disabled_node_types", [])
|
|
225
|
+
if disabled:
|
|
226
|
+
t.add_row("Disabled node types", ", ".join(disabled))
|
|
227
|
+
_console.print(t)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Phase 2 — Browse and inspect
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
@app.command("list")
|
|
235
|
+
def list_cmd(
|
|
236
|
+
node_type: Optional[str] = typer.Option(None, "--type", "-T",
|
|
237
|
+
help="Filter by type: instruction|condition|learning|decision|task|blocker|context|taskpattern"),
|
|
238
|
+
scope: Optional[str] = typer.Option(None, "--scope", "-s",
|
|
239
|
+
help="Filter by scope: global | project:<id> | task_type:<pattern>"),
|
|
240
|
+
all_nodes: bool = typer.Option(False, "--all", "-a",
|
|
241
|
+
help="Include inactive nodes (default: active only)"),
|
|
242
|
+
limit: int = typer.Option(50, "--limit", "-n"),
|
|
243
|
+
output_json: bool = typer.Option(False, "--json", help="Raw JSON output"),
|
|
244
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
245
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
246
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
247
|
+
):
|
|
248
|
+
"""List knowledge graph nodes."""
|
|
249
|
+
params: dict = {"limit": limit}
|
|
250
|
+
if node_type:
|
|
251
|
+
params["node_type"] = node_type
|
|
252
|
+
if scope:
|
|
253
|
+
params["scope"] = scope
|
|
254
|
+
if all_nodes:
|
|
255
|
+
params["active_only"] = "false"
|
|
256
|
+
|
|
257
|
+
data = _kg_get(remote, team, branch, "/api/knowledge/nodes", params=params)
|
|
258
|
+
nodes = data if isinstance(data, list) else data.get("nodes", [])
|
|
259
|
+
|
|
260
|
+
if output_json:
|
|
261
|
+
typer.echo(_json.dumps(nodes, indent=2, default=str))
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
if not nodes:
|
|
265
|
+
typer.echo("No nodes found.")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
label = f"{len(nodes)} node(s)"
|
|
269
|
+
if node_type:
|
|
270
|
+
label += f" — type={node_type}"
|
|
271
|
+
_print_node_table(nodes, title=label)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@app.command("get")
|
|
275
|
+
def get_cmd(
|
|
276
|
+
node_id: str = typer.Argument(..., help="Node ID (full or 8-char prefix)"),
|
|
277
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
278
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
279
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
280
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
281
|
+
):
|
|
282
|
+
"""Show full details for a single knowledge node."""
|
|
283
|
+
node = _kg_get(remote, team, branch, f"/api/knowledge/nodes/{node_id}")
|
|
284
|
+
if output_json:
|
|
285
|
+
typer.echo(_json.dumps(node, indent=2, default=str))
|
|
286
|
+
return
|
|
287
|
+
_print_node_detail(node)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@app.command("search")
|
|
291
|
+
def search_cmd(
|
|
292
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
293
|
+
limit: int = typer.Option(10, "--limit", "-n"),
|
|
294
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
295
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
296
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
297
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
298
|
+
):
|
|
299
|
+
"""Search knowledge nodes by keyword."""
|
|
300
|
+
data = _kg_get(remote, team, branch, "/api/knowledge/search",
|
|
301
|
+
params={"q": query, "limit": limit,
|
|
302
|
+
"team_id": team, "branch": branch})
|
|
303
|
+
nodes = data if isinstance(data, list) else data.get("results", [])
|
|
304
|
+
|
|
305
|
+
if output_json:
|
|
306
|
+
typer.echo(_json.dumps(nodes, indent=2, default=str))
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if not nodes:
|
|
310
|
+
typer.echo(f"No results for '{query}'.")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
_print_node_table(nodes, title=f"Results for '{query}'")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@app.command("delete")
|
|
317
|
+
def delete_cmd(
|
|
318
|
+
node_id: str = typer.Argument(..., help="Node ID to soft-delete (deactivate)"),
|
|
319
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
320
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
321
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
322
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
323
|
+
):
|
|
324
|
+
"""Soft-delete (deactivate) a knowledge node."""
|
|
325
|
+
if not yes:
|
|
326
|
+
typer.confirm(f"Deactivate node {node_id!r}?", abort=True)
|
|
327
|
+
_kg_delete(remote, team, branch, f"/api/knowledge/nodes/{node_id}")
|
|
328
|
+
typer.echo(f"Node {node_id} deactivated.")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
# Phase 1 — Human workflow: instructions, conditions, learnings
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
@app.command("set-instruction")
|
|
336
|
+
def set_instruction_cmd(
|
|
337
|
+
text: str = typer.Argument(..., help="Instruction text"),
|
|
338
|
+
priority: str = typer.Option("medium", "--priority", "-p",
|
|
339
|
+
help="Priority: critical | high | medium | low"),
|
|
340
|
+
scope: str = typer.Option("global", "--scope", "-s",
|
|
341
|
+
help="Scope: global | project:<id> | task_type:<pattern>"),
|
|
342
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
343
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
344
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
345
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
346
|
+
):
|
|
347
|
+
"""Create a human-authored instruction rule."""
|
|
348
|
+
body = {
|
|
349
|
+
"label": text,
|
|
350
|
+
"priority": priority,
|
|
351
|
+
"scope": scope,
|
|
352
|
+
"team_id": team,
|
|
353
|
+
"branch": branch,
|
|
354
|
+
}
|
|
355
|
+
result = _kg_post(remote, team, branch, "/api/knowledge/instructions", body)
|
|
356
|
+
if output_json:
|
|
357
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
358
|
+
return
|
|
359
|
+
node_id = result.get("id", result.get("node_id", "?"))
|
|
360
|
+
typer.echo(f"Instruction created: {node_id}")
|
|
361
|
+
typer.echo(f" Priority : {priority}")
|
|
362
|
+
typer.echo(f" Scope : {scope}")
|
|
363
|
+
typer.echo(f" Text : {text}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@app.command("set-condition")
|
|
367
|
+
def set_condition_cmd(
|
|
368
|
+
when: str = typer.Option(..., "--when", "-w", help="Trigger condition (if clause)"),
|
|
369
|
+
then: str = typer.Option(..., "--then", "-t", help="Action to take (then clause)"),
|
|
370
|
+
scope: str = typer.Option("global", "--scope", "-s"),
|
|
371
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
372
|
+
team: str = typer.Option("default", "--team"),
|
|
373
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
374
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
375
|
+
):
|
|
376
|
+
"""Create an if/then condition rule."""
|
|
377
|
+
body = {
|
|
378
|
+
"label": f"IF {when} THEN {then}",
|
|
379
|
+
"when_clause": when,
|
|
380
|
+
"then_clause": then,
|
|
381
|
+
"scope": scope,
|
|
382
|
+
"team_id": team,
|
|
383
|
+
"branch": branch,
|
|
384
|
+
}
|
|
385
|
+
result = _kg_post(remote, team, branch, "/api/knowledge/conditions", body)
|
|
386
|
+
if output_json:
|
|
387
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
388
|
+
return
|
|
389
|
+
node_id = result.get("id", result.get("node_id", "?"))
|
|
390
|
+
typer.echo(f"Condition created: {node_id}")
|
|
391
|
+
typer.echo(f" When : {when}")
|
|
392
|
+
typer.echo(f" Then : {then}")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@app.command("validate-learning")
|
|
396
|
+
def validate_learning_cmd(
|
|
397
|
+
learning_id: str = typer.Argument(..., help="Learning node ID"),
|
|
398
|
+
accept: bool = typer.Option(False, "--accept", "-a", help="Accept the learning"),
|
|
399
|
+
reject: bool = typer.Option(False, "--reject", "-r", help="Reject the learning"),
|
|
400
|
+
confidence: Optional[float] = typer.Option(None, "--confidence", "-c",
|
|
401
|
+
help="Override confidence (0.0–1.0, accept only)"),
|
|
402
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
403
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
404
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
405
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
406
|
+
):
|
|
407
|
+
"""Accept or reject an AI-generated learning."""
|
|
408
|
+
if not accept and not reject:
|
|
409
|
+
_err.print("[red]Specify --accept or --reject.[/red]")
|
|
410
|
+
raise typer.Exit(1)
|
|
411
|
+
if accept and reject:
|
|
412
|
+
_err.print("[red]Specify only one of --accept or --reject.[/red]")
|
|
413
|
+
raise typer.Exit(1)
|
|
414
|
+
|
|
415
|
+
body: dict = {"accepted": accept}
|
|
416
|
+
if confidence is not None and accept:
|
|
417
|
+
body["confidence"] = confidence
|
|
418
|
+
|
|
419
|
+
result = _kg_post(remote, team, branch,
|
|
420
|
+
f"/api/knowledge/learnings/{learning_id}/validate", body)
|
|
421
|
+
if output_json:
|
|
422
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
423
|
+
return
|
|
424
|
+
verdict = "accepted" if accept else "rejected"
|
|
425
|
+
typer.echo(f"Learning {learning_id} {verdict}.")
|
|
426
|
+
if accept and confidence is not None:
|
|
427
|
+
typer.echo(f" Confidence set to {confidence}")
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@app.command("override-instruction")
|
|
431
|
+
def override_instruction_cmd(
|
|
432
|
+
instruction_id: str = typer.Argument(..., help="ID of the instruction to replace"),
|
|
433
|
+
text: str = typer.Argument(..., help="New instruction text"),
|
|
434
|
+
priority: str = typer.Option("medium", "--priority", "-p"),
|
|
435
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
436
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
437
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
438
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
439
|
+
):
|
|
440
|
+
"""Replace an instruction (deactivates the original, creates a new one)."""
|
|
441
|
+
body = {
|
|
442
|
+
"label": text,
|
|
443
|
+
"priority": priority,
|
|
444
|
+
"team_id": team,
|
|
445
|
+
"branch": branch,
|
|
446
|
+
}
|
|
447
|
+
result = _kg_post(remote, team, branch,
|
|
448
|
+
f"/api/knowledge/instructions/{instruction_id}/override", body)
|
|
449
|
+
if output_json:
|
|
450
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
451
|
+
return
|
|
452
|
+
new_id = result.get("id", result.get("node_id", "?"))
|
|
453
|
+
typer.echo(f"Instruction {instruction_id} overridden → new ID: {new_id}")
|
|
454
|
+
typer.echo(f" Text : {text}")
|
|
455
|
+
typer.echo(f" Priority : {priority}")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@app.command("promote")
|
|
459
|
+
def promote_cmd(
|
|
460
|
+
learning_id: str = typer.Argument(..., help="Learning node ID to promote to Instruction"),
|
|
461
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
462
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
463
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
464
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
465
|
+
):
|
|
466
|
+
"""Promote a validated learning to an instruction."""
|
|
467
|
+
result = _kg_post(remote, team, branch,
|
|
468
|
+
f"/api/knowledge/learnings/{learning_id}/promote", {})
|
|
469
|
+
if output_json:
|
|
470
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
471
|
+
return
|
|
472
|
+
new_id = result.get("id", result.get("node_id", "?"))
|
|
473
|
+
typer.echo(f"Learning {learning_id} promoted to Instruction: {new_id}")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
# Phase 3 — Task lifecycle
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
@app.command("start-task")
|
|
481
|
+
def start_task_cmd(
|
|
482
|
+
label: str = typer.Argument(..., help="Task description"),
|
|
483
|
+
task_type: str = typer.Option("general", "--type", "-T", help="Task type / pattern key"),
|
|
484
|
+
scope: str = typer.Option("global", "--scope", "-s"),
|
|
485
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
486
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
487
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
488
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
489
|
+
):
|
|
490
|
+
"""Register a new task and load its context (instructions + conditions)."""
|
|
491
|
+
body = {
|
|
492
|
+
"label": label,
|
|
493
|
+
"task_type": task_type,
|
|
494
|
+
"scope": scope,
|
|
495
|
+
"team_id": team,
|
|
496
|
+
"branch": branch,
|
|
497
|
+
}
|
|
498
|
+
result = _kg_post(remote, team, branch, "/api/knowledge/tasks", body)
|
|
499
|
+
if output_json:
|
|
500
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
501
|
+
return
|
|
502
|
+
task_id = result.get("id", result.get("task_id", "?"))
|
|
503
|
+
typer.echo(f"Task started: {task_id}")
|
|
504
|
+
typer.echo(f" Label : {label}")
|
|
505
|
+
typer.echo(f" Type : {task_type}")
|
|
506
|
+
typer.echo(f" Scope : {scope}")
|
|
507
|
+
typer.echo(f"\nLoad context with: ravmem kg load-context {task_id}")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@app.command("complete-task")
|
|
511
|
+
def complete_task_cmd(
|
|
512
|
+
task_id: str = typer.Argument(..., help="Task node ID"),
|
|
513
|
+
outcome: str = typer.Option("", "--outcome", "-o", help="Outcome description"),
|
|
514
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
515
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
516
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
517
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
518
|
+
):
|
|
519
|
+
"""Mark a task as complete."""
|
|
520
|
+
body = {"outcome": outcome, "team_id": team, "branch": branch}
|
|
521
|
+
result = _kg_post(remote, team, branch,
|
|
522
|
+
f"/api/knowledge/tasks/{task_id}/complete", body)
|
|
523
|
+
if output_json:
|
|
524
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
525
|
+
return
|
|
526
|
+
typer.echo(f"Task {task_id} completed.")
|
|
527
|
+
if outcome:
|
|
528
|
+
typer.echo(f" Outcome: {outcome}")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@app.command("load-context")
|
|
532
|
+
def load_context_cmd(
|
|
533
|
+
task_id: str = typer.Argument(..., help="Task node ID"),
|
|
534
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
535
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
536
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
537
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
538
|
+
):
|
|
539
|
+
"""Run 6-step context resolution for a task (instructions, learnings, decisions, similar tasks)."""
|
|
540
|
+
ctx = _kg_get(remote, team, branch,
|
|
541
|
+
f"/api/knowledge/tasks/{task_id}/context",
|
|
542
|
+
params={"team_id": team, "branch": branch})
|
|
543
|
+
if output_json:
|
|
544
|
+
typer.echo(_json.dumps(ctx, indent=2, default=str))
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
def _section(title: str, nodes: list) -> None:
|
|
548
|
+
if not nodes:
|
|
549
|
+
return
|
|
550
|
+
_console.print(f"\n[bold cyan]{title}[/bold cyan] ({len(nodes)})")
|
|
551
|
+
for n in nodes:
|
|
552
|
+
label = n.get("label", n.get("text", ""))
|
|
553
|
+
nid = n.get("id", "")[:8]
|
|
554
|
+
conf = n.get("confidence")
|
|
555
|
+
conf_str = f" conf={conf:.2f}" if conf is not None else ""
|
|
556
|
+
_console.print(f" [dim]{nid}[/dim] {label}{conf_str}")
|
|
557
|
+
|
|
558
|
+
_console.rule(f"[bold]Context for task {task_id[:8]}[/bold]")
|
|
559
|
+
_section("Instructions", ctx.get("instructions", []))
|
|
560
|
+
_section("Conditions", ctx.get("conditions", []))
|
|
561
|
+
_section("Learnings", ctx.get("learnings", []))
|
|
562
|
+
_section("Decisions", ctx.get("decisions", []))
|
|
563
|
+
_section("Context nodes", ctx.get("context_nodes", []))
|
|
564
|
+
similar = ctx.get("similar_tasks", [])
|
|
565
|
+
if similar:
|
|
566
|
+
_console.print(f"\n[bold cyan]Similar past tasks[/bold cyan] ({len(similar)})")
|
|
567
|
+
for t in similar:
|
|
568
|
+
tid = t.get("id", "")[:8]
|
|
569
|
+
_console.print(f" [dim]{tid}[/dim] {t.get('label', '')}")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@app.command("log-learning")
|
|
573
|
+
def log_learning_cmd(
|
|
574
|
+
text: str = typer.Argument(..., help="Learning text"),
|
|
575
|
+
domain: str = typer.Option("", "--domain", "-d", help="Knowledge domain"),
|
|
576
|
+
task_id: Optional[str] = typer.Option(None, "--task", help="Associated task ID"),
|
|
577
|
+
confidence: float = typer.Option(0.6, "--confidence", "-c", help="Initial confidence (0.0–1.0)"),
|
|
578
|
+
scope: str = typer.Option("global", "--scope", "-s"),
|
|
579
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
580
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
581
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
582
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
583
|
+
):
|
|
584
|
+
"""Record a discovered learning."""
|
|
585
|
+
body: dict = {
|
|
586
|
+
"content": text,
|
|
587
|
+
"domain": domain,
|
|
588
|
+
"confidence": confidence,
|
|
589
|
+
"scope": scope,
|
|
590
|
+
"team_id": team,
|
|
591
|
+
"branch": branch,
|
|
592
|
+
"source": "human",
|
|
593
|
+
"agent_id": "human",
|
|
594
|
+
"task_id": task_id or "",
|
|
595
|
+
}
|
|
596
|
+
result = _kg_post(remote, team, branch, "/api/knowledge/learnings", body)
|
|
597
|
+
if output_json:
|
|
598
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
599
|
+
return
|
|
600
|
+
node_id = result.get("id", result.get("node_id", "?"))
|
|
601
|
+
typer.echo(f"Learning logged: {node_id}")
|
|
602
|
+
typer.echo(f" Text : {text}")
|
|
603
|
+
typer.echo(f" Confidence : {confidence}")
|
|
604
|
+
if domain:
|
|
605
|
+
typer.echo(f" Domain : {domain}")
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@app.command("log-decision")
|
|
609
|
+
def log_decision_cmd(
|
|
610
|
+
text: str = typer.Argument(..., help="Decision summary"),
|
|
611
|
+
rationale: str = typer.Option("", "--rationale", "-r", help="Why this decision was made"),
|
|
612
|
+
alternatives: str = typer.Option("", "--alternatives", "-a",
|
|
613
|
+
help="Comma-separated alternatives that were considered"),
|
|
614
|
+
task_id: Optional[str] = typer.Option(None, "--task", help="Associated task ID"),
|
|
615
|
+
scope: str = typer.Option("global", "--scope", "-s"),
|
|
616
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
617
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
618
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
619
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
620
|
+
):
|
|
621
|
+
"""Record an architectural or implementation decision."""
|
|
622
|
+
body: dict = {
|
|
623
|
+
"label": text,
|
|
624
|
+
"rationale": rationale,
|
|
625
|
+
"alternatives": [a.strip() for a in alternatives.split(",") if a.strip()],
|
|
626
|
+
"scope": scope,
|
|
627
|
+
"team_id": team,
|
|
628
|
+
"branch": branch,
|
|
629
|
+
"source": "human",
|
|
630
|
+
}
|
|
631
|
+
if task_id:
|
|
632
|
+
body["task_id"] = task_id
|
|
633
|
+
result = _kg_post(remote, team, branch, "/api/knowledge/decisions", body)
|
|
634
|
+
if output_json:
|
|
635
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
636
|
+
return
|
|
637
|
+
node_id = result.get("id", result.get("node_id", "?"))
|
|
638
|
+
typer.echo(f"Decision logged: {node_id}")
|
|
639
|
+
typer.echo(f" Decision : {text}")
|
|
640
|
+
if rationale:
|
|
641
|
+
typer.echo(f" Rationale : {rationale}")
|
|
642
|
+
if alternatives:
|
|
643
|
+
typer.echo(f" Alternatives : {alternatives}")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@app.command("mark-blocked")
|
|
647
|
+
def mark_blocked_cmd(
|
|
648
|
+
reason: str = typer.Argument(..., help="Description of the blocker"),
|
|
649
|
+
task_id: Optional[str] = typer.Option(None, "--task", help="Associated task ID"),
|
|
650
|
+
scope: str = typer.Option("global", "--scope", "-s"),
|
|
651
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
652
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
653
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
654
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
655
|
+
):
|
|
656
|
+
"""Create a blocker node linked to a task."""
|
|
657
|
+
body: dict = {
|
|
658
|
+
"label": reason,
|
|
659
|
+
"scope": scope,
|
|
660
|
+
"team_id": team,
|
|
661
|
+
"branch": branch,
|
|
662
|
+
"source": "human",
|
|
663
|
+
}
|
|
664
|
+
if task_id:
|
|
665
|
+
body["task_id"] = task_id
|
|
666
|
+
result = _kg_post(remote, team, branch, "/api/knowledge/blockers", body)
|
|
667
|
+
if output_json:
|
|
668
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
669
|
+
return
|
|
670
|
+
node_id = result.get("id", result.get("node_id", "?"))
|
|
671
|
+
typer.echo(f"Blocker created: {node_id}")
|
|
672
|
+
typer.echo(f" Reason : {reason}")
|
|
673
|
+
typer.echo(f"\nResolve with: ravmem kg resolve-blocker {node_id}")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
@app.command("resolve-blocker")
|
|
677
|
+
def resolve_blocker_cmd(
|
|
678
|
+
blocker_id: str = typer.Argument(..., help="Blocker node ID"),
|
|
679
|
+
decision_id: Optional[str] = typer.Option(None, "--decision", "-d",
|
|
680
|
+
help="Optional Decision node ID that resolved this blocker"),
|
|
681
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
682
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
683
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
684
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
685
|
+
):
|
|
686
|
+
"""Mark a blocker as resolved."""
|
|
687
|
+
body: dict = {"team_id": team, "branch": branch}
|
|
688
|
+
if decision_id:
|
|
689
|
+
body["decision_id"] = decision_id
|
|
690
|
+
result = _kg_post(remote, team, branch,
|
|
691
|
+
f"/api/knowledge/blockers/{blocker_id}/resolve", body)
|
|
692
|
+
if output_json:
|
|
693
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
694
|
+
return
|
|
695
|
+
typer.echo(f"Blocker {blocker_id} resolved.")
|
|
696
|
+
if decision_id:
|
|
697
|
+
typer.echo(f" Linked decision: {decision_id}")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# ---------------------------------------------------------------------------
|
|
701
|
+
# Phase 4 — Advanced / power-user
|
|
702
|
+
# ---------------------------------------------------------------------------
|
|
703
|
+
|
|
704
|
+
@app.command("update")
|
|
705
|
+
def update_cmd(
|
|
706
|
+
node_id: str = typer.Argument(..., help="Node ID to update"),
|
|
707
|
+
label: Optional[str] = typer.Option(None, "--label", "-l", help="New label/text"),
|
|
708
|
+
priority: Optional[str] = typer.Option(None, "--priority", "-p",
|
|
709
|
+
help="New priority (instruction nodes only)"),
|
|
710
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
711
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
712
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
713
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
714
|
+
):
|
|
715
|
+
"""Update a node's properties (creates a CoW copy on the current branch)."""
|
|
716
|
+
body: dict = {}
|
|
717
|
+
if label:
|
|
718
|
+
body["label"] = label
|
|
719
|
+
if priority:
|
|
720
|
+
body["priority"] = priority
|
|
721
|
+
if not body:
|
|
722
|
+
_err.print("[red]Provide at least --label or --priority.[/red]")
|
|
723
|
+
raise typer.Exit(1)
|
|
724
|
+
result = _kg_patch(remote, team, branch, f"/api/knowledge/nodes/{node_id}", body)
|
|
725
|
+
if output_json:
|
|
726
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
727
|
+
return
|
|
728
|
+
new_id = result.get("id", node_id)
|
|
729
|
+
typer.echo(f"Node updated → {new_id}")
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
@app.command("corroborate")
|
|
733
|
+
def corroborate_cmd(
|
|
734
|
+
learning_id: str = typer.Argument(..., help="Learning node ID"),
|
|
735
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
736
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
737
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
738
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
739
|
+
):
|
|
740
|
+
"""Increment evidence count and nudge confidence on a learning."""
|
|
741
|
+
result = _kg_post(remote, team, branch,
|
|
742
|
+
f"/api/knowledge/learnings/{learning_id}/corroborate", {})
|
|
743
|
+
if output_json:
|
|
744
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
745
|
+
return
|
|
746
|
+
conf = result.get("confidence")
|
|
747
|
+
ev = result.get("evidence_count")
|
|
748
|
+
typer.echo(f"Learning {learning_id} corroborated.")
|
|
749
|
+
if conf is not None:
|
|
750
|
+
typer.echo(f" Confidence : {conf:.2f}")
|
|
751
|
+
if ev is not None:
|
|
752
|
+
typer.echo(f" Evidence count: {ev}")
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@app.command("link-symbols")
|
|
756
|
+
def link_symbols_cmd(
|
|
757
|
+
node_id: str = typer.Argument(..., help="KG node ID"),
|
|
758
|
+
symbol_ids: list[str] = typer.Argument(..., help="One or more code graph Symbol IDs"),
|
|
759
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
760
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
761
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
762
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
763
|
+
):
|
|
764
|
+
"""Link a knowledge node to code graph symbols."""
|
|
765
|
+
body = {"symbol_ids": symbol_ids}
|
|
766
|
+
result = _kg_post(remote, team, branch,
|
|
767
|
+
f"/api/knowledge/nodes/{node_id}/link-symbols", body)
|
|
768
|
+
if output_json:
|
|
769
|
+
typer.echo(_json.dumps(result, indent=2, default=str))
|
|
770
|
+
return
|
|
771
|
+
typer.echo(f"Node {node_id} linked to {len(symbol_ids)} symbol(s).")
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@app.command("set-mode")
|
|
775
|
+
def set_mode_cmd(
|
|
776
|
+
mode: str = typer.Argument(..., help="Mode: hitl | auto"),
|
|
777
|
+
team: str = typer.Option("default", "--team", "-t"),
|
|
778
|
+
branch: str = typer.Option("global", "--branch", "-b"),
|
|
779
|
+
remote: str = typer.Option("", "--remote", "-r"),
|
|
780
|
+
):
|
|
781
|
+
"""Switch the knowledge graph between HITL and auto confidence modes."""
|
|
782
|
+
if mode not in ("hitl", "auto"):
|
|
783
|
+
_err.print("[red]Mode must be 'hitl' or 'auto'.[/red]")
|
|
784
|
+
raise typer.Exit(1)
|
|
785
|
+
body = {"mode": mode, "team_id": team, "branch": branch}
|
|
786
|
+
_kg_post(remote, team, branch, "/api/knowledge/config", body)
|
|
787
|
+
typer.echo(f"KG mode set to: {mode.upper()}")
|