visp-memory 0.3.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 (231) hide show
  1. visp_memory/__init__.py +35 -0
  2. visp_memory/capture/__init__.py +12 -0
  3. visp_memory/capture/bootstrap.py +366 -0
  4. visp_memory/capture/conversation.py +185 -0
  5. visp_memory/capture/git.py +620 -0
  6. visp_memory/capture/instructions.py +181 -0
  7. visp_memory/capture/tests.py +113 -0
  8. visp_memory/config.py +421 -0
  9. visp_memory/core/__init__.py +15 -0
  10. visp_memory/core/anchors.py +216 -0
  11. visp_memory/core/arcadedb_storage.py +1355 -0
  12. visp_memory/core/clock.py +58 -0
  13. visp_memory/core/compression.py +574 -0
  14. visp_memory/core/context_compiler.py +156 -0
  15. visp_memory/core/cross_repo.py +131 -0
  16. visp_memory/core/embeddings.py +388 -0
  17. visp_memory/core/hybrid_retrieval.py +351 -0
  18. visp_memory/core/indexing.py +121 -0
  19. visp_memory/core/injection.py +649 -0
  20. visp_memory/core/intent_evaluator.py +187 -0
  21. visp_memory/core/lifecycle.py +530 -0
  22. visp_memory/core/llm.py +96 -0
  23. visp_memory/core/memory.py +1096 -0
  24. visp_memory/core/memory_context.py +121 -0
  25. visp_memory/core/memory_import_export.py +180 -0
  26. visp_memory/core/model_router.py +126 -0
  27. visp_memory/core/neo4j_storage.py +1648 -0
  28. visp_memory/core/ranking.py +344 -0
  29. visp_memory/core/reflection.py +130 -0
  30. visp_memory/core/remote_storage.py +559 -0
  31. visp_memory/core/reporting.py +428 -0
  32. visp_memory/core/repository.py +148 -0
  33. visp_memory/core/storage.py +2605 -0
  34. visp_memory/core/task_brief.py +542 -0
  35. visp_memory/core/team.py +115 -0
  36. visp_memory/core/tokens.py +138 -0
  37. visp_memory/core/trust.py +231 -0
  38. visp_memory/hooks/__init__.py +52 -0
  39. visp_memory/hooks/aider.py +112 -0
  40. visp_memory/hooks/base.py +172 -0
  41. visp_memory/hooks/claude_code.py +140 -0
  42. visp_memory/hooks/claude_code_auto.py +277 -0
  43. visp_memory/hooks/codex.py +247 -0
  44. visp_memory/hooks/cursor.py +113 -0
  45. visp_memory/hooks/generic.py +208 -0
  46. visp_memory/interfaces/__init__.py +5 -0
  47. visp_memory/interfaces/cli.py +2693 -0
  48. visp_memory/interfaces/mcp.py +2104 -0
  49. visp_memory/layers/__init__.py +13 -0
  50. visp_memory/layers/base.py +71 -0
  51. visp_memory/layers/episodic.py +272 -0
  52. visp_memory/layers/intent.py +302 -0
  53. visp_memory/layers/semantic.py +327 -0
  54. visp_memory/quality/__init__.py +3 -0
  55. visp_memory/quality/conflict.py +104 -0
  56. visp_memory/quality/dedup.py +341 -0
  57. visp_memory/quality/reconcile.py +160 -0
  58. visp_memory/quality/secrets.py +230 -0
  59. visp_memory/recall/__init__.py +13 -0
  60. visp_memory/recall/graph.py +623 -0
  61. visp_memory/recall/proactive.py +450 -0
  62. visp_memory/server/__init__.py +3 -0
  63. visp_memory/server/app.py +685 -0
  64. visp_memory/server/auth.py +207 -0
  65. visp_memory/server/auth_store.py +426 -0
  66. visp_memory/server/authorization.py +92 -0
  67. visp_memory/server/routers/__init__.py +8 -0
  68. visp_memory/server/routers/ai.py +205 -0
  69. visp_memory/server/routers/authentication.py +302 -0
  70. visp_memory/server/routers/context.py +66 -0
  71. visp_memory/server/routers/diagnostics.py +385 -0
  72. visp_memory/server/routers/intents.py +286 -0
  73. visp_memory/server/routers/memories.py +693 -0
  74. visp_memory/server/routers/platform.py +88 -0
  75. visp_memory/server/routers/quality.py +201 -0
  76. visp_memory/server/routers/relationships.py +120 -0
  77. visp_memory/server/routers/repositories.py +389 -0
  78. visp_memory/server/routers/sessions.py +75 -0
  79. visp_memory/server/routers/teams.py +191 -0
  80. visp_memory/server/schemas.py +539 -0
  81. visp_memory/server/static/404.html +1 -0
  82. visp_memory/server/static/__next.__PAGE__.txt +9 -0
  83. visp_memory/server/static/__next._full.txt +19 -0
  84. visp_memory/server/static/__next._head.txt +5 -0
  85. visp_memory/server/static/__next._index.txt +7 -0
  86. visp_memory/server/static/__next._tree.txt +2 -0
  87. visp_memory/server/static/_next/static/OGSfUhHWJoUMwAXowgbph/_buildManifest.js +11 -0
  88. visp_memory/server/static/_next/static/OGSfUhHWJoUMwAXowgbph/_clientMiddlewareManifest.js +1 -0
  89. visp_memory/server/static/_next/static/OGSfUhHWJoUMwAXowgbph/_ssgManifest.js +1 -0
  90. visp_memory/server/static/_next/static/chunks/008yi10v1q4dx.js +2 -0
  91. visp_memory/server/static/_next/static/chunks/05-c3ty_6dwfk.js +1 -0
  92. visp_memory/server/static/_next/static/chunks/07-r-9f0qrcuh.js +1 -0
  93. visp_memory/server/static/_next/static/chunks/0_8y92xg60ach.js +31 -0
  94. visp_memory/server/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  95. visp_memory/server/static/_next/static/chunks/0ha5sx_ga-3xm.css +1 -0
  96. visp_memory/server/static/_next/static/chunks/0ihy_oxbd_ql7.js +1 -0
  97. visp_memory/server/static/_next/static/chunks/0sbgmos1me2w7.js +1 -0
  98. visp_memory/server/static/_next/static/chunks/10mw-ef74o_0e.js +1 -0
  99. visp_memory/server/static/_next/static/chunks/16wtvn_4n5ryb.js +1 -0
  100. visp_memory/server/static/_next/static/chunks/181fjtiwe9leb.js +1 -0
  101. visp_memory/server/static/_next/static/chunks/1os6n3cys6qq2.js +1 -0
  102. visp_memory/server/static/_next/static/chunks/229t_xxklg5dq.js +1 -0
  103. visp_memory/server/static/_next/static/chunks/23x4zp3dciho5.js +1 -0
  104. visp_memory/server/static/_next/static/chunks/25o46h8mdjlrg.js +1 -0
  105. visp_memory/server/static/_next/static/chunks/27jktro2p5rq9.js +4 -0
  106. visp_memory/server/static/_next/static/chunks/28beg3dm4qs6d.js +9 -0
  107. visp_memory/server/static/_next/static/chunks/2hkl13_hoi6rf.js +1 -0
  108. visp_memory/server/static/_next/static/chunks/2otspn0_vpeiy.js +1 -0
  109. visp_memory/server/static/_next/static/chunks/2xzui1nn6_dgk.js +1 -0
  110. visp_memory/server/static/_next/static/chunks/3924qjq6v2pkr.js +5 -0
  111. visp_memory/server/static/_next/static/chunks/3q-_2alq3i82j.js +1 -0
  112. visp_memory/server/static/_next/static/chunks/3sdblimdmjxu9.js +1 -0
  113. visp_memory/server/static/_next/static/chunks/3xdh3y-yb-_j8.js +1 -0
  114. visp_memory/server/static/_next/static/chunks/44rth4b4ij8_e.js +1 -0
  115. visp_memory/server/static/_next/static/chunks/turbopack-37zifqtg7l6ev.js +1 -0
  116. visp_memory/server/static/_not-found/__next._full.txt +17 -0
  117. visp_memory/server/static/_not-found/__next._head.txt +5 -0
  118. visp_memory/server/static/_not-found/__next._index.txt +7 -0
  119. visp_memory/server/static/_not-found/__next._not-found.__PAGE__.txt +5 -0
  120. visp_memory/server/static/_not-found/__next._not-found.txt +5 -0
  121. visp_memory/server/static/_not-found/__next._tree.txt +2 -0
  122. visp_memory/server/static/_not-found.html +1 -0
  123. visp_memory/server/static/_not-found.txt +17 -0
  124. visp_memory/server/static/apple-icon.png +0 -0
  125. visp_memory/server/static/auth/__next._full.txt +21 -0
  126. visp_memory/server/static/auth/__next._head.txt +5 -0
  127. visp_memory/server/static/auth/__next._index.txt +7 -0
  128. visp_memory/server/static/auth/__next._tree.txt +2 -0
  129. visp_memory/server/static/auth/__next.auth.__PAGE__.txt +9 -0
  130. visp_memory/server/static/auth/__next.auth.txt +5 -0
  131. visp_memory/server/static/auth.html +1 -0
  132. visp_memory/server/static/auth.txt +21 -0
  133. visp_memory/server/static/brief/__next._full.txt +21 -0
  134. visp_memory/server/static/brief/__next._head.txt +5 -0
  135. visp_memory/server/static/brief/__next._index.txt +7 -0
  136. visp_memory/server/static/brief/__next._tree.txt +2 -0
  137. visp_memory/server/static/brief/__next.brief.__PAGE__.txt +9 -0
  138. visp_memory/server/static/brief/__next.brief.txt +5 -0
  139. visp_memory/server/static/brief.html +1 -0
  140. visp_memory/server/static/brief.txt +21 -0
  141. visp_memory/server/static/graph/__next._full.txt +21 -0
  142. visp_memory/server/static/graph/__next._head.txt +5 -0
  143. visp_memory/server/static/graph/__next._index.txt +7 -0
  144. visp_memory/server/static/graph/__next._tree.txt +2 -0
  145. visp_memory/server/static/graph/__next.graph.__PAGE__.txt +9 -0
  146. visp_memory/server/static/graph/__next.graph.txt +5 -0
  147. visp_memory/server/static/graph.html +1 -0
  148. visp_memory/server/static/graph.txt +21 -0
  149. visp_memory/server/static/health/__next._full.txt +21 -0
  150. visp_memory/server/static/health/__next._head.txt +5 -0
  151. visp_memory/server/static/health/__next._index.txt +7 -0
  152. visp_memory/server/static/health/__next._tree.txt +2 -0
  153. visp_memory/server/static/health/__next.health.__PAGE__.txt +9 -0
  154. visp_memory/server/static/health/__next.health.txt +5 -0
  155. visp_memory/server/static/health.html +1 -0
  156. visp_memory/server/static/health.txt +21 -0
  157. visp_memory/server/static/icon-dark-32x32.png +0 -0
  158. visp_memory/server/static/icon-light-32x32.png +0 -0
  159. visp_memory/server/static/icon.svg +26 -0
  160. visp_memory/server/static/index.html +1 -0
  161. visp_memory/server/static/index.txt +19 -0
  162. visp_memory/server/static/integrations/__next._full.txt +21 -0
  163. visp_memory/server/static/integrations/__next._head.txt +5 -0
  164. visp_memory/server/static/integrations/__next._index.txt +7 -0
  165. visp_memory/server/static/integrations/__next._tree.txt +2 -0
  166. visp_memory/server/static/integrations/__next.integrations.__PAGE__.txt +9 -0
  167. visp_memory/server/static/integrations/__next.integrations.txt +5 -0
  168. visp_memory/server/static/integrations.html +1 -0
  169. visp_memory/server/static/integrations.txt +21 -0
  170. visp_memory/server/static/intelligence/__next._full.txt +21 -0
  171. visp_memory/server/static/intelligence/__next._head.txt +5 -0
  172. visp_memory/server/static/intelligence/__next._index.txt +7 -0
  173. visp_memory/server/static/intelligence/__next._tree.txt +2 -0
  174. visp_memory/server/static/intelligence/__next.intelligence.__PAGE__.txt +9 -0
  175. visp_memory/server/static/intelligence/__next.intelligence.txt +5 -0
  176. visp_memory/server/static/intelligence.html +1 -0
  177. visp_memory/server/static/intelligence.txt +21 -0
  178. visp_memory/server/static/intents/__next._full.txt +21 -0
  179. visp_memory/server/static/intents/__next._head.txt +5 -0
  180. visp_memory/server/static/intents/__next._index.txt +7 -0
  181. visp_memory/server/static/intents/__next._tree.txt +2 -0
  182. visp_memory/server/static/intents/__next.intents.__PAGE__.txt +9 -0
  183. visp_memory/server/static/intents/__next.intents.txt +5 -0
  184. visp_memory/server/static/intents.html +1 -0
  185. visp_memory/server/static/intents.txt +21 -0
  186. visp_memory/server/static/memories/__next._full.txt +21 -0
  187. visp_memory/server/static/memories/__next._head.txt +5 -0
  188. visp_memory/server/static/memories/__next._index.txt +7 -0
  189. visp_memory/server/static/memories/__next._tree.txt +2 -0
  190. visp_memory/server/static/memories/__next.memories.__PAGE__.txt +9 -0
  191. visp_memory/server/static/memories/__next.memories.txt +5 -0
  192. visp_memory/server/static/memories.html +1 -0
  193. visp_memory/server/static/memories.txt +21 -0
  194. visp_memory/server/static/projects/__next._full.txt +21 -0
  195. visp_memory/server/static/projects/__next._head.txt +5 -0
  196. visp_memory/server/static/projects/__next._index.txt +7 -0
  197. visp_memory/server/static/projects/__next._tree.txt +2 -0
  198. visp_memory/server/static/projects/__next.projects.__PAGE__.txt +9 -0
  199. visp_memory/server/static/projects/__next.projects.txt +5 -0
  200. visp_memory/server/static/projects.html +1 -0
  201. visp_memory/server/static/projects.txt +21 -0
  202. visp_memory/server/static/recall/__next._full.txt +22 -0
  203. visp_memory/server/static/recall/__next._head.txt +5 -0
  204. visp_memory/server/static/recall/__next._index.txt +7 -0
  205. visp_memory/server/static/recall/__next._tree.txt +2 -0
  206. visp_memory/server/static/recall/__next.recall.__PAGE__.txt +9 -0
  207. visp_memory/server/static/recall/__next.recall.txt +6 -0
  208. visp_memory/server/static/recall.html +1 -0
  209. visp_memory/server/static/recall.txt +22 -0
  210. visp_memory/server/static/settings/__next._full.txt +21 -0
  211. visp_memory/server/static/settings/__next._head.txt +5 -0
  212. visp_memory/server/static/settings/__next._index.txt +7 -0
  213. visp_memory/server/static/settings/__next._tree.txt +2 -0
  214. visp_memory/server/static/settings/__next.settings.__PAGE__.txt +9 -0
  215. visp_memory/server/static/settings/__next.settings.txt +5 -0
  216. visp_memory/server/static/settings.html +1 -0
  217. visp_memory/server/static/settings.txt +21 -0
  218. visp_memory/server/static/users/__next._full.txt +21 -0
  219. visp_memory/server/static/users/__next._head.txt +5 -0
  220. visp_memory/server/static/users/__next._index.txt +7 -0
  221. visp_memory/server/static/users/__next._tree.txt +2 -0
  222. visp_memory/server/static/users/__next.users.__PAGE__.txt +9 -0
  223. visp_memory/server/static/users/__next.users.txt +5 -0
  224. visp_memory/server/static/users.html +1 -0
  225. visp_memory/server/static/users.txt +21 -0
  226. visp_memory-0.3.0.dist-info/METADATA +818 -0
  227. visp_memory-0.3.0.dist-info/RECORD +231 -0
  228. visp_memory-0.3.0.dist-info/WHEEL +4 -0
  229. visp_memory-0.3.0.dist-info/entry_points.txt +3 -0
  230. visp_memory-0.3.0.dist-info/licenses/LICENSE +202 -0
  231. visp_memory-0.3.0.dist-info/licenses/NOTICE +15 -0
