highhxpack 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.
- highhxpack/__init__.py +106 -0
- highhxpack/__main__.py +6 -0
- highhxpack/__version__.py +3 -0
- highhxpack/cli/__init__.py +1 -0
- highhxpack/cli/commands/__init__.py +1 -0
- highhxpack/cli/commands/consolidate.py +92 -0
- highhxpack/cli/commands/export.py +34 -0
- highhxpack/cli/commands/forget.py +75 -0
- highhxpack/cli/commands/import_.py +46 -0
- highhxpack/cli/commands/init.py +84 -0
- highhxpack/cli/commands/inspect.py +99 -0
- highhxpack/cli/commands/memories.py +92 -0
- highhxpack/cli/commands/recall.py +121 -0
- highhxpack/cli/commands/remember.py +122 -0
- highhxpack/cli/commands/stats.py +48 -0
- highhxpack/cli/context.py +70 -0
- highhxpack/cli/display.py +154 -0
- highhxpack/cli/main.py +155 -0
- highhxpack/consolidation/__init__.py +39 -0
- highhxpack/consolidation/conflict.py +178 -0
- highhxpack/consolidation/deduplication.py +157 -0
- highhxpack/consolidation/importance.py +64 -0
- highhxpack/consolidation/summarization.py +109 -0
- highhxpack/core/__init__.py +6 -0
- highhxpack/core/config.py +352 -0
- highhxpack/core/lifecycle.py +42 -0
- highhxpack/core/manager.py +643 -0
- highhxpack/core/memory.py +766 -0
- highhxpack/core/transfer.py +482 -0
- highhxpack/embeddings/__init__.py +22 -0
- highhxpack/embeddings/base.py +60 -0
- highhxpack/embeddings/local.py +125 -0
- highhxpack/embeddings/providers.py +177 -0
- highhxpack/exceptions.py +115 -0
- highhxpack/extraction/__init__.py +24 -0
- highhxpack/extraction/entities.py +100 -0
- highhxpack/extraction/events.py +42 -0
- highhxpack/extraction/extractor.py +287 -0
- highhxpack/extraction/facts.py +119 -0
- highhxpack/extraction/patterns.py +104 -0
- highhxpack/extraction/preferences.py +58 -0
- highhxpack/graph/__init__.py +7 -0
- highhxpack/graph/edges.py +26 -0
- highhxpack/graph/graph.py +151 -0
- highhxpack/graph/nodes.py +36 -0
- highhxpack/integrations/__init__.py +11 -0
- highhxpack/integrations/generic.py +85 -0
- highhxpack/integrations/langchain.py +67 -0
- highhxpack/integrations/llamaindex.py +56 -0
- highhxpack/models/__init__.py +36 -0
- highhxpack/models/event.py +55 -0
- highhxpack/models/memory.py +144 -0
- highhxpack/models/relationship.py +79 -0
- highhxpack/models/result.py +227 -0
- highhxpack/models/user.py +37 -0
- highhxpack/providers/__init__.py +48 -0
- highhxpack/providers/base.py +106 -0
- highhxpack/providers/custom.py +47 -0
- highhxpack/providers/ollama.py +72 -0
- highhxpack/providers/openai.py +91 -0
- highhxpack/py.typed +0 -0
- highhxpack/retrieval/__init__.py +7 -0
- highhxpack/retrieval/filters.py +147 -0
- highhxpack/retrieval/hybrid.py +24 -0
- highhxpack/retrieval/keyword.py +63 -0
- highhxpack/retrieval/ranking.py +174 -0
- highhxpack/retrieval/retriever.py +143 -0
- highhxpack/retrieval/semantic.py +36 -0
- highhxpack/storage/__init__.py +6 -0
- highhxpack/storage/base.py +210 -0
- highhxpack/storage/migrations.py +188 -0
- highhxpack/storage/sqlite.py +934 -0
- highhxpack/storage/vector.py +87 -0
- highhxpack/utils/__init__.py +1 -0
- highhxpack/utils/hashing.py +31 -0
- highhxpack/utils/logging.py +52 -0
- highhxpack/utils/text.py +117 -0
- highhxpack/utils/timestamps.py +78 -0
- highhxpack/utils/validation.py +179 -0
- highhxpack-0.1.0.dist-info/METADATA +297 -0
- highhxpack-0.1.0.dist-info/RECORD +84 -0
- highhxpack-0.1.0.dist-info/WHEEL +4 -0
- highhxpack-0.1.0.dist-info/entry_points.txt +2 -0
- highhxpack-0.1.0.dist-info/licenses/LICENSE +21 -0
highhxpack/__init__.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""HighHXPack: a local-first memory and context engine for AI applications.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from highhxpack import Memory
|
|
6
|
+
|
|
7
|
+
memory = Memory()
|
|
8
|
+
memory.remember("user_123", "I prefer Python for AI development.")
|
|
9
|
+
for result in memory.recall("user_123", "What language do I prefer for AI?"):
|
|
10
|
+
print(result.score, result.content)
|
|
11
|
+
|
|
12
|
+
Everything importable from this module is public API and follows semantic
|
|
13
|
+
versioning. Submodules are internal unless documented otherwise.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from highhxpack.__version__ import __version__
|
|
17
|
+
from highhxpack.consolidation.conflict import ConflictPolicy
|
|
18
|
+
from highhxpack.core.config import Config
|
|
19
|
+
from highhxpack.core.memory import Memory
|
|
20
|
+
from highhxpack.embeddings.base import EmbeddingProvider
|
|
21
|
+
from highhxpack.embeddings.local import HashingEmbedding
|
|
22
|
+
from highhxpack.embeddings.providers import CallableEmbedding
|
|
23
|
+
from highhxpack.exceptions import (
|
|
24
|
+
ConfigurationError,
|
|
25
|
+
ConflictNotFoundError,
|
|
26
|
+
EmbeddingError,
|
|
27
|
+
ExportError,
|
|
28
|
+
ExtractionError,
|
|
29
|
+
HighHXPackError,
|
|
30
|
+
MemoryImportError,
|
|
31
|
+
MemoryNotFoundError,
|
|
32
|
+
MigrationError,
|
|
33
|
+
ProviderError,
|
|
34
|
+
ProviderNotAvailableError,
|
|
35
|
+
StorageError,
|
|
36
|
+
ValidationError,
|
|
37
|
+
)
|
|
38
|
+
from highhxpack.models import (
|
|
39
|
+
Conflict,
|
|
40
|
+
ConflictStatus,
|
|
41
|
+
ConsolidationReport,
|
|
42
|
+
EventKind,
|
|
43
|
+
ExportReport,
|
|
44
|
+
ImportReport,
|
|
45
|
+
MemoryEvent,
|
|
46
|
+
MemoryRecord,
|
|
47
|
+
MemoryStats,
|
|
48
|
+
MemoryStatus,
|
|
49
|
+
MemoryType,
|
|
50
|
+
RecallResult,
|
|
51
|
+
Relationship,
|
|
52
|
+
RememberAction,
|
|
53
|
+
RememberResult,
|
|
54
|
+
ScoreBreakdown,
|
|
55
|
+
UserSummary,
|
|
56
|
+
)
|
|
57
|
+
from highhxpack.providers.base import LLMProvider
|
|
58
|
+
from highhxpack.providers.custom import CallableProvider
|
|
59
|
+
from highhxpack.retrieval.ranking import ScoringConfig
|
|
60
|
+
from highhxpack.retrieval.retriever import Strategy
|
|
61
|
+
from highhxpack.storage.base import StorageBackend
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"CallableEmbedding",
|
|
65
|
+
"CallableProvider",
|
|
66
|
+
"Config",
|
|
67
|
+
"ConfigurationError",
|
|
68
|
+
"Conflict",
|
|
69
|
+
"ConflictNotFoundError",
|
|
70
|
+
"ConflictPolicy",
|
|
71
|
+
"ConflictStatus",
|
|
72
|
+
"ConsolidationReport",
|
|
73
|
+
"EmbeddingError",
|
|
74
|
+
"EmbeddingProvider",
|
|
75
|
+
"EventKind",
|
|
76
|
+
"ExportError",
|
|
77
|
+
"ExportReport",
|
|
78
|
+
"ExtractionError",
|
|
79
|
+
"HashingEmbedding",
|
|
80
|
+
"HighHXPackError",
|
|
81
|
+
"ImportReport",
|
|
82
|
+
"LLMProvider",
|
|
83
|
+
"Memory",
|
|
84
|
+
"MemoryEvent",
|
|
85
|
+
"MemoryImportError",
|
|
86
|
+
"MemoryNotFoundError",
|
|
87
|
+
"MemoryRecord",
|
|
88
|
+
"MemoryStats",
|
|
89
|
+
"MemoryStatus",
|
|
90
|
+
"MemoryType",
|
|
91
|
+
"MigrationError",
|
|
92
|
+
"ProviderError",
|
|
93
|
+
"ProviderNotAvailableError",
|
|
94
|
+
"RecallResult",
|
|
95
|
+
"Relationship",
|
|
96
|
+
"RememberAction",
|
|
97
|
+
"RememberResult",
|
|
98
|
+
"ScoreBreakdown",
|
|
99
|
+
"ScoringConfig",
|
|
100
|
+
"StorageBackend",
|
|
101
|
+
"StorageError",
|
|
102
|
+
"Strategy",
|
|
103
|
+
"UserSummary",
|
|
104
|
+
"ValidationError",
|
|
105
|
+
"__version__",
|
|
106
|
+
]
|
highhxpack/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command-line interface. The entry point is :func:`highhxpack.cli.main.main`."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI subcommands. Each module exposes ``register(subparsers, common)``."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""``highhxpack consolidate``, ``highhxpack conflicts`` and ``highhxpack resolve``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from highhxpack.cli.context import EXIT_OK, Context
|
|
9
|
+
from highhxpack.cli.display import format_time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
13
|
+
consolidate = subparsers.add_parser(
|
|
14
|
+
"consolidate",
|
|
15
|
+
parents=[common],
|
|
16
|
+
help="merge duplicates, archive expired memories and apply the conflict policy",
|
|
17
|
+
)
|
|
18
|
+
consolidate.add_argument("--dry-run", action="store_true", help="show what would change")
|
|
19
|
+
consolidate.add_argument(
|
|
20
|
+
"--summarize", action="store_true", help="also add summaries of related memories"
|
|
21
|
+
)
|
|
22
|
+
consolidate.set_defaults(handler=run_consolidate)
|
|
23
|
+
|
|
24
|
+
conflicts = subparsers.add_parser(
|
|
25
|
+
"conflicts", parents=[common], help="list contradictory memories"
|
|
26
|
+
)
|
|
27
|
+
conflicts.add_argument("--all", action="store_true", help="include resolved conflicts")
|
|
28
|
+
conflicts.set_defaults(handler=run_conflicts)
|
|
29
|
+
|
|
30
|
+
resolve = subparsers.add_parser(
|
|
31
|
+
"resolve", parents=[common], help="resolve a conflict by choosing the memory to keep"
|
|
32
|
+
)
|
|
33
|
+
resolve.add_argument("conflict_id", help="conflict id (from `highhxpack conflicts`)")
|
|
34
|
+
resolve.add_argument("--keep", required=True, help="id or prefix of the memory to keep")
|
|
35
|
+
resolve.set_defaults(handler=run_resolve)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_consolidate(args: argparse.Namespace, ctx: Context) -> int:
|
|
39
|
+
report = ctx.memory.consolidate(ctx.user, dry_run=args.dry_run, summarize=args.summarize)
|
|
40
|
+
console = ctx.console
|
|
41
|
+
if console.json_mode:
|
|
42
|
+
console.json(report.to_dict())
|
|
43
|
+
return EXIT_OK
|
|
44
|
+
if not report.actions:
|
|
45
|
+
console.info("Nothing to consolidate.")
|
|
46
|
+
return EXIT_OK
|
|
47
|
+
prefix = "Would " if report.dry_run else ""
|
|
48
|
+
for action in report.actions:
|
|
49
|
+
console.info(f" {prefix}{action}" if prefix else f" {action}")
|
|
50
|
+
console.success(
|
|
51
|
+
f"{'Planned' if report.dry_run else 'Done'}: {len(report.merged)} merged, "
|
|
52
|
+
f"{len(report.superseded)} superseded, {report.conflicts_detected} conflicts, "
|
|
53
|
+
f"{len(report.expired_archived)} expired archived, "
|
|
54
|
+
f"{len(report.summaries_created)} summaries."
|
|
55
|
+
)
|
|
56
|
+
return EXIT_OK
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run_conflicts(args: argparse.Namespace, ctx: Context) -> int:
|
|
60
|
+
memory = ctx.memory
|
|
61
|
+
conflicts = memory.conflicts(ctx.explicit_user, status=None if args.all else "open")
|
|
62
|
+
console = ctx.console
|
|
63
|
+
if console.json_mode:
|
|
64
|
+
console.json([c.to_dict() for c in conflicts])
|
|
65
|
+
return EXIT_OK
|
|
66
|
+
if not conflicts:
|
|
67
|
+
console.info("No conflicts." if args.all else "No open conflicts.")
|
|
68
|
+
return EXIT_OK
|
|
69
|
+
records = memory.storage.get_memories([i for c in conflicts for i in (c.memory_id, c.other_id)])
|
|
70
|
+
for c in conflicts:
|
|
71
|
+
newer, older = records.get(c.memory_id), records.get(c.other_id)
|
|
72
|
+
console.print(
|
|
73
|
+
console.style(f"{c.id}", "bold") + f" {c.kind} {c.status} "
|
|
74
|
+
f"{format_time(c.created_at)} user={c.user_id}"
|
|
75
|
+
)
|
|
76
|
+
if newer:
|
|
77
|
+
console.print(f" newer {newer.short_id} [{newer.status}]: {newer.content}")
|
|
78
|
+
if older:
|
|
79
|
+
console.print(f" older {older.short_id} [{older.status}]: {older.content}")
|
|
80
|
+
if c.resolution:
|
|
81
|
+
console.print(console.style(f" {c.resolution}", "dim"))
|
|
82
|
+
return EXIT_OK
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_resolve(args: argparse.Namespace, ctx: Context) -> int:
|
|
86
|
+
memory = ctx.memory
|
|
87
|
+
conflict = memory.resolve_conflict(args.conflict_id, keep=memory.resolve_id(args.keep))
|
|
88
|
+
if ctx.console.json_mode:
|
|
89
|
+
ctx.console.json(conflict.to_dict())
|
|
90
|
+
else:
|
|
91
|
+
ctx.console.success(f"Resolved: {conflict.resolution}")
|
|
92
|
+
return EXIT_OK
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""``highhxpack export``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from highhxpack.cli.context import EXIT_OK, Context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
12
|
+
parser = subparsers.add_parser(
|
|
13
|
+
"export",
|
|
14
|
+
parents=[common],
|
|
15
|
+
help="export memories to a JSON file (all users unless --user is given)",
|
|
16
|
+
description="Write memories, history, relationships and conflicts to a JSON file "
|
|
17
|
+
"readable only by you.",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("file", help="destination file, e.g. memories.json")
|
|
20
|
+
parser.add_argument("-f", "--force", action="store_true", help="overwrite an existing file")
|
|
21
|
+
parser.set_defaults(handler=run)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(args: argparse.Namespace, ctx: Context) -> int:
|
|
25
|
+
report = ctx.memory.export(args.file, user_id=ctx.explicit_user, overwrite=args.force)
|
|
26
|
+
if ctx.console.json_mode:
|
|
27
|
+
ctx.console.json(report.to_dict())
|
|
28
|
+
else:
|
|
29
|
+
ctx.console.success(
|
|
30
|
+
f"Exported {report.memories} memories, {report.events} history events, "
|
|
31
|
+
f"{report.relationships} relationships and {report.conflicts} conflicts "
|
|
32
|
+
f"to {report.path}"
|
|
33
|
+
)
|
|
34
|
+
return EXIT_OK
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""``highhxpack forget`` and ``highhxpack restore``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from highhxpack.cli.context import EXIT_ERROR, EXIT_OK, Context
|
|
10
|
+
from highhxpack.exceptions import ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
14
|
+
forget = subparsers.add_parser(
|
|
15
|
+
"forget",
|
|
16
|
+
parents=[common],
|
|
17
|
+
help="archive (or permanently delete) a memory",
|
|
18
|
+
description="Archive a memory so it is no longer recalled; it stays in history and "
|
|
19
|
+
"can be restored. With --permanent the memory and its history are erased.",
|
|
20
|
+
)
|
|
21
|
+
forget.add_argument("memory_id", help="memory id or unique prefix (at least 4 characters)")
|
|
22
|
+
forget.add_argument("--permanent", action="store_true", help="erase instead of archiving")
|
|
23
|
+
forget.add_argument("-y", "--yes", action="store_true", help="do not ask for confirmation")
|
|
24
|
+
forget.set_defaults(handler=run_forget)
|
|
25
|
+
|
|
26
|
+
restore = subparsers.add_parser(
|
|
27
|
+
"restore",
|
|
28
|
+
parents=[common],
|
|
29
|
+
help="make an archived or superseded memory active again",
|
|
30
|
+
)
|
|
31
|
+
restore.add_argument("memory_id", help="memory id or unique prefix")
|
|
32
|
+
restore.set_defaults(handler=run_restore)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_forget(args: argparse.Namespace, ctx: Context) -> int:
|
|
36
|
+
memory = ctx.memory
|
|
37
|
+
memory_id = memory.resolve_id(args.memory_id)
|
|
38
|
+
record = memory.get(memory_id)
|
|
39
|
+
if args.permanent and not args.yes:
|
|
40
|
+
if not sys.stdin.isatty() or ctx.console.json_mode:
|
|
41
|
+
# Never erase data without an explicit confirmation.
|
|
42
|
+
raise ValidationError(
|
|
43
|
+
"Refusing to permanently erase a memory without confirmation.",
|
|
44
|
+
hint="Pass --yes to confirm when running non-interactively.",
|
|
45
|
+
)
|
|
46
|
+
answer = input(f"Permanently erase {record.short_id} ({record.content!r})? [y/N] ")
|
|
47
|
+
if answer.strip().lower() not in {"y", "yes"}:
|
|
48
|
+
ctx.console.info("Cancelled.")
|
|
49
|
+
return EXIT_ERROR
|
|
50
|
+
if args.permanent:
|
|
51
|
+
memory.delete(memory_id)
|
|
52
|
+
if ctx.console.json_mode:
|
|
53
|
+
ctx.console.json({"id": memory_id, "deleted": True})
|
|
54
|
+
else:
|
|
55
|
+
ctx.console.success(f"Erased {record.short_id}.")
|
|
56
|
+
return EXIT_OK
|
|
57
|
+
archived = memory.forget(memory_id)
|
|
58
|
+
if ctx.console.json_mode:
|
|
59
|
+
ctx.console.json(archived.to_dict())
|
|
60
|
+
else:
|
|
61
|
+
ctx.console.success(
|
|
62
|
+
f"Forgot {archived.short_id}. It is archived; undo with "
|
|
63
|
+
f"`highhxpack restore {archived.short_id}`."
|
|
64
|
+
)
|
|
65
|
+
return EXIT_OK
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_restore(args: argparse.Namespace, ctx: Context) -> int:
|
|
69
|
+
memory = ctx.memory
|
|
70
|
+
restored = memory.restore(memory.resolve_id(args.memory_id))
|
|
71
|
+
if ctx.console.json_mode:
|
|
72
|
+
ctx.console.json(restored.to_dict())
|
|
73
|
+
else:
|
|
74
|
+
ctx.console.success(f"Restored {restored.short_id}: {restored.content}")
|
|
75
|
+
return EXIT_OK
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""``highhxpack import``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from highhxpack.cli.context import EXIT_OK, Context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
12
|
+
parser = subparsers.add_parser(
|
|
13
|
+
"import",
|
|
14
|
+
parents=[common],
|
|
15
|
+
help="import memories from a JSON export",
|
|
16
|
+
description="Import a file created by `highhxpack export`. Every record is validated; "
|
|
17
|
+
"invalid records are skipped and reported. With --user, all imported data is assigned "
|
|
18
|
+
"to that user.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("file", help="file created by `highhxpack export`")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--replace", action="store_true", help="replace memories whose id already exists"
|
|
23
|
+
)
|
|
24
|
+
parser.set_defaults(handler=run)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(args: argparse.Namespace, ctx: Context) -> int:
|
|
28
|
+
report = ctx.memory.import_(
|
|
29
|
+
args.file,
|
|
30
|
+
user_id=ctx.explicit_user,
|
|
31
|
+
on_existing="replace" if args.replace else "skip",
|
|
32
|
+
)
|
|
33
|
+
console = ctx.console
|
|
34
|
+
if console.json_mode:
|
|
35
|
+
console.json(report.to_dict())
|
|
36
|
+
return EXIT_OK
|
|
37
|
+
console.success(
|
|
38
|
+
f"Imported {report.imported} memories ({report.replaced} replaced, "
|
|
39
|
+
f"{report.skipped} skipped), {report.events} history events and "
|
|
40
|
+
f"{report.relationships} relationships."
|
|
41
|
+
)
|
|
42
|
+
for error in report.errors[:20]:
|
|
43
|
+
console.warn(error)
|
|
44
|
+
if len(report.errors) > 20:
|
|
45
|
+
console.warn(f"{console.sym('more')} {len(report.errors) - 20} more problems (use --json)")
|
|
46
|
+
return EXIT_OK
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""``highhxpack init`` and ``highhxpack config``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from highhxpack.cli.context import EXIT_OK, Context
|
|
10
|
+
from highhxpack.core.config import CONFIG_FILENAME, write_config_template
|
|
11
|
+
from highhxpack.utils.validation import validate_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
15
|
+
init = subparsers.add_parser(
|
|
16
|
+
"init",
|
|
17
|
+
parents=[common],
|
|
18
|
+
help="create the data directory, configuration file and database",
|
|
19
|
+
description="Create the data directory, a commented configuration file and the "
|
|
20
|
+
"database. Safe to run more than once.",
|
|
21
|
+
)
|
|
22
|
+
init.add_argument("--force", action="store_true", help="overwrite an existing config file")
|
|
23
|
+
init.set_defaults(handler=run_init)
|
|
24
|
+
|
|
25
|
+
config = subparsers.add_parser(
|
|
26
|
+
"config",
|
|
27
|
+
parents=[common],
|
|
28
|
+
help="show the resolved configuration",
|
|
29
|
+
description="Show the effective configuration after applying the config file, "
|
|
30
|
+
"environment variables and command-line options. Secrets are masked.",
|
|
31
|
+
)
|
|
32
|
+
config.set_defaults(handler=run_config)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_init(args: argparse.Namespace, ctx: Context) -> int:
|
|
36
|
+
created_config = False
|
|
37
|
+
# A config file requested with --config / $HIGHHXPACK_CONFIG may not exist yet:
|
|
38
|
+
# creating it is exactly what `init` is for.
|
|
39
|
+
requested = getattr(args, "config", None) or os.environ.get("HIGHHXPACK_CONFIG")
|
|
40
|
+
if requested:
|
|
41
|
+
target = validate_path(requested, field="config")
|
|
42
|
+
if not target.exists() or args.force:
|
|
43
|
+
write_config_template(target, overwrite=args.force)
|
|
44
|
+
created_config = True
|
|
45
|
+
config = ctx.config
|
|
46
|
+
config_path = config.config_file or config.home / CONFIG_FILENAME
|
|
47
|
+
if not requested and (not config_path.exists() or args.force):
|
|
48
|
+
write_config_template(config_path, overwrite=args.force)
|
|
49
|
+
created_config = True
|
|
50
|
+
memory = ctx.memory # creates the database and applies migrations
|
|
51
|
+
data = {
|
|
52
|
+
"home": str(config.home),
|
|
53
|
+
"config_file": str(config_path),
|
|
54
|
+
"config_created": created_config,
|
|
55
|
+
"storage_path": memory.location,
|
|
56
|
+
"schema_version": memory.storage.schema_version,
|
|
57
|
+
"default_user": config.default_user,
|
|
58
|
+
}
|
|
59
|
+
if ctx.console.json_mode:
|
|
60
|
+
ctx.console.json(data)
|
|
61
|
+
return EXIT_OK
|
|
62
|
+
console = ctx.console
|
|
63
|
+
console.success("HighHXPack is ready.")
|
|
64
|
+
console.print(f" config: {config_path}" + ("" if created_config else " (existing, kept)"))
|
|
65
|
+
console.print(f" database: {memory.location}")
|
|
66
|
+
console.print(f" user: {config.default_user}")
|
|
67
|
+
console.print()
|
|
68
|
+
console.print('Try: highhxpack remember "I prefer Python for AI development"')
|
|
69
|
+
console.print(' highhxpack recall "programming language"')
|
|
70
|
+
return EXIT_OK
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_config(args: argparse.Namespace, ctx: Context) -> int:
|
|
74
|
+
data = ctx.config.to_dict(redact=True)
|
|
75
|
+
if ctx.console.json_mode:
|
|
76
|
+
ctx.console.json(data)
|
|
77
|
+
return EXIT_OK
|
|
78
|
+
scoring = data.pop("scoring")
|
|
79
|
+
for key, value in data.items():
|
|
80
|
+
ctx.console.print(f"{key:<18} {value if value is not None else '-'}")
|
|
81
|
+
ctx.console.print(ctx.console.style("scoring", "bold"))
|
|
82
|
+
for key, value in scoring.items():
|
|
83
|
+
ctx.console.print(f" {key:<24} {value}")
|
|
84
|
+
return EXIT_OK
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""``highhxpack inspect`` (a user) and ``highhxpack history`` (a memory)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from highhxpack.cli.context import EXIT_OK, Context
|
|
9
|
+
from highhxpack.cli.display import format_time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
13
|
+
inspect = subparsers.add_parser(
|
|
14
|
+
"inspect",
|
|
15
|
+
parents=[common],
|
|
16
|
+
help="summarize everything stored for a user",
|
|
17
|
+
)
|
|
18
|
+
inspect.add_argument("user_id", nargs="?", help="user to inspect (default: current user)")
|
|
19
|
+
inspect.set_defaults(handler=run_inspect)
|
|
20
|
+
|
|
21
|
+
history = subparsers.add_parser(
|
|
22
|
+
"history",
|
|
23
|
+
aliases=["show"],
|
|
24
|
+
parents=[common],
|
|
25
|
+
help="show a memory and its change history",
|
|
26
|
+
)
|
|
27
|
+
history.add_argument("memory_id", help="memory id or unique prefix")
|
|
28
|
+
history.set_defaults(handler=run_history)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_inspect(args: argparse.Namespace, ctx: Context) -> int:
|
|
32
|
+
memory = ctx.memory
|
|
33
|
+
user = args.user_id or ctx.user
|
|
34
|
+
summary = memory.inspect(user)
|
|
35
|
+
edges = memory.graph.edges(user)
|
|
36
|
+
console = ctx.console
|
|
37
|
+
if console.json_mode:
|
|
38
|
+
console.json({**summary.to_dict(), "graph": [e.to_dict() for e in edges]})
|
|
39
|
+
return EXIT_OK
|
|
40
|
+
console.print(console.style(f"User {summary.user_id}", "bold"))
|
|
41
|
+
console.print(f" memories: {summary.active} active / {summary.total} total")
|
|
42
|
+
if summary.by_type:
|
|
43
|
+
types = ", ".join(f"{k} {v}" for k, v in summary.by_type.items())
|
|
44
|
+
console.print(f" by type: {types}")
|
|
45
|
+
if summary.by_status:
|
|
46
|
+
statuses = ", ".join(f"{k} {v}" for k, v in summary.by_status.items())
|
|
47
|
+
console.print(f" by status: {statuses}")
|
|
48
|
+
console.print(f" conflicts: {summary.open_conflicts} open")
|
|
49
|
+
console.print(
|
|
50
|
+
f" first/last: {format_time(summary.first_memory_at)} / "
|
|
51
|
+
f"{format_time(summary.last_memory_at)}"
|
|
52
|
+
)
|
|
53
|
+
if edges:
|
|
54
|
+
console.print(console.style("Knowledge graph", "bold"))
|
|
55
|
+
for edge in edges[:50]:
|
|
56
|
+
arrow = f"{console.sym('dash')}{edge.relation}{console.sym('arrow')}"
|
|
57
|
+
console.print(f" {edge.source} {arrow} {edge.target}")
|
|
58
|
+
if len(edges) > 50:
|
|
59
|
+
more = f" {console.sym('more')} {len(edges) - 50} more (use --json)"
|
|
60
|
+
console.print(console.style(more, "dim"))
|
|
61
|
+
return EXIT_OK
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_history(args: argparse.Namespace, ctx: Context) -> int:
|
|
65
|
+
memory = ctx.memory
|
|
66
|
+
record = memory.get(memory.resolve_id(args.memory_id))
|
|
67
|
+
events = memory.history(record.id)
|
|
68
|
+
console = ctx.console
|
|
69
|
+
if console.json_mode:
|
|
70
|
+
console.json({"memory": record.to_dict(), "history": [e.to_dict() for e in events]})
|
|
71
|
+
return EXIT_OK
|
|
72
|
+
console.print(console.style(record.id, "bold"))
|
|
73
|
+
console.print(f" content: {record.content}")
|
|
74
|
+
console.print(f" user: {record.user_id}")
|
|
75
|
+
console.print(f" type/status: {record.memory_type} / {record.status}")
|
|
76
|
+
console.print(f" importance: {record.importance:.2f} confidence: {record.confidence:.2f}")
|
|
77
|
+
console.print(
|
|
78
|
+
f" version: {record.version} mentions: {record.mention_count} "
|
|
79
|
+
f"recalls: {record.access_count}"
|
|
80
|
+
)
|
|
81
|
+
console.print(
|
|
82
|
+
f" created: {format_time(record.created_at)} "
|
|
83
|
+
f"updated: {format_time(record.updated_at)}"
|
|
84
|
+
)
|
|
85
|
+
if record.expires_at:
|
|
86
|
+
console.print(f" expires: {format_time(record.expires_at)}")
|
|
87
|
+
if record.superseded_by:
|
|
88
|
+
console.print(f" replaced by: {record.superseded_by}")
|
|
89
|
+
if record.metadata:
|
|
90
|
+
console.print(f" metadata: {record.metadata}")
|
|
91
|
+
console.print(console.style("History", "bold"))
|
|
92
|
+
for event in events:
|
|
93
|
+
extra = f" {console.sym('arrow')} {event.related_id[:8]}" if event.related_id else ""
|
|
94
|
+
reason = f" ({event.reason})" if event.reason else ""
|
|
95
|
+
console.print(
|
|
96
|
+
f" {format_time(event.created_at)} v{event.version} {event.kind}{extra}{reason}"
|
|
97
|
+
)
|
|
98
|
+
console.print(console.style(f" {event.content}", "dim"))
|
|
99
|
+
return EXIT_OK
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""``highhxpack memories``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from highhxpack.cli.context import EXIT_OK, Context
|
|
9
|
+
from highhxpack.cli.display import format_time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(subparsers: Any, common: argparse.ArgumentParser) -> None:
|
|
13
|
+
parser = subparsers.add_parser(
|
|
14
|
+
"memories",
|
|
15
|
+
aliases=["list"],
|
|
16
|
+
parents=[common],
|
|
17
|
+
help="list stored memories",
|
|
18
|
+
description="List the current user's memories (newest first).",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("-t", "--type", dest="memory_type", action="append", help="only this type")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--status",
|
|
23
|
+
default="active",
|
|
24
|
+
help="active, superseded, merged, archived or any (default: active)",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("-n", "--limit", type=int, default=50, help="maximum rows (default: 50)")
|
|
27
|
+
parser.add_argument("--offset", type=int, default=0, help="skip this many rows")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--order",
|
|
30
|
+
choices=("newest", "oldest", "importance", "updated"),
|
|
31
|
+
default="newest",
|
|
32
|
+
help="sort order (default: newest)",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument("--all-users", action="store_true", help="list memories of every user")
|
|
35
|
+
parser.add_argument("--include-expired", action="store_true", help="include expired memories")
|
|
36
|
+
parser.set_defaults(handler=run)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run(args: argparse.Namespace, ctx: Context) -> int:
|
|
40
|
+
memory = ctx.memory
|
|
41
|
+
user = None if args.all_users else ctx.user
|
|
42
|
+
records = memory.list(
|
|
43
|
+
user,
|
|
44
|
+
memory_type=args.memory_type,
|
|
45
|
+
status=args.status,
|
|
46
|
+
limit=args.limit,
|
|
47
|
+
offset=args.offset,
|
|
48
|
+
order=args.order,
|
|
49
|
+
include_expired=args.include_expired,
|
|
50
|
+
)
|
|
51
|
+
console = ctx.console
|
|
52
|
+
if console.json_mode:
|
|
53
|
+
console.json([r.to_dict() for r in records])
|
|
54
|
+
return EXIT_OK
|
|
55
|
+
if not records:
|
|
56
|
+
scope = "any user" if user is None else f"user {user!r}"
|
|
57
|
+
console.info(f'No memories for {scope}. Add one with: highhxpack remember "..."')
|
|
58
|
+
return EXIT_OK
|
|
59
|
+
show_user = user is None
|
|
60
|
+
show_status = args.status != "active"
|
|
61
|
+
headers = [
|
|
62
|
+
"ID",
|
|
63
|
+
*(["USER"] if show_user else []),
|
|
64
|
+
"TYPE",
|
|
65
|
+
*(["STATUS"] if show_status else []),
|
|
66
|
+
"IMP",
|
|
67
|
+
"CREATED",
|
|
68
|
+
"CONTENT",
|
|
69
|
+
]
|
|
70
|
+
rows = [
|
|
71
|
+
[
|
|
72
|
+
r.short_id,
|
|
73
|
+
*([r.user_id] if show_user else []),
|
|
74
|
+
r.memory_type,
|
|
75
|
+
*([r.status] if show_status else []),
|
|
76
|
+
f"{r.importance:.2f}",
|
|
77
|
+
format_time(r.created_at),
|
|
78
|
+
r.content,
|
|
79
|
+
]
|
|
80
|
+
for r in records
|
|
81
|
+
]
|
|
82
|
+
console.table(headers, rows, flex=len(headers) - 1)
|
|
83
|
+
total = memory.count(
|
|
84
|
+
user, memory_type=args.memory_type, status=args.status, include_expired=args.include_expired
|
|
85
|
+
)
|
|
86
|
+
if total > args.offset + len(records):
|
|
87
|
+
console.info(
|
|
88
|
+
console.style(
|
|
89
|
+
f"Showing {len(records)} of {total}. Use --offset/--limit to see more.", "dim"
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
return EXIT_OK
|