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.
Files changed (84) hide show
  1. highhxpack/__init__.py +106 -0
  2. highhxpack/__main__.py +6 -0
  3. highhxpack/__version__.py +3 -0
  4. highhxpack/cli/__init__.py +1 -0
  5. highhxpack/cli/commands/__init__.py +1 -0
  6. highhxpack/cli/commands/consolidate.py +92 -0
  7. highhxpack/cli/commands/export.py +34 -0
  8. highhxpack/cli/commands/forget.py +75 -0
  9. highhxpack/cli/commands/import_.py +46 -0
  10. highhxpack/cli/commands/init.py +84 -0
  11. highhxpack/cli/commands/inspect.py +99 -0
  12. highhxpack/cli/commands/memories.py +92 -0
  13. highhxpack/cli/commands/recall.py +121 -0
  14. highhxpack/cli/commands/remember.py +122 -0
  15. highhxpack/cli/commands/stats.py +48 -0
  16. highhxpack/cli/context.py +70 -0
  17. highhxpack/cli/display.py +154 -0
  18. highhxpack/cli/main.py +155 -0
  19. highhxpack/consolidation/__init__.py +39 -0
  20. highhxpack/consolidation/conflict.py +178 -0
  21. highhxpack/consolidation/deduplication.py +157 -0
  22. highhxpack/consolidation/importance.py +64 -0
  23. highhxpack/consolidation/summarization.py +109 -0
  24. highhxpack/core/__init__.py +6 -0
  25. highhxpack/core/config.py +352 -0
  26. highhxpack/core/lifecycle.py +42 -0
  27. highhxpack/core/manager.py +643 -0
  28. highhxpack/core/memory.py +766 -0
  29. highhxpack/core/transfer.py +482 -0
  30. highhxpack/embeddings/__init__.py +22 -0
  31. highhxpack/embeddings/base.py +60 -0
  32. highhxpack/embeddings/local.py +125 -0
  33. highhxpack/embeddings/providers.py +177 -0
  34. highhxpack/exceptions.py +115 -0
  35. highhxpack/extraction/__init__.py +24 -0
  36. highhxpack/extraction/entities.py +100 -0
  37. highhxpack/extraction/events.py +42 -0
  38. highhxpack/extraction/extractor.py +287 -0
  39. highhxpack/extraction/facts.py +119 -0
  40. highhxpack/extraction/patterns.py +104 -0
  41. highhxpack/extraction/preferences.py +58 -0
  42. highhxpack/graph/__init__.py +7 -0
  43. highhxpack/graph/edges.py +26 -0
  44. highhxpack/graph/graph.py +151 -0
  45. highhxpack/graph/nodes.py +36 -0
  46. highhxpack/integrations/__init__.py +11 -0
  47. highhxpack/integrations/generic.py +85 -0
  48. highhxpack/integrations/langchain.py +67 -0
  49. highhxpack/integrations/llamaindex.py +56 -0
  50. highhxpack/models/__init__.py +36 -0
  51. highhxpack/models/event.py +55 -0
  52. highhxpack/models/memory.py +144 -0
  53. highhxpack/models/relationship.py +79 -0
  54. highhxpack/models/result.py +227 -0
  55. highhxpack/models/user.py +37 -0
  56. highhxpack/providers/__init__.py +48 -0
  57. highhxpack/providers/base.py +106 -0
  58. highhxpack/providers/custom.py +47 -0
  59. highhxpack/providers/ollama.py +72 -0
  60. highhxpack/providers/openai.py +91 -0
  61. highhxpack/py.typed +0 -0
  62. highhxpack/retrieval/__init__.py +7 -0
  63. highhxpack/retrieval/filters.py +147 -0
  64. highhxpack/retrieval/hybrid.py +24 -0
  65. highhxpack/retrieval/keyword.py +63 -0
  66. highhxpack/retrieval/ranking.py +174 -0
  67. highhxpack/retrieval/retriever.py +143 -0
  68. highhxpack/retrieval/semantic.py +36 -0
  69. highhxpack/storage/__init__.py +6 -0
  70. highhxpack/storage/base.py +210 -0
  71. highhxpack/storage/migrations.py +188 -0
  72. highhxpack/storage/sqlite.py +934 -0
  73. highhxpack/storage/vector.py +87 -0
  74. highhxpack/utils/__init__.py +1 -0
  75. highhxpack/utils/hashing.py +31 -0
  76. highhxpack/utils/logging.py +52 -0
  77. highhxpack/utils/text.py +117 -0
  78. highhxpack/utils/timestamps.py +78 -0
  79. highhxpack/utils/validation.py +179 -0
  80. highhxpack-0.1.0.dist-info/METADATA +297 -0
  81. highhxpack-0.1.0.dist-info/RECORD +84 -0
  82. highhxpack-0.1.0.dist-info/WHEEL +4 -0
  83. highhxpack-0.1.0.dist-info/entry_points.txt +2 -0
  84. 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,6 @@
1
+ """Allow ``python -m highhxpack``."""
2
+
3
+ from highhxpack.cli.main import entrypoint
4
+
5
+ if __name__ == "__main__":
6
+ entrypoint()
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the package version."""
2
+
3
+ __version__ = "0.1.0"
@@ -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