@@ -0,0 +1,35 @@
1
+ """
2
+ Visp Memory - Human-inspired memory system for LLMs
3
+
4
+ A cognitive memory architecture that mimics how humans remember:
5
+ - Episodic: Compressed events and experiences
6
+ - Semantic: Extracted knowledge and patterns
7
+ - Intent: Current direction and priorities
8
+
9
+ Designed to give LLMs persistent context without re-analyzing everything.
10
+ """
11
+
12
+ try:
13
+ from importlib.metadata import PackageNotFoundError, version
14
+ except ImportError: # pragma: no cover - Python 3.10+ includes importlib.metadata
15
+ PackageNotFoundError = Exception
16
+ version = None
17
+
18
+ from visp_memory.config import MemoryConfig
19
+ from visp_memory.core.memory import Memory
20
+
21
+ # Single source of truth is pyproject.toml; this is only reached when the package is
22
+ # not installed (e.g. running straight from a source checkout).
23
+ _FALLBACK_VERSION = "0.3.0"
24
+
25
+ try:
26
+ # Must match the distribution name in pyproject.toml. Querying a name that is not
27
+ # installed raises PackageNotFoundError and silently falls back, so a stale name
28
+ # here reports a stale version rather than failing loudly -- which is exactly what
29
+ # happened after the rename: the lookup asked for "visp-memory-mcp" and every
30
+ # install reported the hardcoded fallback instead of its real version.
31
+ __version__ = version("visp-memory") if version else _FALLBACK_VERSION
32
+ except PackageNotFoundError:
33
+ __version__ = _FALLBACK_VERSION
34
+
35
+ __all__ = ["Memory", "MemoryConfig", "__version__"]
@@ -0,0 +1,12 @@
1
+ """
2
+ Automatic Capture System
3
+
4
+ Captures memories automatically from development activity:
5
+ - Git commits, merges, and branch activity
6
+ - Test results and failures
7
+ - Conversation transcripts
8
+ """
9
+
10
+ from visp_memory.capture.git import GitCapture
11
+
12
+ __all__ = ["GitCapture"]
@@ -0,0 +1,366 @@
1
+ """First-run project bootstrap: turn existing history into useful memory.
2
+
3
+ An empty memory store is worth nothing, and nobody will hand-write memories for weeks
4
+ to find out whether the idea works. Every repository already contains the raw material,
5
+ though: commit history records what was tried, what broke, what was reverted, and which
6
+ files keep needing repair.
7
+
8
+ This module mines that, but **selectively**. A bulk import of every commit would be the
9
+ same mistake the injection policy exists to prevent -- most commits are ``chore: bump
10
+ version`` and ``style: satisfy ruff``, and a store full of those makes recall worse, not
11
+ better. The research this project is built on is explicit that unfiltered context is
12
+ worth less than no context, so the bootstrap keeps only commits carrying signal:
13
+
14
+ - **Reverts** become warnings. A revert is the strongest available evidence that an
15
+ approach was tried and failed, and it is exactly what an assistant needs to not
16
+ suggest it again.
17
+ - **Repeatedly fixed files** become warnings. A file with several independent bug fixes
18
+ is empirically fragile, which no amount of reading the current code reveals.
19
+ - **Commits that explain themselves** become decisions. A message containing "because",
20
+ "so that", or "instead of" carries the rationale that code alone cannot.
21
+ - **Fixes and features** become episodes at their natural importance.
22
+ - Everything else is skipped and counted, so the report is honest about what was dropped.
23
+
24
+ Instruction files (``README``, ``CLAUDE.md``, ``AGENTS.md``, ADRs) are ingested through
25
+ :mod:`visp_memory.capture.instructions`, which already handles sectioning and dedup.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import re
32
+ from collections import Counter, defaultdict
33
+ from dataclasses import dataclass, field
34
+ from pathlib import Path
35
+ from typing import Any, Optional
36
+
37
+ from visp_memory.core.trust import Provenance, provenance_tag
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # Everything the bootstrap writes is derived from the repository itself, which is the
42
+ # ground truth being described -- not from any external source. Labelling it explicitly
43
+ # means the trust layer can tell it apart from a memory that arrived via a fetched page.
44
+ _DERIVED_TAG = provenance_tag(Provenance.DERIVED)
45
+
46
+ # Conventional-commit prefixes with no durable recall value. A memory store full of
47
+ # these dilutes every query that follows.
48
+ LOW_SIGNAL_PREFIXES = frozenset({"chore", "style", "ci", "build", "deps", "release"})
49
+
50
+ # Messages that are pure mechanics regardless of prefix.
51
+ _LOW_SIGNAL_PATTERNS = (
52
+ re.compile(r"^\s*(bump|update|upgrade|pin)\b.*\bversion\b", re.I),
53
+ re.compile(r"^\s*v?\d+\.\d+\.\d+\s*$"),
54
+ re.compile(r"^\s*merge\b", re.I),
55
+ re.compile(r"^\s*(wip|tmp|temp|fixup|squash)\b", re.I),
56
+ re.compile(r"^\s*(initial commit|first commit)\s*$", re.I),
57
+ )
58
+
59
+ _REVERT_RE = re.compile(r"^\s*(revert|rollback)\b", re.I)
60
+
61
+ # Rationale markers: the words people use when explaining *why*, which is the part that
62
+ # never survives in the code itself.
63
+ _RATIONALE_RE = re.compile(
64
+ r"\b(because|so that|instead of|rather than|in order to|the reason|"
65
+ r"we decided|decided to|to avoid|to prevent|turns out)\b",
66
+ re.I,
67
+ )
68
+
69
+ _FIX_RE = re.compile(r"^\s*(fix|bug|hotfix|patch)\b", re.I)
70
+
71
+ # A file needs at least this many independent fix commits before "historically fragile"
72
+ # is a claim rather than a coincidence.
73
+ FRAGILE_FILE_MIN_FIXES = 3
74
+
75
+ # Files that change constantly for uninteresting reasons.
76
+ _IGNORED_PATH_RE = re.compile(
77
+ r"(^|/)(package-lock\.json|uv\.lock|poetry\.lock|yarn\.lock|"
78
+ r"CHANGELOG\.md|\.github/)", re.I
79
+ )
80
+
81
+ # Fragility is a claim about *code*. Documentation accumulates "fix" commits for typos
82
+ # and stale instructions, which says nothing about whether editing it is risky; mining
83
+ # this repository flagged README.md as historically fragile, which is worse than useless.
84
+ _NON_CODE_PATH_RE = re.compile(
85
+ r"(\.(md|rst|txt|json|ya?ml|toml|ini|cfg|lock|svg|png|jpe?g|gif|ico)$"
86
+ r"|(^|/)(docs?|examples?)/)",
87
+ re.I,
88
+ )
89
+
90
+ DEFAULT_COMMIT_LIMIT = 400
91
+
92
+
93
+ @dataclass
94
+ class BootstrapReport:
95
+ """What the first run found, including what it deliberately ignored."""
96
+
97
+ commits_scanned: int = 0
98
+ skipped_low_signal: int = 0
99
+ decisions: int = 0
100
+ warnings: int = 0
101
+ events: int = 0
102
+ instruction_sections: int = 0
103
+ span_days: int = 0
104
+ memory_ids: list[str] = field(default_factory=list)
105
+ errors: list[str] = field(default_factory=list)
106
+
107
+ @property
108
+ def total(self) -> int:
109
+ return self.decisions + self.warnings + self.events + self.instruction_sections
110
+
111
+ def headline(self) -> str:
112
+ """The line a first-time user sees. It has to be true."""
113
+ if not self.total:
114
+ return "No memories imported — this repository has no history worth seeding yet."
115
+
116
+ span = ""
117
+ if self.span_days >= 60:
118
+ span = f" spanning {self.span_days // 30} months"
119
+ elif self.span_days >= 1:
120
+ span = f" spanning {self.span_days} days"
121
+
122
+ return (
123
+ f"Imported {self.total} memories from {self.commits_scanned} commits{span}."
124
+ )
125
+
126
+ def detail_lines(self) -> list[str]:
127
+ def plural(count: int, singular: str, suffix: str = "s") -> str:
128
+ return f"{count} {singular}{'' if count == 1 else suffix}"
129
+
130
+ lines = []
131
+ if self.warnings:
132
+ lines.append(
133
+ f"{plural(self.warnings, 'warning')} "
134
+ "(reverts and repeatedly-fixed files)"
135
+ )
136
+ if self.decisions:
137
+ lines.append(
138
+ f"{plural(self.decisions, 'decision')} "
139
+ "(commits that explain their reasoning)"
140
+ )
141
+ if self.events:
142
+ lines.append(f"{plural(self.events, 'event')} (fixes and features)")
143
+ if self.instruction_sections:
144
+ lines.append(
145
+ f"{plural(self.instruction_sections, 'section')} from instruction files"
146
+ )
147
+ if self.skipped_low_signal:
148
+ lines.append(f"{plural(self.skipped_low_signal, 'low-signal commit')} skipped")
149
+ return lines
150
+
151
+ def as_dict(self) -> dict[str, Any]:
152
+ return {
153
+ "commits_scanned": self.commits_scanned,
154
+ "skipped_low_signal": self.skipped_low_signal,
155
+ "decisions": self.decisions,
156
+ "warnings": self.warnings,
157
+ "events": self.events,
158
+ "instruction_sections": self.instruction_sections,
159
+ "span_days": self.span_days,
160
+ "total": self.total,
161
+ "errors": self.errors,
162
+ }
163
+
164
+
165
+ def is_low_signal(message: str) -> bool:
166
+ """Report whether a commit message carries no durable recall value."""
167
+ subject = (message or "").strip().splitlines()[0] if (message or "").strip() else ""
168
+ if not subject:
169
+ return True
170
+
171
+ for pattern in _LOW_SIGNAL_PATTERNS:
172
+ if pattern.search(subject):
173
+ return True
174
+
175
+ prefix = subject.split(":", 1)[0].strip().lower()
176
+ # Strip a conventional-commit scope, e.g. "chore(deps)".
177
+ prefix = re.sub(r"\(.*\)$", "", prefix).strip()
178
+ if prefix in LOW_SIGNAL_PREFIXES:
179
+ return True
180
+
181
+ # A bare subject with no verb-like content is noise regardless of prefix.
182
+ return len(subject) < 12
183
+
184
+
185
+ def _changed_paths(commit) -> list[str]:
186
+ try:
187
+ return [p for p in commit.stats.files.keys() if not _IGNORED_PATH_RE.search(str(p))]
188
+ except Exception:
189
+ return []
190
+
191
+
192
+ def bootstrap_project(
193
+ memory: Any,
194
+ repo_path: Optional[Path] = None,
195
+ *,
196
+ limit: int = DEFAULT_COMMIT_LIMIT,
197
+ include_instructions: bool = True,
198
+ ) -> BootstrapReport:
199
+ """Seed a memory store from an existing repository.
200
+
201
+ Safe to run more than once: commits and instruction sections are content-hashed by
202
+ the existing capture manifest, so re-running updates rather than duplicates.
203
+ """
204
+ report = BootstrapReport()
205
+ repo_path = Path(repo_path or Path.cwd())
206
+
207
+ try:
208
+ import git # noqa: F401 (presence check)
209
+
210
+ from visp_memory.capture.git import GitCapture
211
+ except ImportError:
212
+ report.errors.append(
213
+ "git capture unavailable — install with: pip install 'visp-memory[capture]'"
214
+ )
215
+ return report
216
+
217
+ try:
218
+ capture = GitCapture(memory, repo_path=repo_path)
219
+ except Exception as exc:
220
+ report.errors.append(f"not a git repository ({exc})")
221
+ return report
222
+
223
+ try:
224
+ commits = list(capture.repo.iter_commits(max_count=limit))
225
+ except Exception as exc:
226
+ # A repository with no commits yet raises here; that is not an error worth
227
+ # showing a first-time user as a failure.
228
+ logger.debug("no commits to bootstrap from: %s", exc)
229
+ commits = []
230
+
231
+ if commits:
232
+ newest = commits[0].committed_datetime
233
+ oldest = commits[-1].committed_datetime
234
+ report.span_days = max(0, (newest - oldest).days)
235
+
236
+ fix_counts: Counter[str] = Counter()
237
+ fix_examples: dict[str, str] = {}
238
+ reverts: list[tuple[str, list[str]]] = []
239
+ rationales: list[tuple[str, str]] = []
240
+ plain_events: list[Any] = []
241
+
242
+ for commit in commits:
243
+ if len(commit.parents) > 1:
244
+ continue # merge commits duplicate their branch's content
245
+
246
+ report.commits_scanned += 1
247
+ message = (commit.message or "").strip()
248
+ subject = message.splitlines()[0] if message else ""
249
+
250
+ if is_low_signal(message):
251
+ report.skipped_low_signal += 1
252
+ continue
253
+
254
+ paths = _changed_paths(commit)
255
+
256
+ if _REVERT_RE.search(subject):
257
+ reverts.append((subject, paths))
258
+ continue
259
+
260
+ if _FIX_RE.search(subject):
261
+ for path in paths:
262
+ if _NON_CODE_PATH_RE.search(str(path)):
263
+ continue
264
+ fix_counts[path] += 1
265
+ fix_examples.setdefault(path, subject)
266
+
267
+ if _RATIONALE_RE.search(message):
268
+ rationales.append((subject, message))
269
+ continue
270
+
271
+ plain_events.append(commit)
272
+
273
+ # --- Warnings: reverts -------------------------------------------------------
274
+ for subject, paths in reverts:
275
+ scope = ", ".join(sorted(paths)[:3]) if paths else "the codebase"
276
+ try:
277
+ memory_id = memory.warn(
278
+ area=scope,
279
+ warning=(
280
+ f"Previously reverted: {subject}. This approach was tried and backed "
281
+ f"out — check the history before proposing it again."
282
+ ),
283
+ severity=0.8,
284
+ tags=[_DERIVED_TAG, "git"],
285
+ )
286
+ report.memory_ids.append(memory_id)
287
+ report.warnings += 1
288
+ except Exception as exc:
289
+ report.errors.append(f"revert warning failed: {exc}")
290
+
291
+ # --- Warnings: repeatedly fixed files ----------------------------------------
292
+ for path, count in fix_counts.most_common():
293
+ if count < FRAGILE_FILE_MIN_FIXES:
294
+ continue
295
+ try:
296
+ memory_id = memory.warn(
297
+ area=path,
298
+ warning=(
299
+ f"Historically fragile: {count} separate bug fixes have landed here "
300
+ f"(e.g. \"{fix_examples.get(path, '')}\"). Change with care and check "
301
+ f"for regressions."
302
+ ),
303
+ severity=0.7,
304
+ tags=[_DERIVED_TAG, "git"],
305
+ )
306
+ report.memory_ids.append(memory_id)
307
+ report.warnings += 1
308
+ except Exception as exc:
309
+ report.errors.append(f"fragile-file warning failed: {exc}")
310
+
311
+ # --- Decisions: commits that explain themselves ------------------------------
312
+ for subject, message in rationales:
313
+ body = message[len(subject) :].strip()
314
+ reason = body or subject
315
+ try:
316
+ memory_id = memory.decision(
317
+ what=subject, why=reason[:1000], tags=[_DERIVED_TAG, "git"]
318
+ )
319
+ report.memory_ids.append(memory_id)
320
+ report.decisions += 1
321
+ except Exception as exc:
322
+ report.errors.append(f"decision capture failed: {exc}")
323
+
324
+ # --- Events: the remaining meaningful commits --------------------------------
325
+ for commit in plain_events:
326
+ try:
327
+ memory_id = capture.on_commit(commit.hexsha)
328
+ if memory_id:
329
+ report.memory_ids.append(memory_id)
330
+ report.events += 1
331
+ except Exception as exc:
332
+ report.errors.append(f"commit {commit.hexsha[:7]} failed: {exc}")
333
+
334
+ # --- Instruction files --------------------------------------------------------
335
+ if include_instructions:
336
+ try:
337
+ from visp_memory.capture.instructions import ingest_instructions
338
+
339
+ ingest = ingest_instructions(memory, root=repo_path)
340
+ report.instruction_sections = ingest.stored
341
+ report.memory_ids.extend(ingest.memory_ids)
342
+ except Exception as exc:
343
+ report.errors.append(f"instruction ingest failed: {exc}")
344
+
345
+ return report
346
+
347
+
348
+ def summarize_fragile_files(memory: Any, repo_path: Optional[Path] = None) -> dict[str, int]:
349
+ """Return fix-count-per-file, for reporting without writing memories."""
350
+ counts: dict[str, int] = defaultdict(int)
351
+ try:
352
+ import git
353
+
354
+ repo = git.Repo(Path(repo_path or Path.cwd()), search_parent_directories=True)
355
+ except Exception:
356
+ return {}
357
+
358
+ for commit in repo.iter_commits(max_count=DEFAULT_COMMIT_LIMIT):
359
+ if len(commit.parents) > 1:
360
+ continue
361
+ subject = (commit.message or "").strip().splitlines()[0:1]
362
+ if not subject or not _FIX_RE.search(subject[0]):
363
+ continue
364
+ for path in _changed_paths(commit):
365
+ counts[path] += 1
366
+ return dict(counts)
@@ -0,0 +1,185 @@
1
+ """
2
+ Conversation Capture Module
3
+
4
+ Parses LLM conversation logs (e.g. from Claude Desktop, ChatGPT exports)
5
+ to extract structured memories.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional
12
+
13
+ from visp_memory.capture.git import CaptureManifest, capture_content_hash
14
+ from visp_memory.core.llm import LLMClient, create_llm_client
15
+ from visp_memory.core.memory import Memory
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ConversationCapture:
21
+ """Captures memories from conversation logs."""
22
+
23
+ def __init__(self, memory: Memory):
24
+ self.memory = memory
25
+ self.config = memory.config.capture
26
+ self._client: Optional[LLMClient] = None
27
+
28
+ def _get_client(self) -> LLMClient:
29
+ if self._client:
30
+ return self._client
31
+
32
+ provider = self.config.llm_provider
33
+ # Fallback to compression provider if not set in capture
34
+ if not provider:
35
+ provider = self.memory.config.compression.llm_provider
36
+
37
+ if not provider:
38
+ raise ValueError(
39
+ "No LLM provider configured for conversation capture. "
40
+ "Set VISP_MEMORY_CAPTURE_LLM_PROVIDER."
41
+ )
42
+
43
+ self._client = create_llm_client(
44
+ provider=provider,
45
+ model=self.config.llm_model or self.memory.config.compression.llm_model,
46
+ # Reuse generic key if applicable, or rely on env.
47
+ api_key=self.memory.config.embedding.api_key,
48
+ )
49
+ return self._client
50
+
51
+ def parse_file(self, path: str, dry_run: bool = False) -> Dict[str, Any]:
52
+ """Parse a conversation file and record memories."""
53
+ path = Path(path)
54
+ if not path.exists():
55
+ raise FileNotFoundError(f"File not found: {path}")
56
+
57
+ content = path.read_text(encoding="utf-8")
58
+ return self.parse_text(content, source=path.name, dry_run=dry_run)
59
+
60
+ def parse_text(
61
+ self, text: str, source: str = "conversation", dry_run: bool = False
62
+ ) -> Dict[str, Any]:
63
+ """Parse conversation text and record memories."""
64
+ manifest = CaptureManifest(self.memory)
65
+ content_hash = capture_content_hash({"source": source, "text": text})
66
+ status, entry = manifest.check("conversation", source, content_hash)
67
+ if status == "unchanged" and not dry_run:
68
+ return {
69
+ "decisions": 0,
70
+ "learnings": 0,
71
+ "bugs": 0,
72
+ "tasks": 0,
73
+ "raw": {},
74
+ "capture_manifest": {
75
+ "status": "unchanged",
76
+ "output_memory_ids": entry.get("output_memory_ids", []) if entry else [],
77
+ },
78
+ }
79
+
80
+ client = self._get_client()
81
+
82
+ system_prompt = """You are a Memory Extraction System.
83
+ Your goal is to analyze the user's conversation with an AI and extract structured memories.
84
+
85
+ Extract these categories:
86
+ 1. DECISIONS: Key architectural or design choices made (what, why, alternatives).
87
+ 2. LEARNINGS: New facts, patterns, or technical knowledge gained.
88
+ 3. BUGS: Fixed bugs or discovered issues (description, cause, fix).
89
+ 4. TASKS: Items marked as todo or future work.
90
+
91
+ Output strictly valid JSON with this schema:
92
+ {
93
+ "decisions": [{"what": "...", "why": "...", "alternatives": ["..."]}],
94
+ "learnings": [{"knowledge": "...", "category": "fact/pattern", "importance": 0.0-1.0}],
95
+ "bugs": [{"description": "...", "cause": "...", "fix": "..."}],
96
+ "tasks": [{"description": "...", "status": "todo"}]
97
+ }
98
+ If nothing relevant is found for a category, return an empty list.
99
+ """
100
+
101
+ prompt = (
102
+ "Analyze this conversation log and extract memories:\n\n"
103
+ f"{text[:20000]}"
104
+ )
105
+
106
+ try:
107
+ response = client.completion(
108
+ prompt=prompt,
109
+ system_prompt=system_prompt,
110
+ response_format={"type": "json_object"}
111
+ if hasattr(client, "client") and hasattr(client.client, "chat")
112
+ else None, # Hint for OpenAI
113
+ )
114
+
115
+ # Clean response if markdown code block
116
+ clean_resp = response.replace("```json", "").replace("```", "").strip()
117
+ data = json.loads(clean_resp)
118
+
119
+ summary = {
120
+ "decisions": len(data.get("decisions", [])),
121
+ "learnings": len(data.get("learnings", [])),
122
+ "bugs": len(data.get("bugs", [])),
123
+ "tasks": len(data.get("tasks", [])),
124
+ "raw": data,
125
+ }
126
+
127
+ if dry_run:
128
+ return summary
129
+
130
+ # Record Memories
131
+ repo_id = self.memory.config.repo_id
132
+ output_memory_ids = []
133
+
134
+ for d in data.get("decisions", []):
135
+ memory_id = self.memory.decision(
136
+ what=d["what"],
137
+ why=d.get("why", "Unknown"),
138
+ alternatives=d.get("alternatives"),
139
+ repo_id=repo_id,
140
+ )
141
+ output_memory_ids.append(str(memory_id))
142
+
143
+ for learning in data.get("learnings", []):
144
+ memory_id = self.memory.learn(
145
+ knowledge=learning["knowledge"],
146
+ category=learning.get("category", "fact"),
147
+ importance=learning.get("importance", 0.5),
148
+ repo_id=repo_id,
149
+ )
150
+ output_memory_ids.append(str(memory_id))
151
+
152
+ for b in data.get("bugs", []):
153
+ # episodic.record() accepts `context` (not `metadata`), and
154
+ # "bug_found" is the valid EpisodeCategory for a discovered bug.
155
+ memory_id = self.memory.record(
156
+ event=f"Bug: {b['description']}",
157
+ category="bug_found",
158
+ context={"cause": b.get("cause"), "fix": b.get("fix")},
159
+ repo_id=repo_id,
160
+ )
161
+ output_memory_ids.append(str(memory_id))
162
+
163
+ for task in data.get("tasks", []):
164
+ memory_id = self.memory.intent.set_goal(
165
+ goal=task["description"],
166
+ repo_id=repo_id,
167
+ context={
168
+ "captured_as": "task",
169
+ "source": source,
170
+ "status": task.get("status", "todo"),
171
+ },
172
+ )
173
+ output_memory_ids.append(str(memory_id))
174
+
175
+ manifest.record("conversation", source, content_hash, output_memory_ids, status=status)
176
+ summary["capture_manifest"] = {
177
+ "status": status,
178
+ "output_memory_ids": output_memory_ids,
179
+ }
180
+
181
+ return summary
182
+
183
+ except Exception as e:
184
+ logger.error(f"Failed to parse conversation: {e}")
185
+ raise